From 3024aaa8a1a67ba04f2ad53725f4d4da599dbf25 Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Tue, 6 May 2025 10:43:40 +0200 Subject: [PATCH 1/2] der: add macro for `APPLICATION`, `CONTEXT-SPECIFIC` and `PRIVATE` tags --- der/src/asn1.rs | 4 + der/src/asn1/application.rs | 14 ++ der/src/asn1/context_specific.rs | 267 ++----------------------------- der/src/asn1/internal_macros.rs | 262 ++++++++++++++++++++++++++++++ der/src/asn1/private.rs | 14 ++ 5 files changed, 309 insertions(+), 252 deletions(-) create mode 100644 der/src/asn1/application.rs create mode 100644 der/src/asn1/private.rs diff --git a/der/src/asn1.rs b/der/src/asn1.rs index 22cc4d0ad..60a7b2dec 100644 --- a/der/src/asn1.rs +++ b/der/src/asn1.rs @@ -5,6 +5,7 @@ mod internal_macros; mod any; +mod application; pub(crate) mod bit_string; #[cfg(feature = "alloc")] mod bmp_string; @@ -21,6 +22,7 @@ mod octet_string; mod oid; mod optional; mod printable_string; +mod private; #[cfg(feature = "real")] mod real; mod sequence; @@ -33,6 +35,7 @@ mod videotex_string; pub use self::{ any::AnyRef, + application::{Application, ApplicationRef}, bit_string::{BitStringIter, BitStringRef}, choice::Choice, context_specific::{ContextSpecific, ContextSpecificRef}, @@ -43,6 +46,7 @@ pub use self::{ null::Null, octet_string::OctetStringRef, printable_string::PrintableStringRef, + private::{Private, PrivateRef}, sequence::{Sequence, SequenceRef}, sequence_of::{SequenceOf, SequenceOfIter}, set_of::{SetOf, SetOfIter}, diff --git a/der/src/asn1/application.rs b/der/src/asn1/application.rs new file mode 100644 index 000000000..93becff6a --- /dev/null +++ b/der/src/asn1/application.rs @@ -0,0 +1,14 @@ +//! Application class field. + +use crate::{ + Choice, Class, Decode, DecodeValue, DerOrd, Encode, EncodeValue, EncodeValueRef, Error, Header, + Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, asn1::AnyRef, + tag::IsConstructed, +}; +use core::cmp::Ordering; + +#[cfg(doc)] +use crate::ErrorKind; + +impl_custom_class!(Application, Application, "APPLICATION", "0b01000000"); +impl_custom_class_ref!(ApplicationRef, Application, "APPLICATION", "0b01000000"); diff --git a/der/src/asn1/context_specific.rs b/der/src/asn1/context_specific.rs index 626272cdc..c6c4e3266 100644 --- a/der/src/asn1/context_specific.rs +++ b/der/src/asn1/context_specific.rs @@ -7,258 +7,21 @@ use crate::{ }; use core::cmp::Ordering; -/// Context-specific field which wraps an owned inner value. -/// -/// This type decodes/encodes a field which is specific to a particular context -/// and is identified by a [`TagNumber`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub struct ContextSpecific { - /// Context-specific tag number sans the leading `0b10000000` class - /// identifier bit and `0b100000` constructed flag. - pub tag_number: TagNumber, - - /// Tag mode: `EXPLICIT` VS `IMPLICIT`. - pub tag_mode: TagMode, - - /// Value of the field. - pub value: T, -} - -impl ContextSpecific { - /// Attempt to decode an `EXPLICIT` ASN.1 `CONTEXT-SPECIFIC` field with the - /// provided [`TagNumber`]. - /// - /// This method has the following behavior which decodes tag numbers one by one - /// in extension fields, which are denoted in an ASN.1 schema using - /// the `...` ellipsis extension marker: - /// - /// - Returns `Ok(None)` if anything other than a [`ContextSpecific`] field - /// is encountered. - /// - Returns `Ok(None)` if a [`ContextSpecific`] field with a different tag - /// number is encountered. These fields are not consumed in this case. - /// - Returns `Err(ErrorKind::Noncanonical)` if constructed bit is primitive. - /// - Returns `Ok(Some(..))` if tag number matches. - pub fn decode_explicit<'a, R: Reader<'a>>( - reader: &mut R, - tag_number: TagNumber, - ) -> Result, T::Error> - where - T: Decode<'a>, - { - if !Tag::peek_matches(reader, Class::ContextSpecific, tag_number)? { - return Ok(None); - } - Ok(Some(Self::decode(reader)?)) - } - - /// Attempt to decode an `IMPLICIT` ASN.1 `CONTEXT-SPECIFIC` field with the - /// provided [`TagNumber`]. - /// - /// This method otherwise behaves the same as `decode_explicit`, - /// but should be used in cases where the particular fields are `IMPLICIT` - /// as opposed to `EXPLICIT`. - /// - /// Differences from `EXPLICIT`: - /// - Returns `Err(ErrorKind::Noncanonical)` if constructed bit - /// does not match constructed bit of the base encoding. - pub fn decode_implicit<'a, R: Reader<'a>>( - reader: &mut R, - tag_number: TagNumber, - ) -> Result, T::Error> - where - T: DecodeValue<'a> + IsConstructed, - { - // Peek tag number - if !Tag::peek_matches(reader, Class::ContextSpecific, tag_number)? { - return Ok(None); - } - // Decode IMPLICIT header - let header = Header::decode(reader)?; - - // read_nested checks if header matches decoded length - let value = reader.read_nested(header.length, |reader| { - // Decode inner IMPLICIT value - T::decode_value(reader, header) - })?; - - // the encoding shall be constructed if the base encoding is constructed - if header.tag.is_constructed() != T::CONSTRUCTED { - return Err(header.tag.non_canonical_error().into()); - } - - Ok(Some(Self { - tag_number, - tag_mode: TagMode::Implicit, - value, - })) - } -} - -impl<'a, T> Choice<'a> for ContextSpecific -where - T: Decode<'a> + Tagged, -{ - fn can_decode(tag: Tag) -> bool { - tag.is_context_specific() - } -} - -impl<'a, T> Decode<'a> for ContextSpecific -where - T: Decode<'a>, -{ - type Error = T::Error; - - fn decode>(reader: &mut R) -> Result { - // Decode EXPLICIT header - let header = Header::decode(reader)?; - - // encoding shall be constructed - if !header.tag.is_constructed() { - return Err(header.tag.non_canonical_error().into()); - } - match header.tag { - Tag::ContextSpecific { number, .. } => Ok(Self { - tag_number: number, - tag_mode: TagMode::default(), - value: reader.read_nested(header.length, |reader| { - // Decode inner tag-length-value of EXPLICIT - T::decode(reader) - })?, - }), - tag => Err(tag.unexpected_error(None).into()), - } - } -} - -impl EncodeValue for ContextSpecific -where - T: EncodeValue + Tagged, -{ - fn value_len(&self) -> Result { - match self.tag_mode { - TagMode::Explicit => self.value.encoded_len(), - TagMode::Implicit => self.value.value_len(), - } - } - - fn encode_value(&self, writer: &mut impl Writer) -> Result<(), Error> { - match self.tag_mode { - TagMode::Explicit => self.value.encode(writer), - TagMode::Implicit => self.value.encode_value(writer), - } - } -} - -impl Tagged for ContextSpecific -where - T: Tagged, -{ - fn tag(&self) -> Tag { - let constructed = match self.tag_mode { - // ISO/IEC 8825-1:2021 - // 8.14.3 If implicit tagging (see Rec. ITU-T X.680 | ISO/IEC 8824-1, 31.2.7) was not used in the definition of the type, the - // encoding shall be constructed and the contents octets shall be the complete base encoding [Encode]. - TagMode::Explicit => true, - - // ISO/IEC 8825-1:2021 - // 8.14.4 If implicit tagging was used in the definition of the type, then: - // a) the encoding shall be constructed if the base encoding is constructed, and shall be primitive otherwise; and - // b) the contents octets shall be the same as the contents octets [EncodeValue] of the base encoding. - // - // TODO(dishmaker): use IsConstructed trait for IMPLICIT - TagMode::Implicit => self.value.tag().is_constructed(), - }; - - Tag::ContextSpecific { - number: self.tag_number, - constructed, - } - } -} - -impl<'a, T> TryFrom> for ContextSpecific -where - T: Decode<'a>, -{ - type Error = T::Error; - - fn try_from(any: AnyRef<'a>) -> Result, Self::Error> { - match any.tag() { - Tag::ContextSpecific { - number, - constructed: true, - } => Ok(Self { - tag_number: number, - tag_mode: TagMode::default(), - value: T::from_der(any.value())?, - }), - tag => Err(tag.unexpected_error(None).into()), - } - } -} - -impl ValueOrd for ContextSpecific -where - T: EncodeValue + ValueOrd + Tagged, -{ - fn value_cmp(&self, other: &Self) -> Result { - match self.tag_mode { - TagMode::Explicit => self.der_cmp(other), - TagMode::Implicit => self.value_cmp(other), - } - } -} - -/// Context-specific field reference. -/// -/// This type encodes a field which is specific to a particular context -/// and is identified by a [`TagNumber`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub struct ContextSpecificRef<'a, T> { - /// Context-specific tag number sans the leading `0b10000000` class - /// identifier bit and `0b100000` constructed flag. - pub tag_number: TagNumber, - - /// Tag mode: `EXPLICIT` VS `IMPLICIT`. - pub tag_mode: TagMode, - - /// Value of the field. - pub value: &'a T, -} - -impl<'a, T> ContextSpecificRef<'a, T> { - /// Convert to a [`ContextSpecific`]. - fn encoder(&self) -> ContextSpecific> { - ContextSpecific { - tag_number: self.tag_number, - tag_mode: self.tag_mode, - value: EncodeValueRef(self.value), - } - } -} - -impl EncodeValue for ContextSpecificRef<'_, T> -where - T: EncodeValue + Tagged, -{ - fn value_len(&self) -> Result { - self.encoder().value_len() - } - - fn encode_value(&self, writer: &mut impl Writer) -> Result<(), Error> { - self.encoder().encode_value(writer) - } -} - -impl Tagged for ContextSpecificRef<'_, T> -where - T: Tagged, -{ - fn tag(&self) -> Tag { - self.encoder().tag() - } -} +#[cfg(doc)] +use crate::ErrorKind; + +impl_custom_class!( + ContextSpecific, + ContextSpecific, + "CONTEXT-SPECIFIC", + "0b10000000" +); +impl_custom_class_ref!( + ContextSpecificRef, + ContextSpecific, + "CONTEXT-SPECIFIC", + "0b10000000" +); #[cfg(test)] #[allow(clippy::unwrap_used)] diff --git a/der/src/asn1/internal_macros.rs b/der/src/asn1/internal_macros.rs index 1872c73c4..ed71bdbed 100644 --- a/der/src/asn1/internal_macros.rs +++ b/der/src/asn1/internal_macros.rs @@ -75,3 +75,265 @@ macro_rules! impl_string_type { } }; } + +macro_rules! impl_custom_class { + ($class_type_name: ident, $class_enum_name: ident, $asn1_class_name: literal, $class_bits_str: literal) => { + #[doc = concat!("`", $asn1_class_name, "` field which wraps an owned inner value.")] + /// + /// This type decodes/encodes a field which is specific to a particular context + /// and is identified by a [`TagNumber`]. + #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] + pub struct $class_type_name { + #[doc = concat!("`", $asn1_class_name, "` tag number sans the leading `", $class_bits_str, "` class")] + /// identifier bit and `0b100000` constructed flag. + pub tag_number: TagNumber, + + /// Tag mode: `EXPLICIT` VS `IMPLICIT`. + pub tag_mode: TagMode, + + /// Value of the field. + pub value: T, + } + + impl $class_type_name { + #[doc = concat!("Attempt to decode an `EXPLICIT` ASN.1 `", $asn1_class_name, "` field with the")] + /// provided [`TagNumber`]. + /// + /// This method has the following behavior which decodes tag numbers one by one + /// in extension fields, which are denoted in an ASN.1 schema using + /// the `...` ellipsis extension marker: + /// + #[doc = concat!("- Returns `Ok(None)` if class other than [`Class::", stringify!($class_enum_name), "`] tag")] + /// is encountered. + /// - Returns `Ok(None)` if a field with a different tag number is encountered. + /// These fields are not consumed in this case. + /// - Returns [`ErrorKind::Noncanonical`] if constructed bit is primitive. + /// - Returns `Ok(Some(..))` if tag number matches. + pub fn decode_explicit<'a, R: Reader<'a>>( + reader: &mut R, + tag_number: TagNumber, + ) -> Result, T::Error> + where + T: Decode<'a>, + { + if !Tag::peek_matches(reader, Class::$class_enum_name, tag_number)? { + return Ok(None); + } + Ok(Some(Self::decode(reader)?)) + } + + #[doc = concat!("Attempt to decode an `IMPLICIT` ASN.1 `", $asn1_class_name, "` field with the")] + /// provided [`TagNumber`]. + /// + /// This method otherwise behaves the same as `decode_explicit`, + /// but should be used in cases where the particular fields are `IMPLICIT` + /// as opposed to `EXPLICIT`. + /// + /// Differences from `EXPLICIT`: + /// - Returns [`ErrorKind::Noncanonical`] if constructed bit + /// does not match constructed bit of the base encoding. + pub fn decode_implicit<'a, R: Reader<'a>>( + reader: &mut R, + tag_number: TagNumber, + ) -> Result, T::Error> + where + T: DecodeValue<'a> + IsConstructed, + { + // Peek tag number + if !Tag::peek_matches(reader, Class::$class_enum_name, tag_number)? { + return Ok(None); + } + // Decode IMPLICIT header + let header = Header::decode(reader)?; + + // read_nested checks if header matches decoded length + let value = reader.read_nested(header.length, |reader| { + // Decode inner IMPLICIT value + T::decode_value(reader, header) + })?; + + // the encoding shall be constructed if the base encoding is constructed + if header.tag.is_constructed() != T::CONSTRUCTED { + return Err(header.tag.non_canonical_error().into()); + } + + Ok(Some(Self { + tag_number, + tag_mode: TagMode::Implicit, + value, + })) + } + } + + impl<'a, T> Choice<'a> for $class_type_name + where + T: Decode<'a> + Tagged, + { + fn can_decode(tag: Tag) -> bool { + tag.is_context_specific() + } + } + + impl<'a, T> Decode<'a> for $class_type_name + where + T: Decode<'a>, + { + type Error = T::Error; + + fn decode>(reader: &mut R) -> Result { + // Decode EXPLICIT header + let header = Header::decode(reader)?; + + // encoding shall be constructed + if !header.tag.is_constructed() { + return Err(header.tag.non_canonical_error().into()); + } + match header.tag { + Tag::$class_enum_name { number, .. } => Ok(Self { + tag_number: number, + tag_mode: TagMode::default(), + value: reader.read_nested(header.length, |reader| { + // Decode inner tag-length-value of EXPLICIT + T::decode(reader) + })?, + }), + tag => Err(tag.unexpected_error(None).into()), + } + } + } + + impl EncodeValue for $class_type_name + where + T: EncodeValue + Tagged, + { + fn value_len(&self) -> Result { + match self.tag_mode { + TagMode::Explicit => self.value.encoded_len(), + TagMode::Implicit => self.value.value_len(), + } + } + + fn encode_value(&self, writer: &mut impl Writer) -> Result<(), Error> { + match self.tag_mode { + TagMode::Explicit => self.value.encode(writer), + TagMode::Implicit => self.value.encode_value(writer), + } + } + } + + impl Tagged for $class_type_name + where + T: Tagged, + { + fn tag(&self) -> Tag { + let constructed = match self.tag_mode { + // ISO/IEC 8825-1:2021 + // 8.14.3 If implicit tagging (see Rec. ITU-T X.680 | ISO/IEC 8824-1, 31.2.7) was not used in the definition of the type, the + // encoding shall be constructed and the contents octets shall be the complete base encoding [Encode]. + TagMode::Explicit => true, + + // ISO/IEC 8825-1:2021 + // 8.14.4 If implicit tagging was used in the definition of the type, then: + // a) the encoding shall be constructed if the base encoding is constructed, and shall be primitive otherwise; and + // b) the contents octets shall be the same as the contents octets [EncodeValue] of the base encoding. + // + // TODO(dishmaker): use IsConstructed trait for IMPLICIT + TagMode::Implicit => self.value.tag().is_constructed(), + }; + + Tag::$class_enum_name { + number: self.tag_number, + constructed, + } + } + } + + impl<'a, T> TryFrom> for $class_type_name + where + T: Decode<'a>, + { + type Error = T::Error; + + fn try_from(any: AnyRef<'a>) -> Result<$class_type_name, Self::Error> { + match any.tag() { + Tag::$class_enum_name { + number, + constructed: true, + } => Ok(Self { + tag_number: number, + tag_mode: TagMode::default(), + value: T::from_der(any.value())?, + }), + tag => Err(tag.unexpected_error(None).into()), + } + } + } + + impl ValueOrd for $class_type_name + where + T: EncodeValue + ValueOrd + Tagged, + { + fn value_cmp(&self, other: &Self) -> Result { + match self.tag_mode { + TagMode::Explicit => self.der_cmp(other), + TagMode::Implicit => self.value_cmp(other), + } + } + } + }; +} + +macro_rules! impl_custom_class_ref { + ($ref_class_type_name: ident, $class_type_name: ident, $asn1_class_name: literal, $class_bits_str: literal) => { + #[doc = concat!("`", $asn1_class_name, "` field reference.")] + /// + /// + /// This type encodes a field which is specific to a particular context + /// and is identified by a [`TagNumber`]. + #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] + pub struct $ref_class_type_name<'a, T> { + #[doc = concat!("`", $asn1_class_name, "` tag number sans the leading `", $class_bits_str, "` class")] + /// identifier bit and `0b100000` constructed flag. + pub tag_number: TagNumber, + + /// Tag mode: `EXPLICIT` VS `IMPLICIT`. + pub tag_mode: TagMode, + + /// Value of the field. + pub value: &'a T, + } + + impl<'a, T> $ref_class_type_name<'a, T> { + /// Convert to a [`ContextSpecific`]. + fn encoder(&self) -> $class_type_name> { + $class_type_name { + tag_number: self.tag_number, + tag_mode: self.tag_mode, + value: EncodeValueRef(self.value), + } + } + } + + impl EncodeValue for $ref_class_type_name<'_, T> + where + T: EncodeValue + Tagged, + { + fn value_len(&self) -> Result { + self.encoder().value_len() + } + + fn encode_value(&self, writer: &mut impl Writer) -> Result<(), Error> { + self.encoder().encode_value(writer) + } + } + + impl Tagged for $ref_class_type_name<'_, T> + where + T: Tagged, + { + fn tag(&self) -> Tag { + self.encoder().tag() + } + } + }; +} diff --git a/der/src/asn1/private.rs b/der/src/asn1/private.rs new file mode 100644 index 000000000..448cb56e8 --- /dev/null +++ b/der/src/asn1/private.rs @@ -0,0 +1,14 @@ +//! Private class field. + +use crate::{ + Choice, Class, Decode, DecodeValue, DerOrd, Encode, EncodeValue, EncodeValueRef, Error, Header, + Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, asn1::AnyRef, + tag::IsConstructed, +}; +use core::cmp::Ordering; + +#[cfg(doc)] +use crate::ErrorKind; + +impl_custom_class!(Private, Private, "PRIVATE", "0b11000000"); +impl_custom_class_ref!(PrivateRef, Private, "PRIVATE", "0b11000000"); From 79ddd8e583d5a8ddb5abaa7eef2f75a1ea27be73 Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Tue, 6 May 2025 14:19:14 +0200 Subject: [PATCH 2/2] der: fix CONTEXT-SPECIFIC constructed bit check order --- der/src/asn1/internal_macros.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/der/src/asn1/internal_macros.rs b/der/src/asn1/internal_macros.rs index ed71bdbed..aeea1b2f6 100644 --- a/der/src/asn1/internal_macros.rs +++ b/der/src/asn1/internal_macros.rs @@ -146,17 +146,17 @@ macro_rules! impl_custom_class { // Decode IMPLICIT header let header = Header::decode(reader)?; + // the encoding shall be constructed if the base encoding is constructed + if header.tag.is_constructed() != T::CONSTRUCTED { + return Err(header.tag.non_canonical_error().into()); + } + // read_nested checks if header matches decoded length let value = reader.read_nested(header.length, |reader| { // Decode inner IMPLICIT value T::decode_value(reader, header) })?; - // the encoding shall be constructed if the base encoding is constructed - if header.tag.is_constructed() != T::CONSTRUCTED { - return Err(header.tag.non_canonical_error().into()); - } - Ok(Some(Self { tag_number, tag_mode: TagMode::Implicit,