diff --git a/rcgen/examples/sign-leaf-with-ca.rs b/rcgen/examples/sign-leaf-with-ca.rs index ffe37850..8361475c 100644 --- a/rcgen/examples/sign-leaf-with-ca.rs +++ b/rcgen/examples/sign-leaf-with-ca.rs @@ -1,7 +1,7 @@ use rcgen::DnValue::PrintableString; use rcgen::{ - BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, - Issuer, KeyPair, KeyUsagePurpose, + BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, Issuer, + KeyPair, KeyUsagePurpose, PathLenConstraint, }; use time::{Duration, OffsetDateTime}; @@ -21,7 +21,7 @@ fn new_ca() -> (Certificate, Issuer<'static, KeyPair>) { let mut params = CertificateParams::new(Vec::default()).expect("empty subject alt name can't produce error"); let (yesterday, tomorrow) = validity_period(); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); params.distinguished_name.push( DnType::CountryName, PrintableString("BR".try_into().unwrap()), diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 71dc7843..98238851 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -6,12 +6,11 @@ use pem::Pem; use pki_types::{CertificateDer, CertificateSigningRequestDer}; use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; use yasna::models::ObjectIdentifier; -use yasna::{DERWriter, DERWriterSeq, Tag}; +use yasna::Tag; use crate::csr::CertificateSigningRequest; use crate::extension::{ - AuthorityKeyIdentifier, CrlDistributionPoints, ExtendedKeyUsage, Extension, KeyUsage, - NameConstraintsExt, SubjectAlternativeName, SubjectKeyIdentifier, + AuthorityKeyIdentifier, Extensions, NonEmptySlice, SubjectAlternativeName, SubjectKeyIdentifier, }; use crate::key_pair::{serialize_public_key_der, sign_der, PublicKeyData}; #[cfg(feature = "crypto")] @@ -19,9 +18,9 @@ use crate::ring_like::digest; #[cfg(feature = "pem")] use crate::ENCODE_CONFIG; use crate::{ - oid, write_distinguished_name, write_dt_utc_or_generalized, write_x509_extension, - CrlDistributionPoint, DistinguishedName, Error, ExtendedKeyUsagePurpose, GeneralName, Issuer, - KeyIdMethod, KeyUsagePurpose, NameConstraints, SerialNumber, SigningKey, + oid, write_distinguished_name, write_dt_utc_or_generalized, BasicConstraints, + CrlDistributionPoint, CustomExtension, DistinguishedName, Error, ExtendedKeyUsagePurpose, + GeneralName, Issuer, KeyIdMethod, KeyUsagePurpose, NameConstraints, SerialNumber, SigningKey, }; /// An issued certificate @@ -62,7 +61,13 @@ pub struct CertificateParams { pub serial_number: Option, pub subject_alt_names: Vec, pub distinguished_name: DistinguishedName, - pub is_ca: IsCa, + /// Basic constraints for a CA or an explicit end-entity certificate. + /// + /// `None` omits the extension; `Some(BasicConstraints::EndEntity)` explicitly + /// describes a non-CA certificate. Use + /// `Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained))` for a CA + /// certificate without a path length limit. + pub basic_constraints: Option, pub key_usages: Vec, pub extended_key_usages: Vec, pub name_constraints: Option, @@ -94,7 +99,7 @@ impl Default for CertificateParams { serial_number: None, subject_alt_names: Vec::new(), distinguished_name, - is_ca: IsCa::NoCa, + basic_constraints: None, key_usages: Vec::new(), extended_key_usages: Vec::new(), name_constraints: None, @@ -174,7 +179,7 @@ impl CertificateParams { .map_err(|_| Error::CouldNotParseCertificate)?; Ok(CertificateParams { - is_ca: IsCa::from_x509(&x509)?, + basic_constraints: BasicConstraints::from_x509(&x509)?, subject_alt_names: GeneralName::from_x509(&x509)?, key_usages: KeyUsagePurpose::from_x509(&x509)?, extended_key_usages: ExtendedKeyUsagePurpose::from_x509(&x509)?, @@ -188,60 +193,25 @@ impl CertificateParams { }) } - /// Write a CSR extension request attribute as defined in [RFC 2985]. + /// Returns the X.509 extensions for a CSR extension request attribute as defined + /// in [RFC 2985]. + /// + /// Returns an [`Error`] if the described extensions are invalid. /// /// [RFC 2985]: - fn write_extension_request_attribute(&self, writer: DERWriter) { - writer.write_sequence(|writer| { - writer.next().write_oid(&ObjectIdentifier::from_slice( - oid::PKCS_9_AT_EXTENSION_REQUEST, - )); - writer.next().write_set(|writer| { - writer.next().write_sequence(|writer| { - if let Some(ku) = KeyUsage::from_params(self) { - ku.write(writer.next()); - } - if let Some(san) = SubjectAlternativeName::from_params(self) { - san.write(writer.next()); - } - if let Some(eku) = ExtendedKeyUsage::from_params(self) { - eku.write(writer.next()); - } - self.write_ca_extensions(writer); - for ext in &self.custom_extensions { - write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| { - writer.write_der(ext.content()) - }); - } - }); - }); - }); - } + fn csr_extensions(&self) -> Result, Error> { + let mut exts = Extensions::default(); - /// Write a certificate's BasicConstraints as defined in RFC 5280. - fn write_ca_extensions(&self, writer: &mut DERWriterSeq) { - let is_ca = match &self.is_ca { - IsCa::Ca(bc) => Some(bc), - IsCa::ExplicitNoCa => None, - IsCa::NoCa => return, - }; + exts.push_if_some(SubjectAlternativeName::from_params(self))?; + exts.push_if_some(NonEmptySlice::new(&self.key_usages))?; + exts.push_if_some(NonEmptySlice::new(&self.extended_key_usages))?; + exts.push_if_some(self.basic_constraints)?; - // Write basic_constraints - write_x509_extension(writer.next(), oid::BASIC_CONSTRAINTS, true, |writer| { - writer.write_sequence(|writer| { - let Some(constraints) = is_ca else { - return; - }; + for custom_ext in &self.custom_extensions { + exts.push(custom_ext)?; + } - writer.next().write_bool(true); // cA flag - match constraints { - BasicConstraints::Unconstrained => {}, - BasicConstraints::Constrained(path_len_constraint) => { - writer.next().write_u8(*path_len_constraint); // pathLenConstraint integer - }, - } - }); - }); + Ok(exts) } /// Generate and serialize a certificate signing request (CSR). @@ -283,7 +253,7 @@ impl CertificateParams { serial_number, subject_alt_names, distinguished_name, - is_ca, + basic_constraints, key_usages, extended_key_usages, name_constraints, @@ -294,7 +264,9 @@ impl CertificateParams { } = self; // - subject_key will be used by the caller // - not_before and not_after cannot be put in a CSR - // - key_identifier_method is here because self.write_extended_key_usage uses it + // - The extension request fields (subject_alt_names, key_usages, + // extended_key_usages, basic_constraints, custom_extensions) are handled by + // self.csr_extensions() // - There might be a use case for specifying the key identifier // in the CSR, but in the current API it can't be distinguished // from the defaults so this is left for a later version if @@ -303,7 +275,11 @@ impl CertificateParams { not_before, not_after, key_identifier_method, + subject_alt_names, + key_usages, extended_key_usages, + basic_constraints, + custom_extensions, ); if serial_number.is_some() || name_constraints.is_some() @@ -313,12 +289,9 @@ impl CertificateParams { return Err(Error::UnsupportedInCsr); } - // Whether or not to write an extension request attribute - let write_extension_request = !key_usages.is_empty() - || !subject_alt_names.is_empty() - || !extended_key_usages.is_empty() - || !custom_extensions.is_empty() - || matches!(is_ca, IsCa::ExplicitNoCa | IsCa::Ca(_)); + // The extension request attribute is elided entirely when the built + // collection is empty. + let extension_request = self.csr_extensions()?; let der = sign_der(subject_key, |writer| { // Write version @@ -332,9 +305,7 @@ impl CertificateParams { .write_tagged_implicit(Tag::context(0), |writer| { // RFC 2986 specifies that attributes are a SET OF Attribute writer.write_set_of(|writer| { - if write_extension_request { - self.write_extension_request_attribute(writer.next()); - } + extension_request.write_csr_attribute(writer); for Attribute { oid, values } in attrs { writer.next().write_sequence(|writer| { @@ -358,19 +329,9 @@ impl CertificateParams { pub_key: &K, issuer: &Issuer<'_, impl SigningKey>, ) -> Result, Error> { - // An empty distribution point would be encoded as an empty fullName, - // violating GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName - // (RFC 5280 §4.2.1.13). - if self - .crl_distribution_points - .iter() - .any(|dp| dp.uris.is_empty()) - { - return Err(Error::EmptyCrlDistributionPointUris); - } - + let pub_key_spki = pub_key.subject_public_key_info(); + let extensions = self.extensions(&pub_key_spki, issuer)?; let der = sign_der(&issuer.signing_key, |writer| { - let pub_key_spki = pub_key.subject_public_key_info(); // Write version writer.next().write_tagged(Tag::context(0), |writer| { writer.write_u8(2); @@ -411,23 +372,9 @@ impl CertificateParams { write_distinguished_name(writer.next(), &self.distinguished_name); // Write subjectPublicKeyInfo serialize_public_key_der(pub_key, writer.next()); - // write extensions - let should_write_exts = self.use_authority_key_identifier_extension - || !self.subject_alt_names.is_empty() - || !self.key_usages.is_empty() - || !self.extended_key_usages.is_empty() - || self.name_constraints.iter().any(|c| !c.is_empty()) - || !self.crl_distribution_points.is_empty() - || matches!(self.is_ca, IsCa::ExplicitNoCa) - || matches!(self.is_ca, IsCa::Ca(_)) - || !self.custom_extensions.is_empty(); - if !should_write_exts { - return Ok(()); - } - - writer.next().write_tagged(Tag::context(3), |writer| { - writer.write_sequence(|writer| self.write_extensions(writer, &pub_key_spki, issuer)) - })?; + // Write extensions. The field is omitted entirely when the built + // collection is empty. + extensions.write_cert_der(writer.next()); Ok(()) })?; @@ -435,50 +382,39 @@ impl CertificateParams { Ok(der.into()) } - fn write_extensions( + /// Returns the X.509 extensions that the [`CertificateParams`] describe. + /// + /// Returns an [`Error`] if the described extensions are invalid. + fn extensions( &self, - writer: &mut DERWriterSeq, pub_key_spki: &[u8], issuer: &Issuer<'_, impl SigningKey>, - ) -> Result<(), Error> { - if self.use_authority_key_identifier_extension { - AuthorityKeyIdentifier::from(issuer).write(writer.next()); - } + ) -> Result, Error> { + let mut exts = Extensions::default(); - if let Some(san) = SubjectAlternativeName::from_params(self) { - san.write(writer.next()); - } - if let Some(ku) = KeyUsage::from_params(self) { - ku.write(writer.next()); - } - if let Some(eku) = ExtendedKeyUsage::from_params(self) { - eku.write(writer.next()); - } - - if let Some(nc) = NameConstraintsExt::from_params(self) { - nc.write(writer.next()); - } - - if let Some(crl_dps) = CrlDistributionPoints::from_params(self) { - crl_dps.write(writer.next()); - } - - // SKI is currently only written for CA certificates (IsCa::Ca or - // IsCa::ExplicitNoCa). - if self.is_ca != IsCa::NoCa { - SubjectKeyIdentifier::new(&self.key_identifier_method, pub_key_spki) - .write(writer.next()); + if self.use_authority_key_identifier_extension { + exts.push(AuthorityKeyIdentifier::from(issuer))?; } - self.write_ca_extensions(writer); - - for ext in &self.custom_extensions { - write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| { - writer.write_der(ext.content()) - }); + exts.push_if_some(SubjectAlternativeName::from_params(self))?; + exts.push_if_some(NonEmptySlice::new(&self.key_usages))?; + exts.push_if_some(NonEmptySlice::new(&self.extended_key_usages))?; + exts.push_if_some(self.name_constraints.as_ref())?; + exts.push_if_some(NonEmptySlice::new(&self.crl_distribution_points))?; + + // RFC 5280 §4.2.1.2 describes the SKI as a MUST for CA certificates and a + // SHOULD for end entity certificates, so it is emitted for all certificates. + exts.push(SubjectKeyIdentifier::new( + &self.key_identifier_method, + pub_key_spki, + ))?; + exts.push_if_some(self.basic_constraints)?; + + for custom_ext in &self.custom_extensions { + exts.push(custom_ext)?; } - Ok(()) + Ok(exts) } /// Insert an extended key usage (EKU) into the parameters if it does not already exist @@ -515,59 +451,6 @@ pub struct Attribute { pub values: Vec, } -/// A custom extension of a certificate, as specified in -/// [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2) -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub struct CustomExtension { - oid: Vec, - critical: bool, - - /// The content must be DER-encoded - content: Vec, -} - -impl CustomExtension { - /// Creates a new acmeIdentifier extension for ACME TLS-ALPN-01 - /// as specified in [RFC 8737](https://tools.ietf.org/html/rfc8737#section-3) - /// - /// Panics if the passed `sha_digest` parameter doesn't hold 32 bytes (256 bits). - pub fn new_acme_identifier(sha_digest: &[u8]) -> Self { - assert_eq!(sha_digest.len(), 32, "wrong size of sha_digest"); - let content = yasna::construct_der(|writer| { - writer.write_bytes(sha_digest); - }); - Self { - oid: oid::PE_ACME.to_owned(), - critical: true, - content, - } - } - /// Create a new custom extension with the specified content - pub fn from_oid_content(oid: &[u64], content: Vec) -> Self { - Self { - oid: oid.to_owned(), - critical: false, - content, - } - } - /// Sets the criticality flag of the extension. - pub fn set_criticality(&mut self, criticality: bool) { - self.critical = criticality; - } - /// Obtains the criticality flag of the extension. - pub fn criticality(&self) -> bool { - self.critical - } - /// Obtains the content of the extension. - pub fn content(&self) -> &[u8] { - &self.content - } - /// Obtains the OID components of the extensions, as u64 pieces - pub fn oid_components(&self) -> impl Iterator + '_ { - self.oid.iter().copied() - } -} - #[derive(Debug, PartialEq, Eq, Hash, Clone)] #[non_exhaustive] /// The attribute type of a distinguished name entry @@ -633,69 +516,6 @@ pub fn date_time_ymd(year: i32, month: u8, day: u8) -> OffsetDateTime { primitive_dt.assume_utc() } -/// Whether the certificate is allowed to sign other certificates -#[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum IsCa { - /// The certificate can only sign itself - NoCa, - /// The certificate can only sign itself, adding the extension and `CA:FALSE` - ExplicitNoCa, - /// The certificate may be used to sign other certificates - Ca(BasicConstraints), -} - -impl IsCa { - #[cfg(all(test, feature = "x509-parser"))] - fn from_x509(x509: &x509_parser::certificate::X509Certificate<'_>) -> Result { - let basic_constraints = x509 - .basic_constraints() - .map_err(|_| Error::CouldNotParseCertificate)? - .map(|ext| ext.value); - - match basic_constraints { - Some(bc) => Self::from_basic_constraints(bc), - None => Ok(Self::NoCa), - } - } - - #[cfg(feature = "x509-parser")] - pub(crate) fn from_basic_constraints( - basic_constraints: &x509_parser::extensions::BasicConstraints, - ) -> Result { - use x509_parser::extensions::BasicConstraints as B; - - Ok(match basic_constraints { - B { - ca: true, - path_len_constraint: Some(n), - } if *n <= u8::MAX as u32 => Self::Ca(BasicConstraints::Constrained(*n as u8)), - B { - ca: true, - path_len_constraint: Some(_), - } => return Err(Error::CouldNotParseCertificate), - B { - ca: true, - path_len_constraint: None, - } => Self::Ca(BasicConstraints::Unconstrained), - B { ca: false, .. } => Self::ExplicitNoCa, - }) - } -} - -/// The path length constraint (only relevant for CA certificates) -/// -/// Sets an optional upper limit on the length of the intermediate certificate chain -/// length allowed for this CA certificate (not including the end entity certificate). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum BasicConstraints { - /// No constraint - Unconstrained, - /// Constrain to the contained number of intermediate certificates - Constrained(u8), -} - #[cfg(test)] mod tests { #[cfg(feature = "x509-parser")] @@ -710,8 +530,134 @@ mod tests { use super::*; #[cfg(feature = "x509-parser")] use crate::DnValue; + #[cfg(all(feature = "crypto", feature = "x509-parser"))] + use crate::GeneralSubtree; #[cfg(feature = "crypto")] - use crate::KeyPair; + use crate::{KeyPair, PathLenConstraint}; + + #[cfg(all(feature = "crypto", feature = "x509-parser"))] + #[test] + fn subject_extension_presence_matches_in_certificates_and_csrs() { + use x509_parser::extensions::ParsedExtension; + use x509_parser::prelude::FromDer; + + let (key, _) = KeyPair::generate().unwrap(); + for include_ku in [false, true] { + for include_eku in [false, true] { + let mut params = CertificateParams::new(vec!["example.com".into()]).unwrap(); + if include_ku { + params.key_usages.push(KeyUsagePurpose::DigitalSignature); + } + if include_eku { + params + .extended_key_usages + .push(ExtendedKeyUsagePurpose::ServerAuth); + } + let cert = params.self_signed(&key).unwrap(); + let (_, parsed) = x509_parser::parse_x509_certificate(cert.der()).unwrap(); + assert_eq!(parsed.key_usage().unwrap().is_some(), include_ku); + assert_eq!(parsed.extended_key_usage().unwrap().is_some(), include_eku); + assert!(parsed.subject_alternative_name().unwrap().is_some()); + + let csr = params.serialize_request(&key).unwrap(); + let (_, parsed) = + x509_parser::certification_request::X509CertificationRequest::from_der( + csr.der(), + ) + .unwrap(); + let extensions: Vec<_> = parsed.requested_extensions().unwrap().collect(); + // No malformed empty extension may hide behind a different parsed variant. + assert_eq!( + extensions.len(), + 1 + usize::from(include_ku) + usize::from(include_eku) + ); + assert_eq!( + extensions + .iter() + .any(|ext| matches!(ext, ParsedExtension::KeyUsage(_))), + include_ku + ); + assert_eq!( + extensions + .iter() + .any(|ext| matches!(ext, ParsedExtension::ExtendedKeyUsage(_))), + include_eku + ); + assert!(extensions + .iter() + .any(|ext| matches!(ext, ParsedExtension::SubjectAlternativeName(_)))); + } + } + } + + #[cfg(all(feature = "crypto", feature = "x509-parser"))] + #[test] + fn name_constraints_preserve_each_nonempty_subtree_list() { + let (key, _) = KeyPair::generate().unwrap(); + for include_permitted in [false, true] { + for include_excluded in [false, true] { + let permitted = if include_permitted { + vec![GeneralSubtree::DnsName("example.com".into())] + } else { + Vec::new() + }; + let excluded = if include_excluded { + vec![GeneralSubtree::DnsName("blocked.example.com".into())] + } else { + Vec::new() + }; + let constraints = if include_permitted || include_excluded { + Some(NameConstraints::new(permitted, excluded).unwrap()) + } else { + None + }; + let params = CertificateParams { + basic_constraints: Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)), + name_constraints: constraints, + ..CertificateParams::default() + }; + let cert = params.self_signed(&key).unwrap(); + let (_, parsed) = x509_parser::parse_x509_certificate(cert.der()).unwrap(); + let constraints = parsed.name_constraints().unwrap(); + if !include_permitted && !include_excluded { + assert!(constraints.is_none()); + continue; + } + let constraints = constraints.unwrap().value; + assert_eq!( + constraints.permitted_subtrees.as_ref().map(Vec::len), + include_permitted.then_some(1) + ); + assert_eq!( + constraints.excluded_subtrees.as_ref().map(Vec::len), + include_excluded.then_some(1) + ); + } + } + } + + #[cfg(all(feature = "crypto", feature = "x509-parser"))] + #[test] + fn basic_constraints_absent_for_no_ca() { + let params = CertificateParams::default(); + let (key, _) = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key).unwrap(); + let (_, parsed) = x509_parser::parse_x509_certificate(cert.der()).unwrap(); + assert!(parsed.basic_constraints().unwrap().is_none()); + let csr = params.serialize_request(&key).unwrap(); + use x509_parser::prelude::FromDer; + let (_, parsed) = + x509_parser::certification_request::X509CertificationRequest::from_der(csr.der()) + .unwrap(); + assert!(parsed + .requested_extensions() + .into_iter() + .flatten() + .all(|ext| !matches!( + ext, + x509_parser::extensions::ParsedExtension::BasicConstraints(_) + ))); + } #[cfg(feature = "crypto")] #[test] @@ -724,7 +670,7 @@ mod tests { KeyUsagePurpose::ContentCommitment, ], // This can sign things! - is_ca: IsCa::Ca(BasicConstraints::Constrained(0)), + basic_constraints: Some(BasicConstraints::Ca(PathLenConstraint::Constrained(0))), ..CertificateParams::default() }; @@ -762,7 +708,7 @@ mod tests { #[test] fn test_explicit_no_ca() { let params = CertificateParams { - is_ca: IsCa::ExplicitNoCa, + basic_constraints: Some(BasicConstraints::EndEntity), ..CertificateParams::default() }; @@ -794,20 +740,24 @@ mod tests { #[cfg(feature = "crypto")] #[test] - fn test_empty_crl_distribution_point_uris_rejected() { - let params = CertificateParams { - crl_distribution_points: vec![CrlDistributionPoint { uris: Vec::new() }], - ..CertificateParams::default() - }; - - // A distribution point with no URIs would be encoded as an empty - // fullName, violating GeneralNames ::= SEQUENCE SIZE (1..MAX) OF - // GeneralName (RFC 5280 §4.2.1.13), so it must be rejected. + fn test_end_entity_subject_key_identifier() { + // RFC 5280 §4.2.1.2 describes the SKI as a SHOULD for end entity + // certificates, so we expect it to be present for end entity certs too. + let params = CertificateParams::default(); let (key_pair, _) = KeyPair::generate().unwrap(); - assert_eq!( - params.self_signed(&key_pair).unwrap_err(), - Error::EmptyCrlDistributionPointUris - ); + let cert = params.self_signed(&key_pair).unwrap(); + + let (_rem, cert) = x509_parser::parse_x509_certificate(cert.der()).unwrap(); + let ski = cert + .iter_extensions() + .find_map(|ext| match ext.parsed_extension() { + x509_parser::extensions::ParsedExtension::SubjectKeyIdentifier(ski) => { + Some(ski.0.to_vec()) + }, + _ => None, + }) + .unwrap(); + assert_eq!(ski, params.key_identifier(&key_pair)); } #[cfg(feature = "crypto")] @@ -836,9 +786,10 @@ mod tests { // The CRL distribution points extension must be present even when it // is the only extension requested by the params. let params = CertificateParams { - crl_distribution_points: vec![CrlDistributionPoint { - uris: vec!["http://crl.example.com".to_string()], - }], + crl_distribution_points: vec![CrlDistributionPoint::new(vec![ + "http://crl.example.com".into(), + ]) + .unwrap()], ..CertificateParams::default() }; @@ -859,7 +810,7 @@ mod tests { // Set key usages key_usages: vec![KeyUsagePurpose::DecipherOnly], // This can sign things! - is_ca: IsCa::Ca(BasicConstraints::Constrained(0)), + basic_constraints: Some(BasicConstraints::Ca(PathLenConstraint::Constrained(0))), ..CertificateParams::default() }; @@ -1030,7 +981,7 @@ mod tests { params.subject_alt_names.push(ip_san.clone()); // Because we're using a function for CA certificates - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); // Serialize our cert that has our chosen san, so we can testing parsing/deserializing it. let cert = params.self_signed(&ca_key).unwrap(); diff --git a/rcgen/src/crl.rs b/rcgen/src/crl.rs index a75b468e..df90cc44 100644 --- a/rcgen/src/crl.rs +++ b/rcgen/src/crl.rs @@ -4,15 +4,13 @@ use pki_types::CertificateRevocationListDer; use time::OffsetDateTime; use yasna::{DERWriter, Tag}; -use crate::extension::{ - write_distribution_point_name_uris, AuthorityKeyIdentifier, CrlDistributionPoint, Extension, -}; +use crate::extension::{AuthorityKeyIdentifier, Criticality, Extensions, StaticExtension}; use crate::key_pair::sign_der; #[cfg(feature = "pem")] use crate::ENCODE_CONFIG; use crate::{ dt_to_generalized, oid, write_distinguished_name, write_dt_utc_or_generalized, - write_x509_extension, Error, Issuer, KeyIdMethod, KeyUsagePurpose, SerialNumber, SigningKey, + CrlDistributionPoint, Error, Issuer, KeyIdMethod, KeyUsagePurpose, SerialNumber, SigningKey, }; /// A certificate revocation list (CRL) @@ -38,7 +36,7 @@ use crate::{ /// // Generate a CRL issuer. /// let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]).unwrap(); /// issuer_params.serial_number = Some(SerialNumber::from(9999)); -/// issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); +/// issuer_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); /// issuer_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::CrlSign]; /// #[cfg(feature = "crypto")] /// let (key_pair, _) = KeyPair::generate().unwrap(); @@ -112,6 +110,31 @@ pub enum RevocationReason { AaCompromise = 10, } +impl StaticExtension for RevocationReason { + fn write_value(&self, writer: DERWriter) { + /* + CRLReason ::= ENUMERATED { + 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) } + */ + writer.write_enum(*self as i64); + } + + // RFC 5280 §5.3.1: "The reasonCode is a non-critical CRL entry extension". + const CRITICALITY: Criticality = Criticality::NonCritical; + + const OID: &'static [u64] = oid::CRL_REASONS; +} + /// Parameters used for certificate revocation list (CRL) generation #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq)] @@ -168,23 +191,13 @@ impl CertificateRevocationListParams { return Err(Error::IssuerNotCrlSigner); } - // An empty distribution point would be encoded as an empty fullName, - // violating GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName - // (RFC 5280 §4.2.1.13). - if self - .issuing_distribution_point - .as_ref() - .is_some_and(|idp| idp.distribution_point.uris.is_empty()) - { - return Err(Error::EmptyCrlDistributionPointUris); - } - Ok(CertificateRevocationList { der: self.serialize_der(issuer)?.into(), }) } fn serialize_der(&self, issuer: &Issuer<'_, impl SigningKey>) -> Result, Error> { + let extensions = self.extensions(issuer)?; sign_der(&issuer.signing_key, |writer| { // Write CRL version. // RFC 5280 §5.1.2.1: @@ -229,50 +242,68 @@ impl CertificateRevocationListParams { if !self.revoked_certs.is_empty() { writer.next().write_sequence(|writer| { for revoked_cert in &self.revoked_certs { - revoked_cert.write_der(writer.next()); + revoked_cert.write_der(writer.next())?; } - }); + Ok::<(), Error>(()) + })?; } // Write crlExtensions. // RFC 5280 §5.1.2.7: // This field may only appear if the version is 2 (Section 5.1.2.1). If // present, this field is a sequence of one or more CRL extensions. - // RFC 5280 §5.2: - // Conforming CRL issuers are REQUIRED to include the authority key - // identifier (Section 5.2.1) and the CRL number (Section 5.2.3) - // extensions in all CRLs issued. - writer.next().write_tagged(Tag::context(0), |writer| { - writer.write_sequence(|writer| { - // Write authority key identifier. - AuthorityKeyIdentifier( - self.key_identifier_method - .derive(issuer.signing_key.subject_public_key_info()), - ) - .write(writer.next()); - - // Write CRL number. - write_x509_extension(writer.next(), oid::CRL_NUMBER, false, |writer| { - writer.write_bigint_bytes(self.crl_number.as_ref(), true); - }); - - // Write issuing distribution point (if present). - if let Some(issuing_distribution_point) = &self.issuing_distribution_point { - write_x509_extension( - writer.next(), - oid::CRL_ISSUING_DISTRIBUTION_POINT, - true, - |writer| { - issuing_distribution_point.write_der(writer); - }, - ); - } - }); - }); + // The field is elided entirely when the built collection is empty. + extensions.write_crl_der(writer.next()); Ok(()) }) } + + /// Returns the X.509 extensions described by these CRL parameters. + /// + /// Returns an [`Error`] if the described extensions are invalid. + fn extensions(&self, issuer: &Issuer<'_, impl SigningKey>) -> Result, Error> { + let mut exts = Extensions::default(); + + // RFC 5280 §5.2: + // Conforming CRL issuers are REQUIRED to include the authority key + // identifier (Section 5.2.1) and the CRL number (Section 5.2.3) + // extensions in all CRLs issued. + exts.push(AuthorityKeyIdentifier( + self.key_identifier_method + .derive(issuer.signing_key.subject_public_key_info()), + ))?; + exts.push(CrlNumber::from(&self.crl_number))?; + + exts.push_if_some(self.issuing_distribution_point.as_ref())?; + + Ok(exts) + } +} + +/// An X.509v3 CRL number extension according to [RFC 5280 §5.2.3]. +/// +/// [RFC 5280 §5.2.3]: +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct CrlNumber<'params>(&'params SerialNumber); + +impl<'params> From<&'params SerialNumber> for CrlNumber<'params> { + fn from(number: &'params SerialNumber) -> Self { + Self(number) + } +} + +impl StaticExtension for CrlNumber<'_> { + fn write_value(&self, writer: DERWriter) { + // CRLNumber ::= INTEGER (0..MAX) + writer.write_bigint_bytes(self.0.as_ref(), true); + } + + // RFC 5280 §5.2.3: "CRL issuers conforming to this profile MUST include this + // extension in all CRLs and MUST mark this extension as non-critical." + const CRITICALITY: Criticality = Criticality::NonCritical; + + const OID: &'static [u64] = oid::CRL_NUMBER; } /// A certificate revocation list (CRL) issuing distribution point, to be included in a CRL's @@ -295,12 +326,16 @@ impl CrlIssuingDistributionPoint { scope: None, } } +} - fn write_der(&self, writer: DERWriter) { +// An X.509v3 issuing distribution point extension according to RFC 5280 §5.2.5 +// (). +impl StaticExtension for CrlIssuingDistributionPoint { + fn write_value(&self, writer: DERWriter) { // IssuingDistributionPoint SEQUENCE writer.write_sequence(|writer| { // distributionPoint [0] DistributionPointName OPTIONAL - write_distribution_point_name_uris(writer.next(), &self.distribution_point.uris); + self.distribution_point.write_name(writer.next()); // -- at most one of onlyContainsUserCerts, onlyContainsCACerts, // -- and onlyContainsAttributeCerts may be set to TRUE. @@ -317,6 +352,12 @@ impl CrlIssuingDistributionPoint { } }); } + + // RFC 5280 §5.2.5: "Although the extension is critical, conforming + // implementations are not required to support this extension." + const CRITICALITY: Criticality = Criticality::Critical; + + const OID: &'static [u64] = oid::CRL_ISSUING_DISTRIBUTION_POINT; } /// Describes the scope of a CRL for an issuing distribution point extension. @@ -329,6 +370,26 @@ pub enum CrlScope { CaCertsOnly, } +/// An X.509v3 CRL invalidity date entry extension according to [RFC 5280 §5.3.2]. +/// +/// [RFC 5280 §5.3.2]: +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct InvalidityDate(OffsetDateTime); + +impl StaticExtension for InvalidityDate { + fn write_value(&self, writer: DERWriter) { + // RFC 5280 §5.3.2: InvalidityDate ::= GeneralizedTime. Unlike the Time + // CHOICE used elsewhere, dates in the UTCTime range (1950-2049) must still + // be encoded as GeneralizedTime. + writer.write_generalized_time(&dt_to_generalized(self.0)); + } + + // RFC 5280 §5.3.2: "The invalidity date is a non-critical CRL entry extension". + const CRITICALITY: Criticality = Criticality::NonCritical; + + const OID: &'static [u64] = oid::CRL_INVALIDITY_DATE; +} + /// Parameters used for describing a revoked certificate included in a [`CertificateRevocationList`]. #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq)] @@ -356,7 +417,7 @@ impl RevokedCertParams { } } - fn write_der(&self, writer: DERWriter) { + fn write_der(&self, writer: DERWriter) -> Result<(), Error> { writer.write_sequence(|writer| { // Write serial number. // RFC 5280 §4.1.2.2: @@ -373,44 +434,22 @@ impl RevokedCertParams { // Write revocation date. write_dt_utc_or_generalized(writer.next(), self.revocation_time); - // Write extensions if applicable. + // Write crlEntryExtensions. // RFC 5280 §5.3: // Support for the CRL entry extensions defined in this specification is // optional for conforming CRL issuers and applications. However, CRL // issuers SHOULD include reason codes (Section 5.3.1) and invalidity // dates (Section 5.3.2) whenever this information is available. - // RFC 5280 §5.3.1: "The reason code CRL entry extension SHOULD be - // absent instead of using the unspecified (0) reasonCode value." - let reason_code = self - .reason_code - .filter(|reason| *reason != RevocationReason::Unspecified); - let has_invalidity_date = self.invalidity_date.is_some(); - if reason_code.is_some() || has_invalidity_date { - writer.next().write_sequence(|writer| { - // Write reason code if present. - if let Some(reason_code) = reason_code { - write_x509_extension(writer.next(), oid::CRL_REASONS, false, |writer| { - writer.write_enum(reason_code as i64); - }); - } + // The field is elided entirely when the built collection is empty. + let mut exts = Extensions::default(); + exts.push_if_some( + self.reason_code + .filter(|reason| *reason != RevocationReason::Unspecified), + )?; + exts.push_if_some(self.invalidity_date.map(InvalidityDate))?; + exts.write_der(writer.next()); - // Write invalidity date if present. - // RFC 5280 §5.3.2: InvalidityDate ::= GeneralizedTime. - // Unlike the Time CHOICE used elsewhere, dates in the - // UTCTime range (1950-2049) must still be encoded as - // GeneralizedTime. - if let Some(invalidity_date) = self.invalidity_date { - write_x509_extension( - writer.next(), - oid::CRL_INVALIDITY_DATE, - false, - |writer| { - writer.write_generalized_time(&dt_to_generalized(invalidity_date)); - }, - ) - } - }); - } + Ok(()) }) } } @@ -421,30 +460,7 @@ mod tests { use x509_parser::{oid_registry, parse_x509_crl}; use super::*; - use crate::{date_time_ymd, BasicConstraints, CertificateParams, IsCa, KeyPair}; - - #[test] - fn test_empty_issuing_distribution_point_uris_rejected() { - let crl = CertificateRevocationListParams { - this_update: date_time_ymd(2025, 5, 1), - next_update: date_time_ymd(2026, 5, 1), - crl_number: SerialNumber::from(1234u64), - issuing_distribution_point: Some(CrlIssuingDistributionPoint { - distribution_point: CrlDistributionPoint { uris: Vec::new() }, - scope: None, - }), - revoked_certs: Vec::new(), - key_identifier_method: KeyIdMethod::Sha256, - }; - - // A distribution point with no URIs would be encoded as an empty - // fullName, violating GeneralNames ::= SEQUENCE SIZE (1..MAX) OF - // GeneralName (RFC 5280 §4.2.1.13), so it must be rejected. - assert_eq!( - crl.signed_by(&test_issuer()).unwrap_err(), - Error::EmptyCrlDistributionPointUris - ); - } + use crate::{date_time_ymd, BasicConstraints, CertificateParams, KeyPair, PathLenConstraint}; #[test] fn test_unspecified_reason_code_not_written() { @@ -506,7 +522,8 @@ mod tests { let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]).unwrap(); issuer_params.serial_number = Some(SerialNumber::from(9999u64)); - issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + issuer_params.basic_constraints = + Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); issuer_params.key_usages = vec![ KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, diff --git a/rcgen/src/csr.rs b/rcgen/src/csr.rs index 33f1e8df..ad62f97a 100644 --- a/rcgen/src/csr.rs +++ b/rcgen/src/csr.rs @@ -6,11 +6,13 @@ use pki_types::CertificateSigningRequestDer; #[cfg(feature = "pem")] use crate::ENCODE_CONFIG; +#[cfg(feature = "x509-parser")] +use crate::{ + BasicConstraints, DistinguishedName, ExtendedKeyUsagePurpose, GeneralName, KeyUsagePurpose, +}; use crate::{ Certificate, CertificateParams, Error, Issuer, PublicKeyData, SignatureAlgorithm, SigningKey, }; -#[cfg(feature = "x509-parser")] -use crate::{DistinguishedName, ExtendedKeyUsagePurpose, GeneralName, IsCa, KeyUsagePurpose}; /// A public key, extracted from a CSR #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -96,7 +98,7 @@ impl CertificateSigningRequestParams { /// - `Subject Alternative Name` (see [`GeneralName`]) /// - `Key Usage` (see [`KeyUsagePurpose`]) /// - `Extended Key Usage` (see [`ExtendedKeyUsagePurpose`]) - /// - `Basic Constraints` (see [`crate::BasicConstraints`]) + /// - `Basic Constraints` (see [`crate::PathLenConstraint`]) /// /// On encountering other extensions, this function will return [`Error::UnsupportedExtension`]. /// If the request's signature is invalid, it will return @@ -175,7 +177,8 @@ impl CertificateSigningRequestParams { } }, x509_parser::extensions::ParsedExtension::BasicConstraints(bc) => { - params.is_ca = IsCa::from_basic_constraints(bc)?; + params.basic_constraints = + Some(BasicConstraints::from_basic_constraints(bc)?); }, _ => return Err(Error::UnsupportedExtension), } @@ -220,7 +223,7 @@ mod tests { use crate::{ BasicConstraints, CertificateParams, CertificateSigningRequestParams, - ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, + ExtendedKeyUsagePurpose, KeyPair, KeyUsagePurpose, PathLenConstraint, }; #[test] @@ -260,7 +263,7 @@ mod tests { use x509_parser::extensions::BasicConstraints as B; let params = CertificateParams { - is_ca: IsCa::ExplicitNoCa, + basic_constraints: Some(BasicConstraints::EndEntity), ..Default::default() }; let (key_pair, _) = KeyPair::generate().unwrap(); @@ -283,13 +286,13 @@ mod tests { #[test] fn serialize_and_deserialize_eq_basic_constraints() { let params = CertificateParams { - is_ca: IsCa::Ca(BasicConstraints::Constrained(10)), + basic_constraints: Some(BasicConstraints::Ca(PathLenConstraint::Constrained(10))), ..Default::default() }; let (key_pair, _) = KeyPair::generate().unwrap(); let csr = params.serialize_request(&key_pair).unwrap(); let csr_de = CertificateSigningRequestParams::from_der(csr.der()).unwrap(); - assert_eq!(csr_de.params.is_ca, params.is_ca); + assert_eq!(csr_de.params.basic_constraints, params.basic_constraints); } } diff --git a/rcgen/src/error.rs b/rcgen/src/error.rs index 9ba0b30e..2949178b 100644 --- a/rcgen/src/error.rs +++ b/rcgen/src/error.rs @@ -45,8 +45,12 @@ pub enum Error { InvalidCrlNextUpdate, /// CRL issuer specifies Key Usages that don't include cRLSign. IssuerNotCrlSigner, + /// Name constraints must contain at least one permitted or excluded subtree. + EmptyNameConstraints, /// A CRL distribution point was specified without any URIs. EmptyCrlDistributionPointUris, + /// Two extensions have the same OID. + DuplicateExtension(String), #[cfg(not(feature = "crypto"))] /// Missing serial number MissingSerialNumber, @@ -99,9 +103,15 @@ impl fmt::Display for Error { f, "CRL issuer must specify no key usage, or key usage including cRLSign" )?, + EmptyNameConstraints => { + write!(f, "Name constraints must include at least one subtree")? + }, EmptyCrlDistributionPointUris => { write!(f, "CRL distribution points must include at least one URI")? }, + DuplicateExtension(oid) => { + write!(f, "Extensions must have unique OIDs (duplicate {oid})")? + }, #[cfg(not(feature = "crypto"))] MissingSerialNumber => write!(f, "A serial number must be specified")?, #[cfg(feature = "x509-parser")] diff --git a/rcgen/src/extension.rs b/rcgen/src/extension.rs index 57ce4172..4fed172a 100644 --- a/rcgen/src/extension.rs +++ b/rcgen/src/extension.rs @@ -1,19 +1,19 @@ use std::fmt::Debug; +use std::iter; use std::net::IpAddr; #[cfg(feature = "x509-parser")] use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; use yasna::models::ObjectIdentifier; -use yasna::{DERWriter, Tag}; +use yasna::{DERWriter, DERWriterSet, Tag}; #[cfg(feature = "crypto")] use crate::ring_like::digest; use crate::string::Ia5String; -#[cfg(feature = "x509-parser")] -use crate::Error; use crate::{ - oid, write_distinguished_name, CertificateParams, DistinguishedName, Issuer, SigningKey, + oid, write_distinguished_name, CertificateParams, DistinguishedName, Error, InvalidAsn1String, + Issuer, SigningKey, }; /// An X.509v3 subject alternative name extension according to [RFC 5280 §4.2.1.6]. @@ -22,22 +22,20 @@ use crate::{ #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct SubjectAlternativeName<'params> { criticality: Criticality, - names: &'params [GeneralName], + names: NonEmptySlice<'params, GeneralName>, } impl<'params> SubjectAlternativeName<'params> { pub(crate) fn from_params(params: &'params CertificateParams) -> Option { - // GeneralNames ::= SEQUENCE SIZE (1..MAX): an empty SAN can't be encoded, - // so the extension is omitted (RFC 5280 §4.2.1.6). - if params.subject_alt_names.is_empty() { - return None; - } + // GeneralNames ::= SEQUENCE SIZE (1..MAX): omit an empty SAN rather than + // encode an invalid empty extension (RFC 5280 §4.2.1.6). + let names = NonEmptySlice::new(¶ms.subject_alt_names)?; Some(Self { // Per RFC 5280 §4.1.2.6, SAN must be marked critical if the subject // is an empty sequence, and SHOULD be non-critical otherwise. criticality: params.distinguished_name.entries.is_empty().into(), - names: ¶ms.subject_alt_names, + names, }) } @@ -216,23 +214,7 @@ fn ip_addr_from_octets(octets: &[u8]) -> Result { } } -/// An X.509v3 key usage extension according to [RFC 5280 §4.2.1.3]. -/// -/// [RFC 5280 §4.2.1.3]: -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct KeyUsage<'params>(&'params [KeyUsagePurpose]); - -impl<'params> KeyUsage<'params> { - pub(crate) fn from_params(params: &'params CertificateParams) -> Option { - if params.key_usages.is_empty() { - return None; - } - - Some(Self(¶ms.key_usages)) - } -} - -impl StaticExtension for KeyUsage<'_> { +impl StaticExtension for NonEmptySlice<'_, KeyUsagePurpose> { fn write_value(&self, writer: DERWriter) { /* KeyUsage ::= BIT STRING { @@ -248,7 +230,7 @@ impl StaticExtension for KeyUsage<'_> { decipherOnly (8) } */ // u16 is large enough to encode the largest possible key usage (two-bytes) - let bit_string = self.0.iter().fold(0u16, |bit_string, key_usage| { + let bit_string = self.iter().fold(0u16, |bit_string, key_usage| { bit_string | key_usage.to_u16() }); @@ -347,30 +329,14 @@ impl KeyUsagePurpose { } } -/// An X.509v3 extended key usage extension according to [RFC 5280 §4.2.1.12]. -/// -/// [RFC 5280 §4.2.1.12]: -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ExtendedKeyUsage<'params>(&'params [ExtendedKeyUsagePurpose]); - -impl<'params> ExtendedKeyUsage<'params> { - pub(crate) fn from_params(params: &'params CertificateParams) -> Option { - if params.extended_key_usages.is_empty() { - return None; - } - - Some(Self(¶ms.extended_key_usages)) - } -} - -impl StaticExtension for ExtendedKeyUsage<'_> { +impl StaticExtension for NonEmptySlice<'_, ExtendedKeyUsagePurpose> { fn write_value(&self, writer: DERWriter) { /* ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId KeyPurposeId ::= OBJECT IDENTIFIER */ writer.write_sequence(|writer| { - for usage in self.0.iter() { + for usage in self.iter() { writer .next() .write_oid(&ObjectIdentifier::from_slice(usage.oid())); @@ -463,28 +429,108 @@ impl ExtendedKeyUsagePurpose { } } -/// An X.509v3 name constraints extension according to [RFC 5280 §4.2.1.10]. +/// Whether a certificate with a basic constraints extension may sign certificates. /// -/// [RFC 5280 §4.2.1.10]: -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct NameConstraintsExt<'params> { - permitted_subtrees: &'params [GeneralSubtree], - excluded_subtrees: &'params [GeneralSubtree], +/// Use `None` in [`CertificateParams::basic_constraints`] to omit this extension. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BasicConstraints { + /// The certificate is not a CA; encode the default `cA:FALSE` value. + EndEntity, + /// The certificate is a CA, with the given path length constraint. + Ca(PathLenConstraint), } -impl<'params> NameConstraintsExt<'params> { - pub(crate) fn from_params(params: &'params CertificateParams) -> Option { - match ¶ms.name_constraints { - // If both subtrees are empty, the extension must be omitted. - Some(nc) if !nc.is_empty() => Some(Self { - permitted_subtrees: &nc.permitted_subtrees, - excluded_subtrees: &nc.excluded_subtrees, - }), - _ => None, +impl StaticExtension for BasicConstraints { + fn write_value(&self, writer: DERWriter) { + /* + BasicConstraints ::= SEQUENCE { + cA BOOLEAN DEFAULT FALSE, + pathLenConstraint INTEGER (0..MAX) OPTIONAL } + */ + writer.write_sequence(|writer| { + let Self::Ca(constraints) = self else { + // The cA flag is DEFAULT FALSE, so DER (X.690 §11.5) requires it + // to be omitted when false: the extension value is an empty + // SEQUENCE. + return; + }; + + writer.next().write_bool(true); // cA flag + if let PathLenConstraint::Constrained(path_len_constraint) = constraints { + writer.next().write_u8(*path_len_constraint); // pathLenConstraint integer + } + }); + } + + // RFC 5280 §4.2.1.9: "Conforming CAs MUST include this extension in all CA + // certificates that contain public keys used to validate digital signatures + // on certificates and MUST mark the extension as critical in such + // certificates." + const CRITICALITY: Criticality = Criticality::Critical; + + const OID: &'static [u64] = oid::BASIC_CONSTRAINTS; +} + +impl BasicConstraints { + #[cfg(all(test, feature = "x509-parser"))] + pub(crate) fn from_x509( + x509: &x509_parser::certificate::X509Certificate<'_>, + ) -> Result, Error> { + let basic_constraints = x509 + .basic_constraints() + .map_err(|_| Error::CouldNotParseCertificate)? + .map(|ext| ext.value); + + match basic_constraints { + Some(bc) => Self::from_basic_constraints(bc).map(Some), + None => Ok(None), } } - fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[GeneralSubtree]) { + #[cfg(feature = "x509-parser")] + pub(crate) fn from_basic_constraints( + basic_constraints: &x509_parser::extensions::BasicConstraints, + ) -> Result { + use x509_parser::extensions::BasicConstraints as B; + + Ok(match basic_constraints { + B { + ca: true, + path_len_constraint: Some(n), + } if *n <= u8::MAX as u32 => Self::Ca(PathLenConstraint::Constrained(*n as u8)), + B { + ca: true, + path_len_constraint: Some(_), + } => return Err(Error::CouldNotParseCertificate), + B { + ca: true, + path_len_constraint: None, + } => Self::Ca(PathLenConstraint::Unconstrained), + B { ca: false, .. } => Self::EndEntity, + }) + } +} + +/// The path length constraint (only relevant for CA certificates) +/// +/// Sets an optional upper limit on the length of the intermediate certificate chain +/// length allowed for this CA certificate (not including the end entity certificate). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PathLenConstraint { + /// No constraint + Unconstrained, + /// Constrain to the contained number of intermediate certificates + Constrained(u8), +} + +impl NameConstraints { + fn write_general_subtrees( + writer: DERWriter, + tag: u64, + general_subtrees: &NonEmptySlice<'_, GeneralSubtree>, + ) { /* GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree GeneralSubtree ::= SEQUENCE { @@ -521,19 +567,20 @@ impl<'params> NameConstraintsExt<'params> { } } -impl StaticExtension for NameConstraintsExt<'_> { +impl StaticExtension for NameConstraints { fn write_value(&self, writer: DERWriter) { /* NameConstraints ::= SEQUENCE { permittedSubtrees [0] GeneralSubtrees OPTIONAL, excludedSubtrees [1] GeneralSubtrees OPTIONAL } */ + writer.write_sequence(|writer| { - if !self.permitted_subtrees.is_empty() { - Self::write_general_subtrees(writer.next(), 0, self.permitted_subtrees); + if let Some(permitted) = NonEmptySlice::new(&self.permitted_subtrees) { + NameConstraints::write_general_subtrees(writer.next(), 0, &permitted); } - if !self.excluded_subtrees.is_empty() { - Self::write_general_subtrees(writer.next(), 1, self.excluded_subtrees); + if let Some(excluded) = NonEmptySlice::new(&self.excluded_subtrees) { + NameConstraints::write_general_subtrees(writer.next(), 1, &excluded); } }); } @@ -545,19 +592,79 @@ impl StaticExtension for NameConstraintsExt<'_> { } /// The [NameConstraints extension](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) -/// (only relevant for CA certificates) -#[allow(clippy::exhaustive_structs)] +/// (only relevant for CA certificates). +/// +/// At least one permitted or excluded subtree is required. Use +/// [`CertificateParams::name_constraints`] set to `None` to omit the extension. #[derive(Debug, PartialEq, Eq, Clone)] pub struct NameConstraints { /// A list of subtrees that the domain has to match. - pub permitted_subtrees: Vec, + permitted_subtrees: Vec, /// A list of subtrees that the domain must not match. /// /// Any name matching an excluded subtree is invalid even if it also matches a permitted subtree. - pub excluded_subtrees: Vec, + excluded_subtrees: Vec, } impl NameConstraints { + /// Construct name constraints with permitted and excluded subtrees. + /// + /// Returns an error if both lists are empty or a DNS or email name is not ASCII. + pub fn new( + permitted_subtrees: Vec, + excluded_subtrees: Vec, + ) -> Result { + if permitted_subtrees.is_empty() && excluded_subtrees.is_empty() { + return Err(Error::EmptyNameConstraints); + } + for subtree in permitted_subtrees.iter().chain(&excluded_subtrees) { + Self::validate_subtree(subtree)?; + } + Ok(Self { + permitted_subtrees, + excluded_subtrees, + }) + } + + /// Return the permitted subtrees. + pub fn permitted_subtrees(&self) -> &[GeneralSubtree] { + &self.permitted_subtrees + } + + /// Return the excluded subtrees. + /// + /// An excluded subtree takes precedence over a matching permitted subtree. + pub fn excluded_subtrees(&self) -> &[GeneralSubtree] { + &self.excluded_subtrees + } + + /// Add a permitted subtree, rejecting DNS or email names that are not ASCII. + pub fn push_permitted_subtree(&mut self, subtree: GeneralSubtree) -> Result<(), Error> { + Self::validate_subtree(&subtree)?; + self.permitted_subtrees.push(subtree); + Ok(()) + } + + /// Add an excluded subtree, rejecting DNS or email names that are not ASCII. + pub fn push_excluded_subtree(&mut self, subtree: GeneralSubtree) -> Result<(), Error> { + Self::validate_subtree(&subtree)?; + self.excluded_subtrees.push(subtree); + Ok(()) + } + + fn validate_subtree(subtree: &GeneralSubtree) -> Result<(), Error> { + match subtree { + GeneralSubtree::DnsName(name) | GeneralSubtree::Rfc822Name(name) + if !name.is_ascii() => + { + Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String( + name.clone(), + ))) + }, + _ => Ok(()), + } + } + #[cfg(all(test, feature = "x509-parser"))] pub(crate) fn from_x509( x509: &x509_parser::certificate::X509Certificate<'_>, @@ -583,14 +690,7 @@ impl NameConstraints { Vec::new() }; - Ok(Some(Self { - permitted_subtrees, - excluded_subtrees, - })) - } - - pub(crate) fn is_empty(&self) -> bool { - self.permitted_subtrees.is_empty() && self.excluded_subtrees.is_empty() + Self::new(permitted_subtrees, excluded_subtrees).map(Some) } } @@ -757,28 +857,14 @@ impl FromStr for CidrSubnet { } } -/// An X.509v3 CRL distribution points extension according to [RFC 5280 §4.2.1.13]. -/// -/// [RFC 5280 §4.2.1.13]: -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct CrlDistributionPoints<'params>(&'params [CrlDistributionPoint]); - -impl<'params> CrlDistributionPoints<'params> { - pub(crate) fn from_params(params: &'params CertificateParams) -> Option { - if params.crl_distribution_points.is_empty() { - return None; - } - - Some(Self(¶ms.crl_distribution_points)) - } -} - -impl StaticExtension for CrlDistributionPoints<'_> { +impl StaticExtension for NonEmptySlice<'_, CrlDistributionPoint> { fn write_value(&self, writer: DERWriter) { // CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint writer.write_sequence(|writer| { - for distribution_point in self.0 { - distribution_point.write_der(writer.next()); + for point in self.iter() { + writer + .next() + .write_sequence(|writer| point.write_name(writer.next())); } }) } @@ -791,54 +877,76 @@ impl StaticExtension for CrlDistributionPoints<'_> { /// A certificate revocation list (CRL) distribution point, to be included in a certificate's /// [distribution points extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13) or -/// a CRL's [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5) +/// a CRL's [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5). +/// +/// A distribution point always contains at least one ASCII URI name. #[non_exhaustive] #[derive(Debug, PartialEq, Eq, Clone)] pub struct CrlDistributionPoint { /// One or more URI distribution point names, indicating a place the current CRL can /// be retrieved. When present, SHOULD include at least one LDAP or HTTP URI. - pub uris: Vec, + uris: Vec, } impl CrlDistributionPoint { - /// Construct a new `CrlDistributionPoint` with the given URIs. - pub fn new(uris: Vec) -> Self { - Self { uris } + /// Construct a distribution point containing one or more URI names. + /// + /// Returns an error if the list is empty or a name is not ASCII. + pub fn new(uris: Vec) -> Result { + if uris.is_empty() { + return Err(Error::EmptyCrlDistributionPointUris); + } + for uri in &uris { + Self::validate_uri(uri)?; + } + Ok(Self { uris }) } - fn write_der(&self, writer: DERWriter) { - // DistributionPoint SEQUENCE - writer.write_sequence(|writer| { - write_distribution_point_name_uris(writer.next(), &self.uris); - }); + /// Add a URI name, rejecting names that are not ASCII. + pub fn push_uri(&mut self, uri: impl Into) -> Result<(), Error> { + let uri = uri.into(); + Self::validate_uri(&uri)?; + self.uris.push(uri); + Ok(()) } -} -pub(crate) fn write_distribution_point_name_uris<'a>( - writer: DERWriter, - uris: impl IntoIterator, -) { - // distributionPoint DistributionPointName - writer.write_tagged_implicit(Tag::context(0), |writer| { - writer.write_sequence(|writer| { - // fullName GeneralNames - writer - .next() - .write_tagged_implicit(Tag::context(0), |writer| { - // GeneralNames - writer.write_sequence(|writer| { - for uri in uris.into_iter() { - // uniformResourceIdentifier [6] IA5String, - writer - .next() - .write_tagged_implicit(Tag::context(6), |writer| { - writer.write_ia5_string(uri) - }); - } - }) - }); + /// Return the URI names identifying this distribution point. + pub fn uris(&self) -> &[String] { + &self.uris + } + + pub(crate) fn write_name(&self, writer: DERWriter) { + // distributionPoint DistributionPointName + writer.write_tagged_implicit(Tag::context(0), |writer| { + writer.write_sequence(|writer| { + // fullName GeneralNames + writer + .next() + .write_tagged_implicit(Tag::context(0), |writer| { + // GeneralNames + writer.write_sequence(|writer| { + for uri in &self.uris { + // uniformResourceIdentifier [6] IA5String, + writer + .next() + .write_tagged_implicit(Tag::context(6), |writer| { + writer.write_ia5_string(uri) + }); + } + }) + }); + }); }); - }); + } + + fn validate_uri(uri: &str) -> Result<(), Error> { + if uri.is_ascii() { + return Ok(()); + } + Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String( + uri.into(), + ))) + } } /// An X.509v3 subject key identifier extension according to [RFC 5280 §4.2.1.2]. @@ -1000,6 +1108,189 @@ impl KeyIdMethod { } } +/// A custom extension of a certificate, as specified in +/// [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2) +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[non_exhaustive] +pub struct CustomExtension { + /// OID identifying the extension. + /// + /// Only one extension with a given OID may appear within a certificate. + pub oid: Vec, + + /// Criticality of the extension. + /// + /// See [`Criticality`] for more information. + pub criticality: Criticality, + + /// The raw DER encoded value of the extension. + /// + /// This should not contain the OID, criticality, OCTET STRING, or the outer + /// extension SEQUENCE of the extension itself: it should only be the DER encoded + /// bytes that will be found within the extension's OCTET STRING value. + pub der_value: Vec, +} + +impl CustomExtension { + /// Create a custom extension with the given OID, criticality, and DER encoded value. + /// + /// `der_value` contains only the extension's value, as found inside its + /// extnValue OCTET STRING. It must not include the OID, criticality, OCTET + /// STRING wrapper, or outer extension SEQUENCE. + pub fn new(oid: &[u64], criticality: Criticality, der_value: Vec) -> Self { + Self { + oid: oid.to_vec(), + criticality, + der_value, + } + } + + /// Creates a new acmeIdentifier extension for ACME TLS-ALPN-01 + /// as specified in [RFC 8737](https://tools.ietf.org/html/rfc8737#section-3) + /// + /// Panics if the passed `sha_digest` parameter doesn't hold 32 bytes (256 bits). + pub fn new_acme_identifier(sha_digest: &[u8]) -> Self { + assert_eq!(sha_digest.len(), 32, "wrong size of sha_digest"); + let der_value = yasna::construct_der(|writer| { + writer.write_bytes(sha_digest); + }); + Self { + oid: oid::PE_ACME.to_owned(), + criticality: Criticality::Critical, + der_value, + } + } + + /// Obtains the OID components of the extensions, as u64 pieces + pub fn oid_components(&self) -> impl Iterator + '_ { + self.oid.iter().copied() + } +} + +impl Extension for &CustomExtension { + fn write_value(&self, writer: DERWriter) { + writer.write_der(&self.der_value) + } + + fn criticality(&self) -> Criticality { + self.criticality + } + + fn oid(&self) -> &[u64] { + &self.oid + } +} + +/// A collection of X.509 extensions. +/// +/// Preserves the order that extensions were added and maintains the invariant that +/// there are no duplicate extension OIDs. The extensions borrow from the params +/// they were built from for the duration of one serialization. +#[derive(Debug, Default)] +pub(crate) struct Extensions<'params> { + exts: Vec>, +} + +impl<'params> Extensions<'params> { + /// Add a prepared extension when its parameters request a value. + pub(crate) fn push_if_some( + &mut self, + extension: Option, + ) -> Result<(), Error> { + if let Some(extension) = extension { + self.push(extension)?; + } + Ok(()) + } + + /// Add an extension to the collection. + /// + /// Returns [`Error::DuplicateExtension`] if the extension's OID is already present + /// in the collection. + pub(crate) fn push(&mut self, extension: impl Extension + 'params) -> Result<(), Error> { + let oid = extension.oid(); + if self.exts.iter().any(|existing| existing.oid() == oid) { + return Err(Error::DuplicateExtension( + ObjectIdentifier::from_slice(oid).to_string(), + )); + } + + self.exts.push(Box::new(extension)); + Ok(()) + } + + /// Write the certificate's optional extensions field. + /// + /// Nothing is written when the collection is empty: presence is decided by the + /// built collection, not predicted from the params, so an empty extensions + /// field is never emitted and requested extensions can never be silently + /// dropped. + pub(crate) fn write_cert_der(&self, writer: DERWriter) { + if self.exts.is_empty() { + return; + } + + writer.write_tagged(Tag::context(3), |writer| self.write_der(writer)); + } + + /// Write the PKCS #9 extensionRequest attribute for a CSR into the + /// attributes SET, containing the collection as its single `Extensions` + /// value. + /// + /// Nothing is written when the collection is empty: attribute values are a + /// SET SIZE(1..MAX), so an empty extension request can't be encoded and the + /// attribute is elided entirely. + pub(crate) fn write_csr_attribute(&self, writer: &mut DERWriterSet<'_>) { + if self.exts.is_empty() { + return; + } + + /* + Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { + type ATTRIBUTE.&id({IOSet}), + values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) + } + ExtensionRequest ::= Extensions + */ + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice( + oid::PKCS_9_AT_EXTENSION_REQUEST, + )); + writer.next().write_set(|writer| { + self.write_der(writer.next()); + }); + }); + } + + /// Write the `crlExtensions [0] EXPLICIT Extensions OPTIONAL` field of a CRL. + /// + /// Nothing is written when the collection is empty. + pub(crate) fn write_crl_der(&self, writer: DERWriter) { + if self.exts.is_empty() { + return; + } + + writer.write_tagged(Tag::context(0), |writer| self.write_der(writer)); + } + + /// Write `Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension`. + /// + /// For example, the untagged `crlEntryExtensions` field of a CRL entry. + /// + /// Nothing is written when the collection is empty. + pub(crate) fn write_der(&self, writer: DERWriter) { + if self.exts.is_empty() { + return; + } + + writer.write_sequence(|writer| { + for extension in &self.exts { + extension.write(writer.next()); + } + }) + } +} + impl Extension for T { fn write_value(&self, writer: DERWriter) { // Calling with fully qualified syntax to disambiguate. @@ -1032,6 +1323,16 @@ pub(crate) trait StaticExtension: Debug { const OID: &'static [u64]; } +// Reuse the same encoder for borrowed prepared extensions. +impl StaticExtension for &T { + fn write_value(&self, writer: DERWriter) { + T::write_value(self, writer); + } + + const CRITICALITY: Criticality = T::CRITICALITY; + const OID: &'static [u64] = T::OID; +} + /// An X.509 extension. /// /// All extensions have an OID, a criticality, and a DER encoded value for inclusion in @@ -1081,8 +1382,9 @@ pub(crate) trait Extension: Debug { /// See [RFC 5280 §4.2] for more information. /// /// [RFC 5280 §4.2]: -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum Criticality { +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[expect(clippy::exhaustive_enums, reason = "X.509 criticality is a boolean")] +pub enum Criticality { /// The extension MUST be recognized and parsed correctly. Critical, @@ -1099,17 +1401,214 @@ impl From for Criticality { } } +/// A borrowed sequence containing at least one item. +/// +/// Some X.509 extensions require nonempty sequences. This type enforces that +/// requirement before their values can be encoded. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct NonEmptySlice<'a, T> { + first: &'a T, + rest: &'a [T], +} + +impl<'a, T> NonEmptySlice<'a, T> { + pub(crate) fn new(items: &'a [T]) -> Option { + items + .split_first() + .map(|(first, rest)| Self { first, rest }) + } + + pub(crate) fn iter(&self) -> impl Iterator { + iter::once(self.first).chain(self.rest.iter()) + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn name_constraints_reject_empty_and_non_ascii_names() { + assert_eq!( + NameConstraints::new(Vec::new(), Vec::new()).unwrap_err(), + Error::EmptyNameConstraints + ); + for subtree in [ + GeneralSubtree::DnsName("é.example.com".into()), + GeneralSubtree::Rfc822Name("é@example.com".into()), + ] { + assert!(matches!( + NameConstraints::new(vec![subtree.clone()], Vec::new()), + Err(Error::InvalidAsn1String(_)) + )); + assert!(matches!( + NameConstraints::new(Vec::new(), vec![subtree]), + Err(Error::InvalidAsn1String(_)) + )); + } + } + + #[test] + fn name_constraints_mutations_preserve_valid_state() { + let mut constraints = NameConstraints::new( + vec![GeneralSubtree::DnsName("example.com".into())], + Vec::new(), + ) + .unwrap(); + let original = constraints.clone(); + assert!(constraints + .push_permitted_subtree(GeneralSubtree::DnsName("é.example.com".into())) + .is_err()); + assert!(constraints + .push_excluded_subtree(GeneralSubtree::Rfc822Name("é@example.com".into())) + .is_err()); + assert_eq!(constraints, original); + constraints + .push_excluded_subtree(GeneralSubtree::DnsName("blocked.example.com".into())) + .unwrap(); + assert_eq!( + constraints.permitted_subtrees(), + original.permitted_subtrees() + ); + assert_eq!( + constraints.excluded_subtrees(), + &[GeneralSubtree::DnsName("blocked.example.com".into())] + ); + } + + #[test] + fn distribution_points_reject_invalid_names_before_encoding() { + assert_eq!( + CrlDistributionPoint::new(Vec::new()).unwrap_err(), + Error::EmptyCrlDistributionPointUris + ); + assert!(matches!( + CrlDistributionPoint::new(vec![ + "http://example.com/crl".into(), + "http://é.example.com/crl".into() + ]), + Err(Error::InvalidAsn1String(_)) + )); + let mut point = CrlDistributionPoint::new(vec!["http://example.com/crl".into()]).unwrap(); + let original = point.clone(); + assert!(point.push_uri("http://é.example.com/crl").is_err()); + assert_eq!(point, original); + point.push_uri("ldap://example.com/crl").unwrap(); + assert_eq!( + point.uris(), + &["http://example.com/crl", "ldap://example.com/crl"] + ); + } + + #[test] + fn extensions_elided_when_prepared_values_are_absent() { + let mut exts = Extensions::default(); + exts.push_if_some(NonEmptySlice::::new(&[])) + .unwrap(); + exts.push_if_some(NonEmptySlice::::new(&[])) + .unwrap(); + exts.push_if_some(None::).unwrap(); + exts.push_if_some(None::<&NameConstraints>).unwrap(); + exts.push_if_some(NonEmptySlice::::new(&[])) + .unwrap(); + let der = yasna::construct_der(|writer| exts.write_der(writer)); + assert!(der.is_empty()); + } + + #[test] + fn extensions_reject_duplicate_oids() { + let mut exts = Extensions::default(); + exts.push(DummyExt { + oid: TEST_OID, + criticality: Criticality::NonCritical, + }) + .unwrap(); + assert_eq!( + exts.push(DummyExt { + oid: TEST_OID, + criticality: Criticality::Critical, + }), + Err(Error::DuplicateExtension( + ObjectIdentifier::from_slice(TEST_OID).to_string() + )), + ); + } + + #[test] + fn extensions_preserve_insertion_order() { + let mut exts = Extensions::default(); + // Add an extension with a lexicographically larger OID first: the encoded + // SEQUENCE must preserve insertion order, not sort. + exts.push(DummyExt { + oid: &[1, 3, 6, 1, 4, 1, 98], + criticality: Criticality::NonCritical, + }) + .unwrap(); + exts.push(DummyExt { + oid: &[1, 3, 6, 1, 4, 1, 97], + criticality: Criticality::NonCritical, + }) + .unwrap(); + + let der = yasna::construct_der(|writer| exts.write_cert_der(writer)); + assert_eq!( + der, + yasna::construct_der(|writer| { + writer.write_tagged(Tag::context(3), |writer| { + writer.write_sequence(|writer| { + // Insertion order, not OID order: 98 first, then 97. + for oid in [&[1, 3, 6, 1, 4, 1, 98], &[1, 3, 6, 1, 4, 1, 97]] { + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(oid)); + writer.next().write_bytes(&yasna::construct_der(|writer| { + writer.write_null() + })); + }); + } + }) + }) + }) + ); + } + + #[test] + fn extensions_elided_when_empty() { + // An empty collection writes nothing at all: no extensions field, no + // empty SEQUENCE. + let exts = Extensions::default(); + let der = yasna::construct_der(|writer| { + writer.write_sequence(|writer| exts.write_cert_der(writer.next())) + }); + assert_eq!( + der, + yasna::construct_der(|writer| writer.write_sequence(|_writer| {})) + ); + } + + #[test] + fn csr_attribute_elided_when_empty() { + // An empty collection must not claim a slot in the attributes SET at + // all: yasna rejects set elements that produce no output. + let exts = Extensions::default(); + let der = yasna::construct_der(|writer| { + writer.write_set_of(|writer| exts.write_csr_attribute(writer)) + }); + assert_eq!( + der, + yasna::construct_der(|writer| writer.write_set_of(|_writer| {})) + ); + } + #[test] fn critical_flag_omitted_when_false() { // The critical flag is DEFAULT FALSE, so DER (X.690 §11.5) requires that a // non-critical extension omit it entirely rather than encode FALSE. // See https://github.com/rustls/rcgen/pull/444 for a past instance of this // bug class. - let ext = DummyExt(Criticality::NonCritical); + let ext = DummyExt { + oid: TEST_OID, + criticality: Criticality::NonCritical, + }; let der = yasna::construct_der(|writer| ext.write(writer)); assert_eq!( der, @@ -1130,7 +1629,10 @@ mod tests { #[test] fn critical_flag_written_when_true() { - let ext = DummyExt(Criticality::Critical); + let ext = DummyExt { + oid: TEST_OID, + criticality: Criticality::Critical, + }; let der = yasna::construct_der(|writer| ext.write(writer)); assert_eq!( der, @@ -1176,17 +1678,40 @@ mod tests { } #[test] - fn name_constraints_absent_when_subtrees_empty() { - // A name constraints extension with no permitted or excluded subtrees - // would violate SEQUENCE SIZE (1..MAX) and must be omitted. - let params = CertificateParams { - name_constraints: Some(crate::NameConstraints { - permitted_subtrees: Vec::new(), - excluded_subtrees: Vec::new(), - }), - ..CertificateParams::default() - }; - assert!(NameConstraintsExt::from_params(¶ms).is_none()); + fn basic_constraints_encoding() { + // The cA flag is DEFAULT FALSE, so DER (X.690 §11.5) requires that + // ExplicitNoCa encode as an empty SEQUENCE with the flag omitted. + // See https://github.com/rustls/rcgen/pull/444. + for (basic_constraints, expected) in [ + ( + // cA absent (FALSE): an empty SEQUENCE. + BasicConstraints::EndEntity, + yasna::construct_der(|writer| writer.write_sequence(|_writer| {})), + ), + ( + BasicConstraints::Ca(PathLenConstraint::Unconstrained), + yasna::construct_der(|writer| { + writer.write_sequence(|writer| writer.next().write_bool(true)) + }), + ), + ( + BasicConstraints::Ca(PathLenConstraint::Constrained(5)), + yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + writer.next().write_bool(true); + writer.next().write_u8(5); + }) + }), + ), + ] { + let value = yasna::construct_der(|writer| { + StaticExtension::write_value(&basic_constraints, writer) + }); + assert_eq!( + value, expected, + "unexpected encoding for {basic_constraints:?}" + ); + } } #[test] @@ -1269,7 +1794,10 @@ mod tests { } #[derive(Debug)] - struct DummyExt(Criticality); + struct DummyExt { + oid: &'static [u64], + criticality: Criticality, + } impl Extension for DummyExt { fn write_value(&self, writer: DERWriter) { @@ -1277,11 +1805,13 @@ mod tests { } fn criticality(&self) -> Criticality { - self.0 + self.criticality } fn oid(&self) -> &[u64] { - &[1, 3, 6, 1, 4, 1, 99] + self.oid } } + + const TEST_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 99]; } diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 77978434..5407e9f3 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -44,15 +44,12 @@ use pki_types::CertificateDer; #[cfg(feature = "crypto")] use pki_types::PrivateKeyDer; use time::{OffsetDateTime, Time}; -use yasna::models::{GeneralizedTime, ObjectIdentifier, UTCTime}; +use yasna::models::{GeneralizedTime, UTCTime}; use yasna::tags::{TAG_BMPSTRING, TAG_TELETEXSTRING, TAG_UNIVERSALSTRING}; use yasna::DERWriter; mod certificate; -pub use certificate::{ - date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, CustomExtension, - DnType, IsCa, -}; +pub use certificate::{date_time_ymd, Attribute, Certificate, CertificateParams, DnType}; mod crl; pub use crl::{ @@ -68,8 +65,9 @@ pub use error::{Error, InvalidAsn1String}; mod extension; pub use extension::{ - CidrSubnet, CrlDistributionPoint, ExtendedKeyUsagePurpose, GeneralName, GeneralSubtree, - KeyIdMethod, KeyUsagePurpose, NameConstraints, OtherNameValue, + BasicConstraints, CidrSubnet, Criticality, CrlDistributionPoint, CustomExtension, + ExtendedKeyUsagePurpose, GeneralName, GeneralSubtree, KeyIdMethod, KeyUsagePurpose, + NameConstraints, OtherNameValue, PathLenConstraint, }; mod key_pair; @@ -537,34 +535,6 @@ fn write_distinguished_name(writer: DERWriter, dn: &DistinguishedName) { }); } -/// Serializes an X.509v3 extension according to RFC 5280 -fn write_x509_extension( - writer: DERWriter, - extension_oid: &[u64], - is_critical: bool, - value_serializer: impl FnOnce(DERWriter), -) { - // Extension specification: - // Extension ::= SEQUENCE { - // extnID OBJECT IDENTIFIER, - // critical BOOLEAN DEFAULT FALSE, - // extnValue OCTET STRING - // -- contains the DER encoding of an ASN.1 value - // -- corresponding to the extension type identified - // -- by extnID - // } - - writer.write_sequence(|writer| { - let oid = ObjectIdentifier::from_slice(extension_oid); - writer.next().write_oid(&oid); - if is_critical { - writer.next().write_bool(true); - } - let bytes = yasna::construct_der(value_serializer); - writer.next().write_bytes(&bytes); - }) -} - /// A certificate serial number. #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct SerialNumber { diff --git a/rustls-cert-gen/src/cert.rs b/rustls-cert-gen/src/cert.rs index 115e10c1..1161eacf 100644 --- a/rustls-cert-gen/src/cert.rs +++ b/rustls-cert-gen/src/cert.rs @@ -8,8 +8,8 @@ use pki_types::PrivateKeyDer; use rcgen::DnValue::PrintableString; use rcgen::{ serialize_private_key_pem, BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, - DistinguishedName, DnType, Error, ExtendedKeyUsagePurpose, GeneralName, IsCa, KeyPair, - KeyUsagePurpose, SignatureAlgorithm, + DistinguishedName, DnType, Error, ExtendedKeyUsagePurpose, GeneralName, KeyPair, + KeyUsagePurpose, PathLenConstraint, SignatureAlgorithm, }; /// Builder to configure TLS [CertificateParams] to be finalized @@ -66,7 +66,7 @@ pub struct CaBuilder { impl CaBuilder { /// Initialize `CaBuilder` pub fn new(mut params: CertificateParams, alg: KeyPairAlgorithm) -> Self { - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); params.key_usages.push(KeyUsagePurpose::DigitalSignature); params.key_usages.push(KeyUsagePurpose::KeyCertSign); params.key_usages.push(KeyUsagePurpose::CrlSign); @@ -148,7 +148,7 @@ pub struct EndEntityBuilder { impl EndEntityBuilder { /// Initialize `EndEntityBuilder` pub fn new(mut params: CertificateParams, alg: KeyPairAlgorithm) -> Self { - params.is_ca = IsCa::NoCa; + params.basic_constraints = None; params.use_authority_key_identifier_extension = true; params.key_usages.push(KeyUsagePurpose::DigitalSignature); Self { params, alg } @@ -324,7 +324,10 @@ mod tests { #[test] fn init_ca() { let cert = CertificateBuilder::new().certificate_authority(); - assert_eq!(cert.params.is_ca, IsCa::Ca(BasicConstraints::Unconstrained)) + assert_eq!( + cert.params.basic_constraints, + Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)) + ) } #[test] fn with_sig_algo_default() -> anyhow::Result<()> { @@ -419,7 +422,7 @@ mod tests { fn init_end_endity() { let params = CertificateParams::default(); let cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); - assert_eq!(cert.params.is_ca, IsCa::NoCa) + assert_eq!(cert.params.basic_constraints, None) } #[test] fn client_auth_end_entity() { @@ -429,7 +432,7 @@ mod tests { .unwrap(); let params = CertificateParams::default(); let mut cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); - assert_eq!(cert.params.is_ca, IsCa::NoCa); + assert_eq!(cert.params.basic_constraints, None); assert_eq!( cert.client_auth().params.extended_key_usages, vec![ExtendedKeyUsagePurpose::ClientAuth] @@ -443,7 +446,7 @@ mod tests { .unwrap(); let params = CertificateParams::default(); let mut cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); - assert_eq!(cert.params.is_ca, IsCa::NoCa); + assert_eq!(cert.params.basic_constraints, None); assert_eq!( cert.server_auth().params.extended_key_usages, vec![ExtendedKeyUsagePurpose::ServerAuth] diff --git a/verify-tests/src/lib.rs b/verify-tests/src/lib.rs index 1df99ac5..e05d5b67 100644 --- a/verify-tests/src/lib.rs +++ b/verify-tests/src/lib.rs @@ -2,7 +2,7 @@ use pki_types::PrivateKeyDer; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint, CrlIssuingDistributionPoint, CrlScope, - DnType, IsCa, Issuer, KeyIdMethod, KeyPair, KeyUsagePurpose, RevocationReason, + DnType, Issuer, KeyIdMethod, KeyPair, KeyUsagePurpose, PathLenConstraint, RevocationReason, RevokedCertParams, SerialNumber, }; use time::{Duration, OffsetDateTime}; @@ -83,7 +83,7 @@ pub fn test_crl() -> ( Certificate, ) { let (mut issuer, key_pair, _) = default_params(); - issuer.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + issuer.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); issuer.key_usages = vec![ KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, @@ -98,9 +98,9 @@ pub fn test_crl() -> ( RevokedCertParams::new(SerialNumber::from_slice(&[0x00, 0xC0, 0xFF, 0xEE]), now); revoked_cert.reason_code = Some(RevocationReason::KeyCompromise); - let mut dp = CrlIssuingDistributionPoint::new(CrlDistributionPoint::new(vec![ - "http://example.com/crl".to_string(), - ])); + let mut dp = CrlIssuingDistributionPoint::new( + CrlDistributionPoint::new(vec!["http://example.com/crl".to_string()]).unwrap(), + ); dp.scope = Some(CrlScope::UserCertsOnly); let mut params = CertificateRevocationListParams::new( @@ -123,8 +123,9 @@ pub fn cert_with_crl_dps() -> Vec { CrlDistributionPoint::new(vec![ "http://example.com/crl.der".to_string(), "http://crls.example.com/1234".to_string(), - ]), - CrlDistributionPoint::new(vec!["ldap://example.com/crl.der".to_string()]), + ]) + .unwrap(), + CrlDistributionPoint::new(vec!["ldap://example.com/crl.der".to_string()]).unwrap(), ]; params.self_signed(&key_pair).unwrap().der().to_vec() diff --git a/verify-tests/tests/botan.rs b/verify-tests/tests/botan.rs index c6d04b0d..ed887d62 100644 --- a/verify-tests/tests/botan.rs +++ b/verify-tests/tests/botan.rs @@ -2,8 +2,8 @@ use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateRevocationListParams, DnType, - DnValue, IsCa, Issuer, KeyPair, KeyUsagePurpose, RevocationReason, RevokedCertParams, - SerialNumber, + DnValue, Issuer, KeyPair, KeyUsagePurpose, PathLenConstraint, RevocationReason, + RevokedCertParams, SerialNumber, }; use time::{Duration, OffsetDateTime}; use verify_tests as util; @@ -128,7 +128,7 @@ fn test_botan_rsa_given() { #[test] fn test_botan_separate_ca() { let (mut ca_params, ca_key) = default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); @@ -151,7 +151,7 @@ fn test_botan_separate_ca() { #[test] fn test_botan_imported_ca() { let (mut params, ca_key) = default_params(); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = params.self_signed(&ca_key).unwrap(); let ca_cert_der = ca_cert.der(); let ca = Issuer::from_ca_cert_der(ca_cert.der(), ca_key).unwrap(); @@ -179,7 +179,7 @@ fn test_botan_imported_ca_with_printable_string() { DnType::CountryName, DnValue::PrintableString("US".try_into().unwrap()), ); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = params.self_signed(&imported_ca_key).unwrap(); let ca = Issuer::from_ca_cert_der(ca_cert.der(), imported_ca_key).unwrap(); @@ -203,7 +203,7 @@ fn test_botan_crl_parse() { // Create an issuer CA. let alg = &rcgen::ECDSA_P256_SHA256; let (mut issuer, _, _) = util::default_params(); - issuer.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + issuer.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); issuer.key_usages = vec![ KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, @@ -214,7 +214,7 @@ fn test_botan_crl_parse() { // Create an end entity cert issued by the issuer. let (mut ee, _, _) = util::default_params(); - ee.is_ca = IsCa::NoCa; + ee.basic_constraints = None; ee.serial_number = Some(SerialNumber::from(99999)); // Botan has a sanity check that enforces a maximum expiration date ee.not_after = rcgen::date_time_ymd(3016, 1, 1); diff --git a/verify-tests/tests/generic.rs b/verify-tests/tests/generic.rs index e4ae1fc9..a821219e 100644 --- a/verify-tests/tests/generic.rs +++ b/verify-tests/tests/generic.rs @@ -49,7 +49,7 @@ mod test_key_params_mismatch { #[cfg(feature = "x509-parser")] mod test_x509_custom_ext { - use rcgen::CustomExtension; + use rcgen::{Criticality, CustomExtension}; use verify_tests as util; use x509_parser::oid_registry::asn1_rs; use x509_parser::prelude::{ @@ -63,11 +63,11 @@ mod test_x509_custom_ext { let test_ext = yasna::construct_der(|writer| { writer.write_utf8_string("🦀 greetz to ferris 🦀"); }); - let mut custom_ext = CustomExtension::from_oid_content( + let custom_ext = CustomExtension::new( test_oid.iter().unwrap().collect::>().as_slice(), + Criticality::Critical, test_ext.clone(), ); - custom_ext.set_criticality(true); // Generate a certificate with the custom extension, parse it with x509-parser. let (mut params, test_key, _) = util::default_params(); @@ -172,7 +172,7 @@ mod test_csr_custom_attributes { #[cfg(feature = "x509-parser")] mod test_csr_basic_constraints { - use rcgen::{BasicConstraints, CertificateSigningRequestParams, Error, IsCa}; + use rcgen::{BasicConstraints, CertificateSigningRequestParams, Error, PathLenConstraint}; /// Tests deserializing a csr with a basic constraint of CA:TRUE,pathlen:5 /// @@ -184,8 +184,8 @@ mod test_csr_basic_constraints { .unwrap(); assert_eq!( - csr_params.params.is_ca, - IsCa::Ca(BasicConstraints::Constrained(5)) + csr_params.params.basic_constraints, + Some(BasicConstraints::Ca(PathLenConstraint::Constrained(5))) ); } @@ -257,8 +257,8 @@ RioOvAyCH6bFMvSJxZm7FYM= CertificateSigningRequestParams::from_pem(CSR_TEST_BASIC_CONSTRAINTS_CA_TRUE).unwrap(); assert_eq!( - csr_params.params.is_ca, - IsCa::Ca(BasicConstraints::Unconstrained) + csr_params.params.basic_constraints, + Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)) ); } @@ -294,7 +294,10 @@ lZLnFMmv1pkn052qtQ== let csr_params = CertificateSigningRequestParams::from_pem(CSR_TEST_BASIC_CONSTRAINTS_CA_FALSE).unwrap(); - assert_eq!(csr_params.params.is_ca, IsCa::ExplicitNoCa); + assert_eq!( + csr_params.params.basic_constraints, + Some(BasicConstraints::EndEntity) + ); } /* diff --git a/verify-tests/tests/openssl.rs b/verify-tests/tests/openssl.rs index 03aa6719..db03541a 100644 --- a/verify-tests/tests/openssl.rs +++ b/verify-tests/tests/openssl.rs @@ -15,7 +15,7 @@ use pki_types::pem::PemObject; use pki_types::PrivateKeyDer; use rcgen::{ BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, DnValue, - GeneralSubtree, IsCa, Issuer, KeyPair, NameConstraints, + GeneralSubtree, Issuer, KeyPair, NameConstraints, PathLenConstraint, }; use verify_tests as util; @@ -311,7 +311,7 @@ fn test_openssl_rsa_combinations_given() { #[test] fn test_openssl_separate_ca() { let (mut ca_params, ca_key, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let ca_cert_pem = ca_cert.pem(); let ca = Issuer::new(ca_params, ca_key); @@ -336,7 +336,7 @@ fn test_openssl_separate_ca_with_printable_string() { DnType::CountryName, DnValue::PrintableString("US".try_into().unwrap()), ); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); @@ -356,7 +356,7 @@ fn test_openssl_separate_ca_with_printable_string() { #[test] fn test_openssl_separate_ca_with_other_signing_alg() { let (mut ca_params, _, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let (ca_key, _) = KeyPair::generate_for(&rcgen::ECDSA_P256_SHA256).unwrap(); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let ca = Issuer::new(ca_params, ca_key); @@ -377,17 +377,17 @@ fn test_openssl_separate_ca_with_other_signing_alg() { #[test] fn test_openssl_separate_ca_name_constraints() { let (mut ca_params, ca_key, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); println!("openssl version: {:x}", openssl::version::number()); - ca_params.name_constraints = Some(NameConstraints { - permitted_subtrees: vec![GeneralSubtree::DnsName("crabs.crabs".to_string())], - //permitted_subtrees : vec![GeneralSubtree::DnsName("".to_string())], - //permitted_subtrees : Vec::new(), - //excluded_subtrees : vec![GeneralSubtree::DnsName(".v".to_string())], - excluded_subtrees: Vec::new(), - }); + ca_params.name_constraints = Some( + NameConstraints::new( + vec![GeneralSubtree::DnsName("crabs.crabs".to_string())], + Vec::new(), + ) + .unwrap(), + ); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let ca = Issuer::new(ca_params, ca_key); @@ -407,18 +407,21 @@ fn test_openssl_separate_ca_name_constraints() { #[test] fn test_openssl_separate_ca_name_constraints_directory_name() { let (mut ca_params, ca_key, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let mut permitted = DistinguishedName::new(); permitted.push(DnType::OrganizationName, "Crab widgits SE"); - ca_params.name_constraints = Some(NameConstraints { - permitted_subtrees: vec![ - GeneralSubtree::DnsName("crabs.crabs".to_string()), - GeneralSubtree::DirectoryName(permitted), - ], - excluded_subtrees: Vec::new(), - }); + ca_params.name_constraints = Some( + NameConstraints::new( + vec![ + GeneralSubtree::DnsName("crabs.crabs".to_string()), + GeneralSubtree::DirectoryName(permitted), + ], + Vec::new(), + ) + .unwrap(), + ); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let ca = Issuer::new(ca_params, ca_key); diff --git a/verify-tests/tests/webpki.rs b/verify-tests/tests/webpki.rs index 4ccfadc7..03a9c942 100644 --- a/verify-tests/tests/webpki.rs +++ b/verify-tests/tests/webpki.rs @@ -11,8 +11,9 @@ use pki_types::{ }; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateRevocationListParams, DnType, - Error, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyIdMethod, KeyPair, KeyUsagePurpose, - PublicKeyData, RevocationReason, RevokedCertParams, SerialNumber, SigningKey, + Error, ExtendedKeyUsagePurpose, Issuer, KeyIdMethod, KeyPair, KeyUsagePurpose, + PathLenConstraint, PublicKeyData, RevocationReason, RevokedCertParams, SerialNumber, + SigningKey, }; #[cfg(feature = "x509-parser")] use rcgen::{CertificateSigningRequestParams, DnValue}; @@ -328,7 +329,7 @@ fn test_webpki_rsa_combinations_given() { #[test] fn test_webpki_separate_ca() { let (mut ca_params, ca_key, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); @@ -356,7 +357,7 @@ fn test_webpki_separate_ca() { #[test] fn test_webpki_separate_ca_with_other_signing_alg() { let (mut ca_params, _, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let (ca_key, _) = KeyPair::generate_for(&rcgen::ECDSA_P256_SHA256).unwrap(); let ca_cert = ca_params.self_signed(&ca_key).unwrap(); @@ -445,17 +446,18 @@ fn from_remote() { #[test] fn test_webpki_separate_ca_name_constraints() { let mut params = util::default_params(); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - params.name_constraints = Some(NameConstraints { - // TODO also add a test with non-empty permitted_subtrees that - // doesn't contain a DirectoryName entry. This isn't possible - // currently due to a limitation of webpki. - permitted_subtrees : vec![GeneralSubtree::DnsName("dev".to_string()), GeneralSubtree::DirectoryName(rcgen::DistinguishedName::new())], - //permitted_subtrees : vec![GeneralSubtree::DnsName("dev".to_string())], - //permitted_subtrees : Vec::new(), - //excluded_subtrees : vec![GeneralSubtree::DnsName("v".to_string())], - excluded_subtrees : Vec::new(), - }); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); + // TODO also test permitted subtrees without a DirectoryName once webpki supports it. + params.name_constraints = Some( + NameConstraints::new( + vec![ + GeneralSubtree::DnsName("dev".to_string()), + GeneralSubtree::DirectoryName(rcgen::DistinguishedName::new()), + ], + Vec::new(), + ) + .unwrap(), + ); let ca_cert = Certificate::from_params(params).unwrap(); println!("{}", ca_cert.serialize_pem().unwrap()); @@ -481,7 +483,7 @@ fn test_webpki_separate_ca_name_constraints() { #[test] fn test_webpki_imported_ca() { let (mut params, ca_key, _) = util::default_params(); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); params.key_usages.push(KeyUsagePurpose::KeyCertSign); let ca_cert = params.self_signed(&ca_key).unwrap(); @@ -517,7 +519,7 @@ fn test_webpki_imported_ca_with_printable_string() { DnType::CountryName, DnValue::PrintableString("US".try_into().unwrap()), ); - params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); let ca_cert = params.self_signed(&ca_key).unwrap(); let ca = Issuer::from_ca_cert_der(ca_cert.der(), ca_key).unwrap(); @@ -576,7 +578,7 @@ fn test_certificate_from_csr() { } let (mut ca_params, ca_key, _) = util::default_params(); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); for eku in &eku_test { ca_params.insert_extended_key_usage(eku.clone()); } @@ -671,7 +673,7 @@ fn test_webpki_crl_revoke() { // Create an issuer CA. let alg = &rcgen::ECDSA_P256_SHA256; let (mut issuer, _, _) = util::default_params(); - issuer.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + issuer.basic_constraints = Some(BasicConstraints::Ca(PathLenConstraint::Unconstrained)); issuer.key_usages = vec![ KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, @@ -682,7 +684,7 @@ fn test_webpki_crl_revoke() { // Create an end entity cert issued by the issuer. let (mut ee, _, _) = util::default_params(); - ee.is_ca = IsCa::NoCa; + ee.basic_constraints = None; ee.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; ee.serial_number = Some(SerialNumber::from(99999)); let (ee_key, _) = KeyPair::generate_for(alg).unwrap();