From 31730e19460a5fc89b305319227fba085abb7ad5 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Mon, 17 Nov 2025 13:41:49 +0100 Subject: [PATCH 1/2] Add support for multiple values of the same name type --- rcgen/src/lib.rs | 57 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 83816182..964b8de3 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -33,6 +33,7 @@ println!("{}", signing_key.serialize_pem()); #![warn(unreachable_pub)] use std::borrow::Cow; +use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt; use std::hash::Hash; @@ -470,7 +471,7 @@ See also the RFC 5280 sections on the [issuer](https://tools.ietf.org/html/rfc52 and [subject](https://tools.ietf.org/html/rfc5280#section-4.1.2.6) fields. */ pub struct DistinguishedName { - entries: HashMap, + entries: HashMap>, order: Vec, } @@ -479,10 +480,17 @@ impl DistinguishedName { pub fn new() -> Self { Self::default() } - /// Obtains the attribute value for the given attribute type - pub fn get(&self, ty: &DnType) -> Option<&DnValue> { - self.entries.get(ty) + + /// Obtains the first attribute value for the given attribute type + pub fn first(&self, ty: &DnType) -> Option<&DnValue> { + self.entries.get(ty)?.first() + } + + /// Obtains all attribute values for the given attribute type + pub fn get(&self, ty: &DnType) -> Option<&[DnValue]> { + self.entries.get(ty).map(|v| v.as_slice()) } + /// Removes the attribute with the specified DnType /// /// Returns true when an actual removal happened, false @@ -502,20 +510,24 @@ impl DistinguishedName { /// let mut dn = DistinguishedName::new(); /// dn.push(DnType::OrganizationName, "Crab widgits SE"); /// dn.push(DnType::CommonName, DnValue::PrintableString("Master Cert".try_into().unwrap())); - /// assert_eq!(dn.get(&DnType::OrganizationName), Some(&DnValue::Utf8String("Crab widgits SE".to_string()))); - /// assert_eq!(dn.get(&DnType::CommonName), Some(&DnValue::PrintableString("Master Cert".try_into().unwrap()))); + /// assert_eq!(dn.first(&DnType::OrganizationName), Some(&DnValue::Utf8String("Crab widgits SE".to_string()))); + /// assert_eq!(dn.first(&DnType::CommonName), Some(&DnValue::PrintableString("Master Cert".try_into().unwrap()))); /// ``` pub fn push(&mut self, ty: DnType, s: impl Into) { - if !self.entries.contains_key(&ty) { - self.order.push(ty.clone()); + match self.entries.entry(ty.clone()) { + Entry::Occupied(mut o) => o.get_mut().push(s.into()), + Entry::Vacant(v) => { + v.insert(Vec::new()).push(s.into()); + self.order.push(ty); + }, } - self.entries.insert(ty, s.into()); } /// Iterate over the entries pub fn iter(&self) -> DistinguishedNameIterator<'_> { DistinguishedNameIterator { distinguished_name: self, iter: self.order.iter(), + current: None, } } @@ -571,15 +583,36 @@ Iterator over [`DistinguishedName`] entries pub struct DistinguishedNameIterator<'a> { distinguished_name: &'a DistinguishedName, iter: std::slice::Iter<'a, DnType>, + current: Option<(&'a DnType, std::slice::Iter<'a, DnValue>)>, } impl<'a> Iterator for DistinguishedNameIterator<'a> { type Item = (&'a DnType, &'a DnValue); fn next(&mut self) -> Option { - self.iter - .next() - .and_then(|ty| self.distinguished_name.entries.get(ty).map(|v| (ty, v))) + if let Some((ty, values)) = &mut self.current { + match values.next() { + Some(val) => return Some((ty, val)), + None => self.current = None, + } + } + + for ty in &mut self.iter { + let Some(values) = self.distinguished_name.entries.get(ty) else { + continue; + }; + + match values.as_slice() { + [] => continue, + [first] => return Some((ty, first)), + [first, rest @ ..] => { + self.current = Some((ty, rest.iter())); + return Some((ty, first)); + }, + } + } + + None } } From ec62aeb5907bbb70b522ee1b825ea25f8ac0fa1f Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Fri, 6 Mar 2026 17:04:33 +1100 Subject: [PATCH 2/2] test: Allow multiple issuer items of the same kind --- rcgen/src/certificate.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 6a238862..02a2d5c8 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1493,4 +1493,43 @@ PITGdT9dgN88nHPCle0B1+OY+OZ5 ); } } + + #[cfg(feature = "x509-parser")] + #[test] + fn test_certificate_with_multiple_domain_components_roundtrip() { + let domain_component_dn_type = DnType::CustomDnType(vec![0, 9, 2342, 19200300, 100, 1, 25]); // Domain Component (DC) + + let dc_value_1 = DnValue::Ia5String("example".try_into().unwrap()); + let dc_value_2 = DnValue::Ia5String("com".try_into().unwrap()); + + let mut params = CertificateParams::new(vec!["crabs".to_owned()]).unwrap(); + params.distinguished_name = DistinguishedName::new(); + params + .distinguished_name + .push(domain_component_dn_type.clone(), dc_value_1.clone()); + + params + .distinguished_name + .push(domain_component_dn_type.clone(), dc_value_2.clone()); + + let key_pair = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + // We should be able to parse the certificate with x509-parser. + assert!(x509_parser::parse_x509_certificate(cert.der()).is_ok()); + + // We should be able to reconstitute params from the DER using x509-parser. + let params_from_cert = CertificateParams::from_ca_cert_der(cert.der()).unwrap(); + + // We should find the expected distinguished name in the reconstituted params. + let expected_names = &[ + (&domain_component_dn_type, &dc_value_1), + (&domain_component_dn_type, &dc_value_2), + ]; + let names = params_from_cert + .distinguished_name + .iter() + .collect::>(); + assert_eq!(names, expected_names); + } }