| //! X.509 objects and types | |
| //! | |
| //! Based on RFC5280 | |
| //! | |
| use crate::error::{X509Error, X509Result}; | |
| use crate::objects::*; | |
| use crate::traits::FromDer; | |
| use der_parser::ber::{parse_ber_integer, BitStringObject, MAX_OBJECT_SIZE}; | |
| use der_parser::der::*; | |
| use der_parser::error::*; | |
| use der_parser::num_bigint::BigUint; | |
| use der_parser::oid::Oid; | |
| use der_parser::*; | |
| use nom::branch::alt; | |
| use nom::bytes::complete::take; | |
| use nom::combinator::{complete, map, map_opt, map_res, opt}; | |
| use nom::multi::{many0, many1}; | |
| use nom::{Err, Offset}; | |
| use oid_registry::*; | |
| use rusticata_macros::newtype_enum; | |
| use std::convert::TryFrom; | |
| use std::fmt; | |
| use std::iter::FromIterator; | |
| /// The version of the encoded certificate. | |
| /// | |
| /// When extensions are used, as expected in this profile, version MUST be 3 | |
| /// (value is `2`). If no extensions are present, but a UniqueIdentifier | |
| /// is present, the version SHOULD be 2 (value is `1`); however, the | |
| /// version MAY be 3. If only basic fields are present, the version | |
| /// SHOULD be 1 (the value is omitted from the certificate as the default | |
| /// value); however, the version MAY be 2 or 3. | |
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
| pub struct X509Version(pub u32); | |
| impl X509Version { | |
| pub(crate) fn from_der_required(i: &[u8]) -> X509Result<X509Version> { | |
| let (rem, hdr) = | |
| der_read_element_header(i).or(Err(Err::Error(X509Error::InvalidVersion)))?; | |
| match hdr.tag.0 { | |
| 0 => { | |
| map(parse_der_u32, X509Version)(rem).or(Err(Err::Error(X509Error::InvalidVersion))) | |
| } | |
| _ => Ok((&rem[1..], X509Version::V1)), | |
| } | |
| } | |
| } | |
| // Parse [0] EXPLICIT Version DEFAULT v1 | |
| impl<'a> FromDer<'a> for X509Version { | |
| fn from_der(i: &'a [u8]) -> X509Result<'a, Self> { | |
| let (rem, hdr) = | |
| der_read_element_header(i).or(Err(Err::Error(X509Error::InvalidVersion)))?; | |
| match hdr.tag.0 { | |
| 0 => { | |
| map(parse_der_u32, X509Version)(rem).or(Err(Err::Error(X509Error::InvalidVersion))) | |
| } | |
| _ => Ok((i, X509Version::V1)), | |
| } | |
| } | |
| } | |
| newtype_enum! { | |
| impl display X509Version { | |
| V1 = 0, | |
| V2 = 1, | |
| V3 = 2, | |
| } | |
| } | |
| /// A generic attribute type and value | |
| /// | |
| /// These objects are used as [`RelativeDistinguishedName`] components. | |
| #[derive(Clone, Debug, PartialEq)] | |
| pub struct AttributeTypeAndValue<'a> { | |
| attr_type: Oid<'a>, | |
| attr_value: DerObject<'a>, // ANY -- DEFINED BY AttributeType | |
| } | |
| impl<'a> AttributeTypeAndValue<'a> { | |
| /// Builds a new `AttributeTypeAndValue` | |
| #[inline] | |
| pub const fn new(attr_type: Oid<'a>, attr_value: DerObject<'a>) -> Self { | |
| AttributeTypeAndValue { | |
| attr_type, | |
| attr_value, | |
| } | |
| } | |
| /// Returns the attribute type | |
| #[inline] | |
| pub const fn attr_type(&self) -> &Oid { | |
| &self.attr_type | |
| } | |
| /// Returns the attribute value, as raw `DerObject` | |
| #[inline] | |
| pub const fn attr_value(&self) -> &DerObject { | |
| &self.attr_value | |
| } | |
| /// Attempt to get the content as `str`. | |
| /// This can fail if the object does not contain a string type. | |
| /// | |
| /// Note: the [`TryFrom`] trait is implemented for `&str`, so this is equivalent to `attr.try_into()`. | |
| /// | |
| /// Only NumericString, PrintableString, UTF8String and IA5String | |
| /// are considered here. Other string types can be read using `as_slice`. | |
| #[inline] | |
| pub fn as_str(&self) -> Result<&'a str, X509Error> { | |
| self.attr_value.as_str().map_err(|e| e.into()) | |
| } | |
| /// Attempt to get the content as a slice. | |
| /// This can fail if the object does not contain a type directly equivalent to a slice (e.g a | |
| /// sequence). | |
| /// | |
| /// Note: the [`TryFrom`] trait is implemented for `&[u8]`, so this is equivalent to `attr.try_into()`. | |
| #[inline] | |
| pub fn as_slice(&self) -> Result<&'a [u8], X509Error> { | |
| self.attr_value.as_slice().map_err(|e| e.into()) | |
| } | |
| } | |
| impl<'a> TryFrom<AttributeTypeAndValue<'a>> for &'a str { | |
| type Error = X509Error; | |
| fn try_from(value: AttributeTypeAndValue<'a>) -> Result<Self, Self::Error> { | |
| value.attr_value.as_str().map_err(|e| e.into()) | |
| } | |
| } | |
| impl<'a> TryFrom<AttributeTypeAndValue<'a>> for &'a [u8] { | |
| type Error = X509Error; | |
| fn try_from(value: AttributeTypeAndValue<'a>) -> Result<Self, Self::Error> { | |
| value.attr_value.as_slice().map_err(|e| e.into()) | |
| } | |
| } | |
| // AttributeTypeAndValue ::= SEQUENCE { | |
| // type AttributeType, | |
| // value AttributeValue } | |
| impl<'a> FromDer<'a> for AttributeTypeAndValue<'a> { | |
| fn from_der(i: &'a [u8]) -> X509Result<'a, Self> { | |
| parse_der_sequence_defined_g(|i, _| { | |
| let (i, attr_type) = map_res(parse_der_oid, |x: DerObject<'a>| x.as_oid_val())(i) | |
| .or(Err(X509Error::InvalidX509Name))?; | |
| let (i, attr_value) = parse_attribute_value(i).or(Err(X509Error::InvalidX509Name))?; | |
| let attr = AttributeTypeAndValue::new(attr_type, attr_value); | |
| Ok((i, attr)) | |
| })(i) | |
| } | |
| } | |
| // AttributeValue ::= ANY -- DEFINED BY AttributeType | |
| #[inline] | |
| fn parse_attribute_value(i: &[u8]) -> DerResult { | |
| alt((parse_der, parse_malformed_string))(i) | |
| } | |
| fn parse_malformed_string(i: &[u8]) -> DerResult { | |
| let (rem, hdr) = der_read_element_header(i)?; | |
| let len = hdr.len.primitive()?; | |
| if len > MAX_OBJECT_SIZE { | |
| return Err(nom::Err::Error(BerError::InvalidLength)); | |
| } | |
| match hdr.tag { | |
| DerTag::PrintableString => { | |
| // if we are in this function, the PrintableString could not be validated. | |
| // Accept it without validating charset, because some tools do not respect the charset | |
| // restrictions (for ex. they use '*' while explicingly disallowed) | |
| let (rem, data) = take(len as usize)(rem)?; | |
| let s = std::str::from_utf8(data).map_err(|_| BerError::BerValueError)?; | |
| let content = DerObjectContent::PrintableString(s); | |
| let obj = DerObject::from_header_and_content(hdr, content); | |
| Ok((rem, obj)) | |
| } | |
| _ => Err(nom::Err::Error(BerError::InvalidTag)), | |
| } | |
| } | |
| /// A Relative Distinguished Name element. | |
| /// | |
| /// These objects are used as [`X509Name`] components. | |
| #[derive(Clone, Debug, PartialEq)] | |
| pub struct RelativeDistinguishedName<'a> { | |
| set: Vec<AttributeTypeAndValue<'a>>, | |
| } | |
| impl<'a> RelativeDistinguishedName<'a> { | |
| /// Builds a new `RelativeDistinguishedName` | |
| #[inline] | |
| pub const fn new(set: Vec<AttributeTypeAndValue<'a>>) -> Self { | |
| RelativeDistinguishedName { set } | |
| } | |
| /// Return an iterator over the components of this object | |
| pub fn iter(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.set.iter() | |
| } | |
| } | |
| impl<'a> FromIterator<AttributeTypeAndValue<'a>> for RelativeDistinguishedName<'a> { | |
| fn from_iter<T: IntoIterator<Item = AttributeTypeAndValue<'a>>>(iter: T) -> Self { | |
| let set = iter.into_iter().collect(); | |
| RelativeDistinguishedName { set } | |
| } | |
| } | |
| impl<'a> FromDer<'a> for RelativeDistinguishedName<'a> { | |
| fn from_der(i: &'a [u8]) -> X509Result<Self> { | |
| parse_der_set_defined_g(|i, _| { | |
| let (i, set) = many1(complete(AttributeTypeAndValue::from_der))(i)?; | |
| let rdn = RelativeDistinguishedName { set }; | |
| Ok((i, rdn)) | |
| })(i) | |
| } | |
| } | |
| #[derive(Clone, Debug, PartialEq)] | |
| pub struct SubjectPublicKeyInfo<'a> { | |
| pub algorithm: AlgorithmIdentifier<'a>, | |
| pub subject_public_key: BitStringObject<'a>, | |
| /// A raw unparsed PKIX, ASN.1 DER form (see RFC 5280, Section 4.1). | |
| pub raw: &'a [u8], | |
| } | |
| impl<'a> FromDer<'a> for SubjectPublicKeyInfo<'a> { | |
| /// Parse the SubjectPublicKeyInfo struct portion of a DER-encoded X.509 Certificate | |
| fn from_der(i: &'a [u8]) -> X509Result<Self> { | |
| let start_i = i; | |
| parse_der_sequence_defined_g(move |i, _| { | |
| let (i, algorithm) = AlgorithmIdentifier::from_der(i)?; | |
| let (i, subject_public_key) = map_res(parse_der_bitstring, |x: DerObject<'a>| { | |
| match x.content { | |
| DerObjectContent::BitString(_, ref b) => Ok(b.to_owned()), // XXX padding ignored | |
| _ => Err(BerError::BerTypeError), | |
| } | |
| })(i) | |
| .or(Err(X509Error::InvalidSPKI))?; | |
| let len = start_i.offset(i); | |
| let raw = &start_i[..len]; | |
| let spki = SubjectPublicKeyInfo { | |
| algorithm, | |
| subject_public_key, | |
| raw, | |
| }; | |
| Ok((i, spki)) | |
| })(i) | |
| } | |
| } | |
| /// Algorithm identifier | |
| /// | |
| /// An algorithm identifier is defined by the following ASN.1 structure: | |
| /// | |
| /// <pre> | |
| /// AlgorithmIdentifier ::= SEQUENCE { | |
| /// algorithm OBJECT IDENTIFIER, | |
| /// parameters ANY DEFINED BY algorithm OPTIONAL } | |
| /// </pre> | |
| /// | |
| /// The algorithm identifier is used to identify a cryptographic | |
| /// algorithm. The OBJECT IDENTIFIER component identifies the algorithm | |
| /// (such as DSA with SHA-1). The contents of the optional parameters | |
| /// field will vary according to the algorithm identified. | |
| #[derive(Clone, Debug, PartialEq)] | |
| pub struct AlgorithmIdentifier<'a> { | |
| pub algorithm: Oid<'a>, | |
| pub parameters: Option<DerObject<'a>>, | |
| } | |
| impl<'a> FromDer<'a> for AlgorithmIdentifier<'a> { | |
| #[allow(clippy::needless_lifetimes)] | |
| fn from_der(i: &[u8]) -> X509Result<AlgorithmIdentifier> { | |
| parse_der_sequence_defined_g(|i, _| { | |
| let (i, algorithm) = map_res(parse_der_oid, |x| x.as_oid_val())(i) | |
| .or(Err(X509Error::InvalidAlgorithmIdentifier))?; | |
| let (i, parameters) = | |
| opt(complete(parse_der))(i).or(Err(X509Error::InvalidAlgorithmIdentifier))?; | |
| let alg = AlgorithmIdentifier { | |
| algorithm, | |
| parameters, | |
| }; | |
| Ok((i, alg)) | |
| })(i) | |
| } | |
| } | |
| /// X.509 Name (as used in `Issuer` and `Subject` fields) | |
| /// | |
| /// The Name describes a hierarchical name composed of attributes, such | |
| /// as country name, and corresponding values, such as US. The type of | |
| /// the component AttributeValue is determined by the AttributeType; in | |
| /// general it will be a DirectoryString. | |
| #[derive(Clone, Debug, PartialEq)] | |
| pub struct X509Name<'a> { | |
| pub(crate) rdn_seq: Vec<RelativeDistinguishedName<'a>>, | |
| pub(crate) raw: &'a [u8], | |
| } | |
| impl<'a> fmt::Display for X509Name<'a> { | |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
| match x509name_to_string(&self.rdn_seq, oid_registry()) { | |
| Ok(o) => write!(f, "{}", o), | |
| Err(_) => write!(f, "<X509Error: Invalid X.509 name>"), | |
| } | |
| } | |
| } | |
| impl<'a> X509Name<'a> { | |
| /// Builds a new `X509Name` from the provided elements. | |
| #[inline] | |
| pub const fn new(rdn_seq: Vec<RelativeDistinguishedName<'a>>, raw: &'a [u8]) -> Self { | |
| X509Name { rdn_seq, raw } | |
| } | |
| /// Attempt to format the current name, using the given registry to convert OIDs to strings. | |
| /// | |
| /// Note: a default registry is provided with this crate, and is returned by the | |
| /// [`oid_registry()`] method. | |
| pub fn to_string_with_registry(&self, oid_registry: &OidRegistry) -> Result<String, X509Error> { | |
| x509name_to_string(&self.rdn_seq, oid_registry) | |
| } | |
| // Not using the AsRef trait, as that would not give back the full 'a lifetime | |
| pub fn as_raw(&self) -> &'a [u8] { | |
| self.raw | |
| } | |
| /// Return an iterator over the `RelativeDistinguishedName` components of the name | |
| pub fn iter(&self) -> impl Iterator<Item = &RelativeDistinguishedName<'a>> { | |
| self.rdn_seq.iter() | |
| } | |
| /// Return an iterator over the `RelativeDistinguishedName` components of the name | |
| pub fn iter_rdn(&self) -> impl Iterator<Item = &RelativeDistinguishedName<'a>> { | |
| self.rdn_seq.iter() | |
| } | |
| /// Return an iterator over the attribute types and values of the name | |
| pub fn iter_attributes(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.rdn_seq.iter().map(|rdn| rdn.set.iter()).flatten() | |
| } | |
| /// Return an iterator over the components identified by the given OID | |
| /// | |
| /// The type of the component AttributeValue is determined by the AttributeType; in | |
| /// general it will be a DirectoryString. | |
| /// | |
| /// Attributes with same OID may be present multiple times, so the returned object is | |
| /// an iterator. | |
| /// Expected number of objects in this iterator are | |
| /// - 0: not found | |
| /// - 1: present once (common case) | |
| /// - 2 or more: attribute is present multiple times | |
| pub fn iter_by_oid(&self, oid: &Oid<'a>) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| // this is necessary, otherwise rustc complains | |
| // that caller creates a temporary value for reference (for ex. | |
| // `self.iter_by_oid(&OID_X509_LOCALITY_NAME)` | |
| // ) | |
| let oid = oid.clone(); | |
| self.iter_attributes() | |
| .filter(move |obj| obj.attr_type == oid) | |
| } | |
| /// Return an iterator over the `CommonName` attributes of the X.509 Name. | |
| /// | |
| /// Returned iterator can be empty if there are no `CommonName` attributes. | |
| /// If you expect only one `CommonName` to be present, then using `next()` will | |
| /// get an `Option<&AttributeTypeAndValue>`. | |
| /// | |
| /// A common operation is to extract the `CommonName` as a string. | |
| /// | |
| /// ``` | |
| /// use x509_parser::x509::X509Name; | |
| /// | |
| /// fn get_first_cn_as_str<'a>(name: &'a X509Name<'_>) -> Option<&'a str> { | |
| /// name.iter_common_name() | |
| /// .next() | |
| /// .and_then(|cn| cn.as_str().ok()) | |
| /// } | |
| /// ``` | |
| /// | |
| /// Note that there are multiple reasons for failure or incorrect behavior, for ex. if | |
| /// the attribute is present multiple times, or is not a UTF-8 encoded string (it can be | |
| /// UTF-16, or even an OCTETSTRING according to the standard). | |
| pub fn iter_common_name(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_COMMON_NAME) | |
| } | |
| /// Return an iterator over the `Country` attributes of the X.509 Name. | |
| pub fn iter_country(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_COUNTRY_NAME) | |
| } | |
| /// Return an iterator over the `Organization` attributes of the X.509 Name. | |
| pub fn iter_organization(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_ORGANIZATION_NAME) | |
| } | |
| /// Return an iterator over the `OrganizationalUnit` attributes of the X.509 Name. | |
| pub fn iter_organizational_unit(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_ORGANIZATIONAL_UNIT) | |
| } | |
| /// Return an iterator over the `StateOrProvinceName` attributes of the X.509 Name. | |
| pub fn iter_state_or_province(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_STATE_OR_PROVINCE_NAME) | |
| } | |
| /// Return an iterator over the `Locality` attributes of the X.509 Name. | |
| pub fn iter_locality(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_X509_LOCALITY_NAME) | |
| } | |
| /// Return an iterator over the `EmailAddress` attributes of the X.509 Name. | |
| pub fn iter_email(&self) -> impl Iterator<Item = &AttributeTypeAndValue<'a>> { | |
| self.iter_by_oid(&OID_PKCS9_EMAIL_ADDRESS) | |
| } | |
| } | |
| impl<'a> FromIterator<RelativeDistinguishedName<'a>> for X509Name<'a> { | |
| fn from_iter<T: IntoIterator<Item = RelativeDistinguishedName<'a>>>(iter: T) -> Self { | |
| let rdn_seq = iter.into_iter().collect(); | |
| X509Name { rdn_seq, raw: &[] } | |
| } | |
| } | |
| impl<'a> From<X509Name<'a>> for Vec<RelativeDistinguishedName<'a>> { | |
| fn from(name: X509Name<'a>) -> Self { | |
| name.rdn_seq | |
| } | |
| } | |
| impl<'a> FromDer<'a> for X509Name<'a> { | |
| /// Parse the X.501 type Name, used for ex in issuer and subject of a X.509 certificate | |
| fn from_der(i: &'a [u8]) -> X509Result<Self> { | |
| let start_i = i; | |
| parse_der_sequence_defined_g(move |i, _| { | |
| let (i, rdn_seq) = many0(complete(RelativeDistinguishedName::from_der))(i)?; | |
| let len = start_i.offset(i); | |
| let name = X509Name { | |
| rdn_seq, | |
| raw: &start_i[..len], | |
| }; | |
| Ok((i, name)) | |
| })(i) | |
| } | |
| } | |
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
| pub struct ReasonCode(pub u8); | |
| newtype_enum! { | |
| impl display ReasonCode { | |
| Unspecified = 0, | |
| KeyCompromise = 1, | |
| CACompromise = 2, | |
| AffiliationChanged = 3, | |
| Superseded = 4, | |
| CessationOfOperation = 5, | |
| CertificateHold = 6, | |
| // value 7 is not used | |
| RemoveFromCRL = 8, | |
| PrivilegeWithdrawn = 9, | |
| AACompromise = 10, | |
| } | |
| } | |
| impl Default for ReasonCode { | |
| fn default() -> Self { | |
| ReasonCode::Unspecified | |
| } | |
| } | |
| // Attempt to convert attribute to string. If type is not a string, return value is the hex | |
| // encoding of the attribute value | |
| fn attribute_value_to_string(attr: &DerObject, _attr_type: &Oid) -> Result<String, X509Error> { | |
| match attr.content { | |
| DerObjectContent::NumericString(s) | |
| | DerObjectContent::PrintableString(s) | |
| | DerObjectContent::UTF8String(s) | |
| | DerObjectContent::IA5String(s) => Ok(s.to_owned()), | |
| _ => { | |
| // type is not a string, get slice and convert it to base64 | |
| attr.as_slice() | |
| .map(|s| base64::encode(s)) | |
| .or(Err(X509Error::InvalidX509Name)) | |
| } | |
| } | |
| } | |
| /// Convert a DER representation of a X.509 name to a human-readable string | |
| /// | |
| /// RDNs are separated with "," | |
| /// Multiple RDNs are separated with "+" | |
| /// | |
| /// Attributes that cannot be represented by a string are hex-encoded | |
| fn x509name_to_string( | |
| rdn_seq: &[RelativeDistinguishedName], | |
| oid_registry: &OidRegistry, | |
| ) -> Result<String, X509Error> { | |
| rdn_seq.iter().fold(Ok(String::new()), |acc, rdn| { | |
| acc.and_then(|mut _vec| { | |
| rdn.set | |
| .iter() | |
| .fold(Ok(String::new()), |acc2, attr| { | |
| acc2.and_then(|mut _vec2| { | |
| let val_str = attribute_value_to_string(&attr.attr_value, &attr.attr_type)?; | |
| // look ABBREV, and if not found, use shortname | |
| let abbrev = match oid2abbrev(&attr.attr_type, oid_registry) { | |
| Ok(s) => String::from(s), | |
| _ => format!("{:?}", attr.attr_type), | |
| }; | |
| let rdn = format!("{}={}", abbrev, val_str); | |
| match _vec2.len() { | |
| 0 => Ok(rdn), | |
| _ => Ok(_vec2 + " + " + &rdn), | |
| } | |
| }) | |
| }) | |
| .map(|v| match _vec.len() { | |
| 0 => v, | |
| _ => _vec + ", " + &v, | |
| }) | |
| }) | |
| }) | |
| } | |
| pub(crate) fn parse_signature_value(i: &[u8]) -> X509Result<BitStringObject> { | |
| map_res(parse_der_bitstring, |x: DerObject| { | |
| match x.content { | |
| DerObjectContent::BitString(_, ref b) => Ok(b.to_owned()), // XXX padding ignored | |
| _ => Err(BerError::BerTypeError), | |
| } | |
| })(i) | |
| .or(Err(Err::Error(X509Error::InvalidSignatureValue))) | |
| } | |
| pub(crate) fn parse_serial(i: &[u8]) -> X509Result<(&[u8], BigUint)> { | |
| // This should be parse_der_integer, but some certificates encode leading zeroes | |
| map_opt(parse_ber_integer, get_serial_info)(i).map_err(|_| X509Error::InvalidSerial.into()) | |
| } | |
| fn get_serial_info(o: DerObject) -> Option<(&[u8], BigUint)> { | |
| // RFC 5280 4.1.2.2: "The serial number MUST be a positive integer" | |
| // however, many CAs do not respect this and send integers with MSB set, | |
| // so we do not use `as_biguint()` | |
| let slice = o.as_slice().ok()?; | |
| let big = BigUint::from_bytes_be(slice); | |
| Some((slice, big)) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use der_parser::ber::BerObjectContent; | |
| use der_parser::oid; | |
| #[test] | |
| fn test_x509_name() { | |
| let name = X509Name { | |
| rdn_seq: vec![ | |
| RelativeDistinguishedName { | |
| set: vec![AttributeTypeAndValue { | |
| attr_type: oid!(2.5.4 .6), // countryName | |
| attr_value: DerObject::from_obj(BerObjectContent::PrintableString("FR")), | |
| }], | |
| }, | |
| RelativeDistinguishedName { | |
| set: vec![AttributeTypeAndValue { | |
| attr_type: oid!(2.5.4 .8), // stateOrProvinceName | |
| attr_value: DerObject::from_obj(BerObjectContent::PrintableString( | |
| "Some-State", | |
| )), | |
| }], | |
| }, | |
| RelativeDistinguishedName { | |
| set: vec![AttributeTypeAndValue { | |
| attr_type: oid!(2.5.4 .10), // organizationName | |
| attr_value: DerObject::from_obj(BerObjectContent::PrintableString( | |
| "Internet Widgits Pty Ltd", | |
| )), | |
| }], | |
| }, | |
| RelativeDistinguishedName { | |
| set: vec![ | |
| AttributeTypeAndValue { | |
| attr_type: oid!(2.5.4 .3), // CN | |
| attr_value: DerObject::from_obj(BerObjectContent::PrintableString( | |
| "Test1", | |
| )), | |
| }, | |
| AttributeTypeAndValue { | |
| attr_type: oid!(2.5.4 .3), // CN | |
| attr_value: DerObject::from_obj(BerObjectContent::PrintableString( | |
| "Test2", | |
| )), | |
| }, | |
| ], | |
| }, | |
| ], | |
| raw: &[], // incorrect, but enough for testing | |
| }; | |
| assert_eq!( | |
| name.to_string(), | |
| "C=FR, ST=Some-State, O=Internet Widgits Pty Ltd, CN=Test1 + CN=Test2" | |
| ); | |
| } | |
| } |