From 7e766becdd51e9e17763e9e2a5f2fbae95970f7e Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Wed, 4 Sep 2024 20:06:40 -0700 Subject: [PATCH 1/2] x509-cert: make `Name` a new type over `RdnSequence` This make `FromStr` documented way to build a name. This also exposes a set of getter methods to get elements from the name (CN, Org, ...). --- cms/tests/builder.rs | 25 +- x509-cert/src/builder/profile/cabf.rs | 12 +- x509-cert/src/builder/profile/cabf/tls.rs | 7 +- x509-cert/src/ext/pkix/name/dirstr.rs | 44 ++- x509-cert/src/name.rs | 234 ++++++++++++- x509-cert/tests/certificate.rs | 138 ++++---- x509-cert/tests/certreq.rs | 2 +- x509-cert/tests/name.rs | 106 ++++-- x509-cert/tests/pkix_extensions.rs | 118 +++---- x509-cert/tests/trust_anchor_format.rs | 397 ++++++++++------------ 10 files changed, 657 insertions(+), 426 deletions(-) diff --git a/cms/tests/builder.rs b/cms/tests/builder.rs index 5d308c731..28de12268 100644 --- a/cms/tests/builder.rs +++ b/cms/tests/builder.rs @@ -13,7 +13,7 @@ use cms::enveloped_data::RecipientInfo::Ktri; use cms::enveloped_data::{EnvelopedData, RecipientIdentifier, RecipientInfo}; use cms::signed_data::{EncapsulatedContentInfo, SignedData, SignerIdentifier}; use const_oid::ObjectIdentifier; -use der::asn1::{OctetString, PrintableString, SetOfVec, Utf8StringRef}; +use der::asn1::{OctetString, PrintableString, SetOfVec}; use der::{Any, AnyRef, Decode, DecodePem, Encode, Tag, Tagged}; use p256::{pkcs8::DecodePrivateKey, NistP256}; use pem_rfc7468::LineEnding; @@ -24,8 +24,7 @@ use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; use sha2::Sha256; use signature::Verifier; use spki::AlgorithmIdentifierOwned; -use x509_cert::attr::{Attribute, AttributeTypeAndValue, AttributeValue}; -use x509_cert::name::{RdnSequence, RelativeDistinguishedName}; +use x509_cert::attr::{Attribute, AttributeValue}; use x509_cert::serial_number::SerialNumber; // TODO bk replace this by const_oid definitions as soon as released @@ -50,30 +49,18 @@ fn ecdsa_signer() -> ecdsa::SigningKey { } fn signer_identifier(id: i32) -> SignerIdentifier { - let mut rdn_sequence = RdnSequence::default(); - let rdn = &[AttributeTypeAndValue { - oid: const_oid::db::rfc4519::CN, - value: Any::from(Utf8StringRef::new(&format!("test client {id}")).unwrap()), - }]; - let set_of_vector = SetOfVec::try_from(rdn.to_vec()).unwrap(); - rdn_sequence.push(RelativeDistinguishedName::from(set_of_vector)); + let issuer = format!("CN=test client {id}").parse().unwrap(); SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { - issuer: rdn_sequence, + issuer, serial_number: SerialNumber::new(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]) .expect("failed to create a serial number"), }) } fn recipient_identifier(id: i32) -> RecipientIdentifier { - let mut rdn_sequence = RdnSequence::default(); - let rdn = &[AttributeTypeAndValue { - oid: const_oid::db::rfc4519::CN, - value: Any::from(Utf8StringRef::new(&format!("test client {id}")).unwrap()), - }]; - let set_of_vector = SetOfVec::try_from(rdn.to_vec()).unwrap(); - rdn_sequence.push(RelativeDistinguishedName::from(set_of_vector)); + let issuer = format!("CN=test client {id}").parse().unwrap(); RecipientIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { - issuer: rdn_sequence, + issuer, serial_number: SerialNumber::new(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]) .expect("failed to create a serial number"), }) diff --git a/x509-cert/src/builder/profile/cabf.rs b/x509-cert/src/builder/profile/cabf.rs index a8b02ac0c..f3691e15f 100644 --- a/x509-cert/src/builder/profile/cabf.rs +++ b/x509-cert/src/builder/profile/cabf.rs @@ -43,7 +43,7 @@ pub fn check_names_encoding(name: &Name, multiple_allowed: bool) -> Result<()> { let mut seen = HashSet::new(); - for rdn in name.iter() { + for rdn in name.iter_rdn() { if rdn.len() != 1 { return Err(Error::NonUniqueRdn); } @@ -87,13 +87,11 @@ pub fn ca_certificate_naming(subject: &Name) -> Result<()> { check_names_encoding(subject, false)?; - for rdn in subject.iter() { - for atv in rdn.iter() { - if !allowed.remove(&atv.oid) { - return Err(Error::InvalidAttribute { oid: atv.oid }); - } - required.remove(&atv.oid); + for atv in subject.iter() { + if !allowed.remove(&atv.oid) { + return Err(Error::InvalidAttribute { oid: atv.oid }); } + required.remove(&atv.oid); } if !required.is_empty() { diff --git a/x509-cert/src/builder/profile/cabf/tls.rs b/x509-cert/src/builder/profile/cabf/tls.rs index 142f8cce2..b92b4a5b5 100644 --- a/x509-cert/src/builder/profile/cabf/tls.rs +++ b/x509-cert/src/builder/profile/cabf/tls.rs @@ -22,7 +22,7 @@ use crate::{ }, AsExtension, Extension, }, - name::{Name, RelativeDistinguishedName}, + name::{Name, RdnSequence, RelativeDistinguishedName}, }; use spki::SubjectPublicKeyInfoRef; @@ -145,7 +145,7 @@ impl CertificateType { // TODO(baloo): not very happy with all that, might as well throw that in a helper // or something. let rdns: vec::Vec = subject - .iter() + .iter_rdn() .filter_map(|rdn| { let out = SetOfVec::::from_iter( rdn.iter() @@ -159,7 +159,8 @@ impl CertificateType { .filter(|rdn| !rdn.is_empty()) .collect(); - let subject: Name = rdns.into(); + let subject: RdnSequence = rdns.into(); + let subject: Name = subject.into(); Ok(Self::DomainValidated(DomainValidated { subject, names })) } diff --git a/x509-cert/src/ext/pkix/name/dirstr.rs b/x509-cert/src/ext/pkix/name/dirstr.rs index a6a0117cb..87f703730 100644 --- a/x509-cert/src/ext/pkix/name/dirstr.rs +++ b/x509-cert/src/ext/pkix/name/dirstr.rs @@ -1,6 +1,8 @@ use alloc::string::String; -use der::asn1::{PrintableString, TeletexString}; -use der::{Choice, ValueOrd}; +use der::{ + asn1::{Any, PrintableString, TeletexString}, + Choice, FixedTag, Header, Reader, ValueOrd, +}; /// DirectoryString as defined in [RFC 5280 Section 4.2.1.4]. /// @@ -51,3 +53,41 @@ pub enum DirectoryString { #[asn1(type = "UTF8String")] Utf8String(String), } + +impl<'a> TryFrom<&'a Any> for DirectoryString { + type Error = der::Error; + fn try_from(any: &'a Any) -> der::Result { + any.decode_as() + } +} + +impl<'a> der::DecodeValue<'a> for DirectoryString { + type Error = der::Error; + + fn decode_value>(reader: &mut R, header: Header) -> Result { + match header.tag { + PrintableString::TAG => { + PrintableString::decode_value(reader, header).map(Self::PrintableString) + } + TeletexString::TAG => { + TeletexString::decode_value(reader, header).map(Self::TeletexString) + } + String::TAG => String::decode_value(reader, header).map(Self::Utf8String), + actual => Err(der::ErrorKind::TagUnexpected { + expected: None, + actual, + } + .into()), + } + } +} + +impl AsRef for DirectoryString { + fn as_ref(&self) -> &str { + match self { + Self::PrintableString(s) => s.as_ref(), + Self::TeletexString(s) => s.as_ref(), + Self::Utf8String(s) => s.as_ref(), + } + } +} diff --git a/x509-cert/src/name.rs b/x509-cert/src/name.rs index 2da628b41..4ca1c209f 100644 --- a/x509-cert/src/name.rs +++ b/x509-cert/src/name.rs @@ -1,9 +1,16 @@ //! Name-related definitions as defined in X.501 (and updated by RFC 5280). -use crate::attr::AttributeTypeAndValue; +use crate::{attr::AttributeTypeAndValue, ext::pkix::name::DirectoryString}; use alloc::vec::Vec; +use const_oid::{ + db::{rfc3280, rfc4519}, + ObjectIdentifier, +}; use core::{fmt, str::FromStr}; -use der::{asn1::SetOfVec, Encode}; +use der::{ + asn1::{Any, Ia5StringRef, PrintableStringRef, SetOfVec}, + Encode, +}; /// X.501 Name as defined in [RFC 5280 Section 4.1.2.4]. X.501 Name is used to represent distinguished names. /// @@ -11,8 +18,229 @@ use der::{asn1::SetOfVec, Encode}; /// Name ::= CHOICE { rdnSequence RDNSequence } /// ``` /// +/// To build name, the syntax described in [RFC 4514 Section 3] is expected. +/// +/// The following attribute names are recognized: +/// ```text +/// String X.500 AttributeType +/// ------ -------------------------------------------- +/// CN commonName (2.5.4.3) +/// L localityName (2.5.4.7) +/// ST stateOrProvinceName (2.5.4.8) +/// O organizationName (2.5.4.10) +/// OU organizationalUnitName (2.5.4.11) +/// C countryName (2.5.4.6) +/// STREET streetAddress (2.5.4.9) +/// DC domainComponent (0.9.2342.19200300.100.1.25) +/// UID userId (0.9.2342.19200300.100.1.1) +/// +/// ``` +/// +/// # Example +/// +/// ``` +/// use std::str::FromStr; +/// use x509_cert::name::Name; +/// +/// // Multiple syntaxes are supported by `from_str`: +/// let subject = Name::from_str("CN=example.com").unwrap(); +/// let subject = Name::from_str("C=US; ST=California; L=Los Angeles; O=InternetCorporationforAssignedNamesandNumbers; CN=www.example.org").unwrap(); +/// let subject = Name::from_str("C=US,ST=California,L=Los Angeles,O=InternetCorporationforAssignedNamesandNumbers,CN=www.example.org").unwrap(); +/// let subject = Name::from_str("C=US/ST=California/L=Los Angeles/O=InternetCorporationforAssignedNamesandNumbers/CN=www.example.org").unwrap(); +/// let subject = Name::from_str("UID=jsmith,DC=example,DC=net").unwrap(); +/// let subject = Name::from_str("OU=Sales+CN=J. Smith,DC=example,DC=net").unwrap(); +/// let subject = Name::from_str(r#"CN=James \"Jim\" Smith\, III,DC=example,DC=net"#).unwrap(); +/// let subject = Name::from_str(r#"CN=Before\0dAfter,DC=example,DC=net"#).unwrap(); +/// let subject = Name::from_str("1.3.6.1.4.1.1466.0=#04024869").unwrap(); +/// ``` +/// +/// [RFC 4514 Section 3]: https://www.rfc-editor.org/rfc/rfc4514#section-3 /// [RFC 5280 Section 4.1.2.4]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4 -pub type Name = RdnSequence; +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Name(RdnSequence); + +// This will implement `From` which is provided as an escape hatch to build names +// from `bmpString`, `TeletexString`, or `UniversalString`: +// ``` +// When CAs have previously issued certificates with issuer fields with +// attributes encoded using TeletexString, BMPString, or +// UniversalString, then the CA MAY continue to use these encodings of +// the DirectoryString to preserve backward compatibility. +// ``` +impl_newtype!(Name, RdnSequence); + +impl Name { + /// Is this [`Name`] empty? + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the number of [`RelativeDistinguishedName`] elements in this [`Name`]. + pub fn len(&self) -> usize { + self.0 .0.len() + } + + /// Returns an iterator over the inner [`AttributeTypeAndValue`]s. + /// + /// This iterator does not expose which attributes are grouped together as + /// [`RelativeDistinguishedName`]s. If you need this, use [`Self::iter_rdn`]. + #[inline] + pub fn iter(&self) -> impl Iterator + '_ { + self.0 .0.iter().flat_map(move |rdn| rdn.0.as_slice()) + } + + /// Returns an iterator over the inner [`RelativeDistinguishedName`]s. + #[inline] + pub fn iter_rdn(&self) -> impl Iterator + '_ { + self.0 .0.iter() + } +} + +impl Name { + /// Returns the element found in the name identified by `oid` + /// + /// This will return `Ok(None)` if no such element is present. + /// + /// If more than one attribute is present with the specified OID, only the first attribute is + /// returned. Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as expected + pub fn by_oid<'a, T>(&'a self, oid: ObjectIdentifier) -> der::Result> + where + T: TryFrom<&'a Any, Error = der::Error>, + T: fmt::Debug, + { + self.iter() + .filter(|atav| atav.oid == oid) + .map(|atav| T::try_from(&atav.value)) + .next() + .transpose() + } + + /// Returns the Common Name (CN) found in the name. + /// + /// This will return `Ok(None)` if no CN is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a string. + pub fn common_name(&self) -> der::Result> { + self.by_oid(rfc4519::COMMON_NAME) + } + + /// Returns the Country (C) found in the name. + /// + /// This will return `Ok(None)` if no Country is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a printableString. + pub fn country(&self) -> der::Result>> { + self.by_oid(rfc4519::COUNTRY_NAME) + } + + /// Returns the State or Province (ST) found in the name. + /// + /// This will return `Ok(None)` if no State or Province is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a string. + pub fn state_or_province(&self) -> der::Result> { + self.by_oid(rfc4519::ST) + } + + /// Returns the Locality (L) found in the name. + /// + /// This will return `Ok(None)` if no Locality is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a string. + pub fn locality(&self) -> der::Result> { + self.by_oid(rfc4519::LOCALITY_NAME) + } + + /// Returns the Organization (O) found in the name. + /// + /// This will return `Ok(None)` if no Organization is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a string. + pub fn organization(&self) -> der::Result> { + self.by_oid(rfc4519::ORGANIZATION_NAME) + } + + /// Returns the Organization Unit (OU) found in the name. + /// + /// This will return `Ok(None)` if no Organization Unit is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as a string. + pub fn organization_unit(&self) -> der::Result> { + self.by_oid(rfc4519::ORGANIZATIONAL_UNIT_NAME) + } + + /// Returns the Email Address (emailAddress) found in the name. + /// + /// This will return `Ok(None)` if no email address is found. + /// + /// If more than one value is present, only the first is returned. + /// Later elements should be fetched using [`Name::iter`]. + /// + /// # Errors + /// + /// This will return [`der::Error`] if the content is not serialized as an ia5String. + pub fn email_address(&self) -> der::Result>> { + self.by_oid(rfc3280::EMAIL_ADDRESS) + } +} + +/// Parse a [`Name`] string. +/// +/// Follows the rules in [RFC 4514]. +/// +/// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 +impl FromStr for Name { + type Err = der::Error; + + fn from_str(s: &str) -> der::Result { + Ok(Self(RdnSequence::from_str(s)?)) + } +} + +/// Serializes the name according to the rules in [RFC 4514]. +/// +/// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} /// X.501 RDNSequence as defined in [RFC 5280 Section 4.1.2.4]. /// diff --git a/x509-cert/tests/certificate.rs b/x509-cert/tests/certificate.rs index f6beb4765..87b001a36 100644 --- a/x509-cert/tests/certificate.rs +++ b/x509-cert/tests/certificate.rs @@ -238,41 +238,35 @@ fn decode_cert() { .unwrap() .is_null()); - let mut counter = 0; - let i = cert.tbs_certificate().issuer().iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Mock" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - Utf8StringRef::try_from(&atav.value).unwrap().to_string(), - "IdenTrust Services LLC" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - Utf8StringRef::try_from(&atav.value).unwrap().to_string(), - "PTE IdenTrust Global Common Root CA 1" - ); - } - counter += 1; + for (counter, atav) in cert.tbs_certificate().issuer().iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Mock" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + Utf8StringRef::try_from(&atav.value).unwrap().to_string(), + "IdenTrust Services LLC" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + Utf8StringRef::try_from(&atav.value).unwrap().to_string(), + "PTE IdenTrust Global Common Root CA 1" + ); } } @@ -293,46 +287,40 @@ fn decode_cert() { 1516628593 ); - counter = 0; - let i = cert.tbs_certificate().subject().iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - // Yes, this cert features RDNs encoded in reverse order - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Test Federal Bridge CA" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "TestFPKI" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "U.S. Government" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } - counter += 1; + for (counter, atav) in cert.tbs_certificate().subject().iter().enumerate() { + // Yes, this cert features RDNs encoded in reverse order + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Test Federal Bridge CA" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "TestFPKI" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "U.S. Government" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); } } diff --git a/x509-cert/tests/certreq.rs b/x509-cert/tests/certreq.rs index 903387ad0..cd5ad36fc 100644 --- a/x509-cert/tests/certreq.rs +++ b/x509-cert/tests/certreq.rs @@ -36,7 +36,7 @@ fn decode_rsa_2048_der() { // Check all the RDNs. assert_eq!(cr.info.subject.len(), NAMES.len()); - for (name, (oid, val)) in cr.info.subject.iter().zip(NAMES) { + for (name, (oid, val)) in cr.info.subject.iter_rdn().zip(NAMES) { let kind = name.iter().next().unwrap(); let value = match kind.value.tag() { Tag::Utf8String => Utf8StringRef::try_from(&kind.value).unwrap().as_str(), diff --git a/x509-cert/tests/name.rs b/x509-cert/tests/name.rs index e0763b5ef..8a455370d 100644 --- a/x509-cert/tests/name.rs +++ b/x509-cert/tests/name.rs @@ -33,37 +33,31 @@ fn decode_name() { Name::from_der(&hex!("3040310B3009060355040613025553311F301D060355040A1316546573742043657274696669636174657320323031313110300E06035504031307476F6F64204341")[..]); let rdn1a = rdn1.unwrap(); - let mut counter = 0; - let i = rdn1a.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Test Certificates 2011" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Good CA" - ); - } - counter += 1; + for (counter, atav) in rdn1a.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Test Certificates 2011" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Good CA" + ); } } @@ -366,3 +360,53 @@ fn rdns_serde() { } } } + +#[cfg(feature = "std")] +#[test] +fn access_attributes() { + use std::str::FromStr; + + let name = Name::from_str("emailAddress=foo@example.com,UID=identity:ds.group.3891111,OU=management:ds.group.3891111,CN=OQFAvDNDWs.google.com,O=Google LLC,L=Mountain View,ST=California,C=US").unwrap(); + + assert_eq!( + <_ as AsRef>::as_ref(&name.common_name().unwrap().unwrap()), + "OQFAvDNDWs.google.com" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.country().unwrap().unwrap()), + "US" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.state_or_province().unwrap().unwrap()), + "California" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.locality().unwrap().unwrap()), + "Mountain View" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.organization().unwrap().unwrap()), + "Google LLC" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.organization_unit().unwrap().unwrap()), + "management:ds.group.3891111" + ); + + assert_eq!( + <_ as AsRef>::as_ref(&name.email_address().unwrap().unwrap()), + "foo@example.com" + ); + + let name = Name::from_str("C=DE,C=US").unwrap(); + + assert_eq!( + <_ as AsRef>::as_ref(&name.country().unwrap().unwrap()), + "US" + ); +} diff --git a/x509-cert/tests/pkix_extensions.rs b/x509-cert/tests/pkix_extensions.rs index 41622f5be..ff7c1d3de 100644 --- a/x509-cert/tests/pkix_extensions.rs +++ b/x509-cert/tests/pkix_extensions.rs @@ -580,37 +580,31 @@ fn decode_cert() { true ); - let mut counter = 0; - let i = cert.tbs_certificate().issuer().iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Test Certificates 2011" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Trust Anchor" - ); - } - counter += 1; + for (counter, atav) in cert.tbs_certificate().issuer().iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Test Certificates 2011" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Trust Anchor" + ); } } @@ -631,37 +625,31 @@ fn decode_cert() { 1924936200 ); - counter = 0; - let i = cert.tbs_certificate().subject().iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Test Certificates 2011" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Good CA" - ); - } - counter += 1; + for (counter, atav) in cert.tbs_certificate().subject().iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Test Certificates 2011" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Good CA" + ); } } @@ -696,10 +684,8 @@ fn decode_cert() { // TODO - parse and compare public key - counter = 0; let exts = cert.tbs_certificate().extensions().unwrap(); - let i = exts.iter(); - for ext in i { + for (counter, ext) in exts.iter().enumerate() { if 0 == counter { assert_eq!( ext.extn_id.to_string(), @@ -746,8 +732,6 @@ fn decode_cert() { assert_eq!(bc.ca, true); assert_eq!(bc.path_len_constraint, Option::None); } - - counter += 1; } assert_eq!( cert.signature_algorithm().oid.to_string(), diff --git a/x509-cert/tests/trust_anchor_format.rs b/x509-cert/tests/trust_anchor_format.rs index 2f824b18e..405d3b3ee 100644 --- a/x509-cert/tests/trust_anchor_format.rs +++ b/x509-cert/tests/trust_anchor_format.rs @@ -81,53 +81,44 @@ fn decode_ta1() { ]; let cert_path = tai.cert_path.as_ref().unwrap(); - let mut counter = 0; let exts = cert_path.policy_set.as_ref().unwrap(); - let i = exts.0.iter(); - for ext in i { + for (counter, ext) in exts.0.iter().enumerate() { assert_eq!(policy_ids[counter], ext.policy_identifier.to_string()); - counter += 1; } - counter = 0; - let i = cert_path.ta_name.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "U.S. Government" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "ECA" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "ECA Root CA 4" - ); - } - counter += 1; + for (counter, atav) in cert_path.ta_name.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "U.S. Government" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "ECA" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "ECA Root CA 4" + ); } } @@ -166,84 +157,72 @@ fn decode_ta2() { let cert_path = tai.cert_path.as_ref().unwrap(); - let mut counter = 0; - let i = cert_path.ta_name.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Entrust" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Certification Authorities" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Entrust Managed Services NFI Root CA" - ); - } - counter += 1; + for (counter, atav) in cert_path.ta_name.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Entrust" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Certification Authorities" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Entrust Managed Services NFI Root CA" + ); } } let nc = cert_path.name_constr.as_ref().unwrap(); - counter = 0; let gsi = nc.excluded_subtrees.as_ref().unwrap().iter(); for gs in gsi { match &gs.base { GeneralName::DirectoryName(dn) => { - let i = dn.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "U.S. Government" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "DoD" - ); - } - counter += 1; + for (counter, atav) in dn.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "U.S. Government" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "DoD" + ); } } } @@ -293,84 +272,72 @@ fn decode_ta3() { cert_path.policy_flags.unwrap() ); - let mut counter = 0; - let i = cert_path.ta_name.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Exostar LLC" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Certification Authorities" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.3"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "Exostar Federated Identity Service Root CA 1" - ); - } - counter += 1; + for (counter, atav) in cert_path.ta_name.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Exostar LLC" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Certification Authorities" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.3"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "Exostar Federated Identity Service Root CA 1" + ); } } let nc = cert_path.name_constr.as_ref().unwrap(); - counter = 0; let gsi = nc.excluded_subtrees.as_ref().unwrap().iter(); for gs in gsi { match &gs.base { GeneralName::DirectoryName(dn) => { - let i = dn.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.6"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "US" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "U.S. Government" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "DoD" - ); - } - counter += 1; + for (counter, atav) in dn.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.6"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "US" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "U.S. Government" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "DoD" + ); } } } @@ -413,41 +380,35 @@ fn decode_ta4() { let cert_path = tai.cert_path.as_ref().unwrap(); - let mut counter = 0; - let i = cert_path.ta_name.iter(); - for rdn in i { - let i1 = rdn.iter(); - for atav in i1 { - if 0 == counter { - assert_eq!(atav.oid.to_string(), "0.9.2342.19200300.100.1.25"); - assert_eq!( - Ia5StringRef::try_from(&atav.value).unwrap().to_string(), - "com" - ); - } else if 1 == counter { - assert_eq!(atav.oid.to_string(), "0.9.2342.19200300.100.1.25"); - assert_eq!( - Ia5StringRef::try_from(&atav.value).unwrap().to_string(), - "raytheon" - ); - } else if 2 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.10"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "CAs" - ); - } else if 3 == counter { - assert_eq!(atav.oid.to_string(), "2.5.4.11"); - assert_eq!( - PrintableStringRef::try_from(&atav.value) - .unwrap() - .to_string(), - "RaytheonRoot" - ); - } - counter += 1; + for (counter, atav) in cert_path.ta_name.iter().enumerate() { + if 0 == counter { + assert_eq!(atav.oid.to_string(), "0.9.2342.19200300.100.1.25"); + assert_eq!( + Ia5StringRef::try_from(&atav.value).unwrap().to_string(), + "com" + ); + } else if 1 == counter { + assert_eq!(atav.oid.to_string(), "0.9.2342.19200300.100.1.25"); + assert_eq!( + Ia5StringRef::try_from(&atav.value).unwrap().to_string(), + "raytheon" + ); + } else if 2 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.10"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "CAs" + ); + } else if 3 == counter { + assert_eq!(atav.oid.to_string(), "2.5.4.11"); + assert_eq!( + PrintableStringRef::try_from(&atav.value) + .unwrap() + .to_string(), + "RaytheonRoot" + ); } } From 593176ccef48f30bef2d951633f4b7fe9b856f36 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Fri, 6 Sep 2024 08:18:52 -0700 Subject: [PATCH 2/2] x509-cert: remove From for Name --- x509-cert/src/builder/profile/cabf/tls.rs | 5 +- x509-cert/src/name.rs | 83 +++++++++++++++++++---- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/x509-cert/src/builder/profile/cabf/tls.rs b/x509-cert/src/builder/profile/cabf/tls.rs index b92b4a5b5..32253cb56 100644 --- a/x509-cert/src/builder/profile/cabf/tls.rs +++ b/x509-cert/src/builder/profile/cabf/tls.rs @@ -22,7 +22,7 @@ use crate::{ }, AsExtension, Extension, }, - name::{Name, RdnSequence, RelativeDistinguishedName}, + name::{Name, RelativeDistinguishedName}, }; use spki::SubjectPublicKeyInfoRef; @@ -159,8 +159,7 @@ impl CertificateType { .filter(|rdn| !rdn.is_empty()) .collect(); - let subject: RdnSequence = rdns.into(); - let subject: Name = subject.into(); + let subject: Name = Name(rdns.into()); Ok(Self::DomainValidated(DomainValidated { subject, names })) } diff --git a/x509-cert/src/name.rs b/x509-cert/src/name.rs index 4ca1c209f..8b0c0740e 100644 --- a/x509-cert/src/name.rs +++ b/x509-cert/src/name.rs @@ -6,10 +6,10 @@ use const_oid::{ db::{rfc3280, rfc4519}, ObjectIdentifier, }; -use core::{fmt, str::FromStr}; +use core::{cmp::Ordering, fmt, str::FromStr}; use der::{ asn1::{Any, Ia5StringRef, PrintableStringRef, SetOfVec}, - Encode, + DecodeValue, Encode, EncodeValue, FixedTag, Header, Length, Reader, Tag, ValueOrd, Writer, }; /// X.501 Name as defined in [RFC 5280 Section 4.1.2.4]. X.501 Name is used to represent distinguished names. @@ -58,17 +58,74 @@ use der::{ /// [RFC 5280 Section 4.1.2.4]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Name(RdnSequence); - -// This will implement `From` which is provided as an escape hatch to build names -// from `bmpString`, `TeletexString`, or `UniversalString`: -// ``` -// When CAs have previously issued certificates with issuer fields with -// attributes encoded using TeletexString, BMPString, or -// UniversalString, then the CA MAY continue to use these encodings of -// the DirectoryString to preserve backward compatibility. -// ``` -impl_newtype!(Name, RdnSequence); +pub struct Name(pub(crate) RdnSequence); + +impl Name { + /// Build a name from an [`RdnSequence`]. + /// + /// + /// This is provided as an escape hatch (see [RFC 5280 Section 4.1.2.4]) to build + /// names from `bmpString`, `TeletexString`, or `UniversalString`: + /// ```text + /// When CAs have previously issued certificates with issuer fields with + /// attributes encoded using TeletexString, BMPString, or + /// UniversalString, then the CA MAY continue to use these encodings of + /// the DirectoryString to preserve backward compatibility. + /// ``` + /// + /// # Safety + /// + /// As the name implies, this is a dangerous helper. You are responsible for ensuring the + /// [`RdnSequence`] complies with the RFC. + /// + /// [RFC 5280 Section 4.1.2.4]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4 + #[cfg(feature = "hazmat")] + pub fn hazmat_from_rdn_sequence(value: RdnSequence) -> Self { + Self(value) + } +} + +impl From for RdnSequence { + #[inline] + fn from(value: Name) -> Self { + value.0 + } +} + +impl AsRef for Name { + #[inline] + fn as_ref(&self) -> &RdnSequence { + &self.0 + } +} + +impl FixedTag for Name { + const TAG: Tag = ::TAG; +} + +impl<'a> DecodeValue<'a> for Name { + type Error = der::Error; + + fn decode_value>(decoder: &mut R, header: Header) -> der::Result { + Ok(Self(RdnSequence::decode_value(decoder, header)?)) + } +} + +impl EncodeValue for Name { + fn encode_value(&self, encoder: &mut impl Writer) -> der::Result<()> { + self.0.encode_value(encoder) + } + + fn value_len(&self) -> der::Result { + self.0.value_len() + } +} + +impl ValueOrd for Name { + fn value_cmp(&self, other: &Self) -> der::Result { + self.0.value_cmp(&other.0) + } +} impl Name { /// Is this [`Name`] empty?