From 347049d980ee6c1a1b69270878fe8f43f9314112 Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:43:23 +0200 Subject: [PATCH] der: add `ClassTaggedExplicit` wrapper Generic wrapper around `APPLICATION`, `CONTEXT-SPECIFIC` and `PRIVATE` tags. Such struct simplifies decoding, as previously we had to assert that decoded tag number is the expected one. --- der/src/asn1.rs | 7 ++-- der/src/asn1/application.rs | 9 ++++- der/src/asn1/class_tagged.rs | 68 ++++++++++++++++++++++++++++++++ der/src/asn1/context_specific.rs | 41 +++++++++++++++++-- der/src/asn1/private.rs | 7 +++- der/src/tag.rs | 27 ++++++++++++- 6 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 der/src/asn1/class_tagged.rs diff --git a/der/src/asn1.rs b/der/src/asn1.rs index 60a7b2dec..512cc2add 100644 --- a/der/src/asn1.rs +++ b/der/src/asn1.rs @@ -11,6 +11,7 @@ pub(crate) mod bit_string; mod bmp_string; mod boolean; mod choice; +mod class_tagged; mod context_specific; mod general_string; mod generalized_time; @@ -35,10 +36,10 @@ mod videotex_string; pub use self::{ any::AnyRef, - application::{Application, ApplicationRef}, + application::{Application, ApplicationExplicit, ApplicationRef}, bit_string::{BitStringIter, BitStringRef}, choice::Choice, - context_specific::{ContextSpecific, ContextSpecificRef}, + context_specific::{ContextSpecific, ContextSpecificExplicit, ContextSpecificRef}, general_string::GeneralStringRef, generalized_time::GeneralizedTime, ia5_string::Ia5StringRef, @@ -46,7 +47,7 @@ pub use self::{ null::Null, octet_string::OctetStringRef, printable_string::PrintableStringRef, - private::{Private, PrivateRef}, + private::{Private, PrivateExplicit, 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 index 93becff6a..388e8da33 100644 --- a/der/src/asn1/application.rs +++ b/der/src/asn1/application.rs @@ -2,8 +2,9 @@ use crate::{ Choice, Class, Decode, DecodeValue, DerOrd, Encode, EncodeValue, EncodeValueRef, Error, Header, - Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, asn1::AnyRef, - tag::IsConstructed, + Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, + asn1::{AnyRef, class_tagged::ClassTaggedExplicit}, + tag::{IsConstructed, class::CLASS_APPLICATION}, }; use core::cmp::Ordering; @@ -12,3 +13,7 @@ use crate::ErrorKind; impl_custom_class!(Application, Application, "APPLICATION", "0b01000000"); impl_custom_class_ref!(ApplicationRef, Application, "APPLICATION", "0b01000000"); + +/// Application class, EXPLICIT +pub type ApplicationExplicit = + ClassTaggedExplicit; diff --git a/der/src/asn1/class_tagged.rs b/der/src/asn1/class_tagged.rs new file mode 100644 index 000000000..697ed6d47 --- /dev/null +++ b/der/src/asn1/class_tagged.rs @@ -0,0 +1,68 @@ +use crate::{ + Class, Decode, DecodeValue, Encode, EncodeValue, Error, FixedTag, Header, Length, Reader, Tag, + TagMode, TagNumber, Writer, +}; + +/// `APPLICATION`, `CONTEXT-SPECIFIC` or `PRIVATE` reference, with const `EXPLICIT` encoding. +/// +/// +/// This type encodes a field which is specific to a particular context +/// and is identified by a [`TagNumber`]. +/// +/// Inner value might implement [`Encode`], [`Decode`] or both. +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct ClassTaggedExplicit { + /// Inner value might implement [`Encode`], [`Decode`] or both. + pub value: T, +} + +impl ClassTaggedExplicit { + /// Returns const [`TagNumber`], associated with this `EXPLICIT` tag wrapper. + pub const fn tag_number() -> TagNumber { + TagNumber(NUMBER) + } + + /// Returns const [`TagMode::Explicit`], associated with this `EXPLICIT` tag wrapper. + pub const fn tag_mode() -> TagMode { + TagMode::Explicit + } +} + +impl EncodeValue + for ClassTaggedExplicit +where + T: Encode, +{ + fn value_len(&self) -> Result { + self.value.encoded_len() + } + + fn encode_value(&self, writer: &mut impl Writer) -> Result<(), Error> { + // Encode EXPLICIT value (with tag and length) + self.value.encode(writer) + } +} + +impl<'a, const NUMBER: u32, T, const CLASS_BITS: u8> DecodeValue<'a> + for ClassTaggedExplicit +where + T: Decode<'a>, +{ + type Error = T::Error; + + fn decode_value>(reader: &mut R, header: Header) -> Result { + // encoding shall be constructed + if !header.tag().is_constructed() { + return Err(reader.error(header.tag().non_canonical_error()).into()); + } + Ok(Self { + value: T::decode(reader)?, + }) + } +} + +impl FixedTag + for ClassTaggedExplicit +{ + const TAG: Tag = Tag::new_non_universal(Class::from_bits(CLASS_BITS), TagNumber(NUMBER), true); +} diff --git a/der/src/asn1/context_specific.rs b/der/src/asn1/context_specific.rs index c6c4e3266..132808e8f 100644 --- a/der/src/asn1/context_specific.rs +++ b/der/src/asn1/context_specific.rs @@ -2,8 +2,9 @@ use crate::{ Choice, Class, Decode, DecodeValue, DerOrd, Encode, EncodeValue, EncodeValueRef, Error, Header, - Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, asn1::AnyRef, - tag::IsConstructed, + Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, + asn1::{AnyRef, class_tagged::ClassTaggedExplicit}, + tag::{IsConstructed, class::CLASS_CONTEXT_SPECIFIC}, }; use core::cmp::Ordering; @@ -23,13 +24,17 @@ impl_custom_class_ref!( "0b10000000" ); +/// ContextSpecific class, EXPLICIT +pub type ContextSpecificExplicit = + ClassTaggedExplicit; + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::ContextSpecific; use crate::{ Decode, Encode, SliceReader, TagMode, TagNumber, - asn1::{BitStringRef, ContextSpecificRef, SetOf, Utf8StringRef}, + asn1::{BitStringRef, ContextSpecificExplicit, ContextSpecificRef, SetOf, Utf8StringRef}, }; use hex_literal::hex; @@ -194,4 +199,34 @@ mod tests { assert_eq!(field.value.get(0).cloned(), Some(hello)); assert_eq!(field.value.get(1).cloned(), Some(world)); } + + #[test] + fn round_trip_explicit() { + let field = + ContextSpecificExplicit::<1, BitStringRef<'_>>::from_der(EXAMPLE_BYTES).unwrap(); + assert_eq!( + field.value, + BitStringRef::from_bytes(&EXAMPLE_BYTES[5..]).unwrap() + ); + assert_eq!( + ContextSpecificExplicit::<1, BitStringRef<'_>>::tag_mode(), + TagMode::Explicit + ); + assert_eq!( + ContextSpecificExplicit::<1, BitStringRef<'_>>::tag_number(), + TagNumber(1) + ); + + let mut buf = [0u8; 128]; + let encoded = field.encode_to_slice(&mut buf).unwrap(); + assert_eq!(encoded, EXAMPLE_BYTES); + + // should not decode as tag CONTEXT-SPECIFIC [2] + assert!(ContextSpecificExplicit::<2, BitStringRef<'_>>::from_der(EXAMPLE_BYTES).is_err()); + + // should be different than CONTEXT-SPECIFIC [1] + let invalid_field = ContextSpecificExplicit::<2, BitStringRef<'_>> { value: field.value }; + let invalid_encoded = invalid_field.encode_to_slice(&mut buf).unwrap(); + assert_ne!(invalid_encoded, EXAMPLE_BYTES); + } } diff --git a/der/src/asn1/private.rs b/der/src/asn1/private.rs index 448cb56e8..15eac0b6e 100644 --- a/der/src/asn1/private.rs +++ b/der/src/asn1/private.rs @@ -2,8 +2,10 @@ use crate::{ Choice, Class, Decode, DecodeValue, DerOrd, Encode, EncodeValue, EncodeValueRef, Error, Header, - Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, asn1::AnyRef, + Length, Reader, Tag, TagMode, TagNumber, Tagged, ValueOrd, Writer, + asn1::{AnyRef, class_tagged::ClassTaggedExplicit}, tag::IsConstructed, + tag::class::CLASS_PRIVATE, }; use core::cmp::Ordering; @@ -12,3 +14,6 @@ use crate::ErrorKind; impl_custom_class!(Private, Private, "PRIVATE", "0b11000000"); impl_custom_class_ref!(PrivateRef, Private, "PRIVATE", "0b11000000"); + +/// Private class, EXPLICIT +pub type PrivateExplicit = ClassTaggedExplicit; diff --git a/der/src/tag.rs b/der/src/tag.rs index 5ccdc7c50..9f6eec58c 100644 --- a/der/src/tag.rs +++ b/der/src/tag.rs @@ -1,7 +1,7 @@ //! ASN.1 tags. #![cfg_attr(feature = "arbitrary", allow(clippy::arithmetic_side_effects))] -mod class; +pub(crate) mod class; mod mode; mod number; @@ -165,6 +165,31 @@ impl Tag { /// rules implemented by this crate. pub(crate) const MAX_SIZE: usize = 6; + /// Creates a [`Tag`] of non-`UNIVERSAL` class. + /// Allowed classes are: + /// - `APPLICATION` [`Class::Application`], + /// - `CONTEXT-SPECIFIC` [`Class::ContextSpecific`], + /// - `PRIVATE` [`Class::Private`], + /// + /// Returns [`Tag::Null`] otherwise. + pub const fn new_non_universal(class: Class, number: TagNumber, constructed: bool) -> Tag { + match class { + Class::Application => Tag::Application { + constructed, + number, + }, + Class::ContextSpecific => Tag::ContextSpecific { + constructed, + number, + }, + Class::Private => Tag::Private { + constructed, + number, + }, + Class::Universal => Tag::Null, + } + } + /// Decode a [`Tag`] in addition to returning the value of the constructed bit. pub(crate) fn decode_with_constructed_bit<'a>( reader: &mut impl Reader<'a>,