diff --git a/rcgen/src/crl.rs b/rcgen/src/crl.rs index 75a89bcd..e06f45b6 100644 --- a/rcgen/src/crl.rs +++ b/rcgen/src/crl.rs @@ -4,13 +4,10 @@ use time::OffsetDateTime; use yasna::DERWriter; use yasna::Tag; -use crate::oid::*; +use crate::ext::Extensions; #[cfg(feature = "pem")] use crate::ENCODE_CONFIG; -use crate::{ - write_distinguished_name, write_dt_utc_or_generalized, write_x509_authority_key_identifier, - write_x509_extension, -}; +use crate::{ext, write_distinguished_name, write_dt_utc_or_generalized}; use crate::{Certificate, Error, KeyIdMethod, KeyUsagePurpose, SerialNumber, SignatureAlgorithm}; /// A certificate revocation list (CRL) @@ -245,41 +242,42 @@ impl CertificateRevocationListParams { // 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. - write_x509_authority_key_identifier(writer.next(), ca); - - // 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); - }, - ); - } - }); - }); + self.extensions(ca).write_crl_der(writer.next()); Ok(()) }) } + /// Returns the X.509 extensions that the [CertificateRevocationListParams] describe. + /// + /// If an issuer [Certificate] is provided, additional extensions specific to the issuer will + /// be included (e.g. the authority key identifier). + fn extensions(&self, issuer: &Certificate) -> Extensions { + 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. + // Safety: `exts` is empty at this point - there can be no duplicate AKI ext OID. + exts.add_extension(Box::new(ext::AuthorityKeyIdentifier::from(issuer))) + .unwrap(); + + // Safety: there can be no duplicate CRL number ext OID by this point. + exts.add_extension(Box::new(ext::CrlNumber::from_params(&self))) + .unwrap(); + + if let Some(idp_ext) = ext::IssuingDistributionPoint::from_params(&self) { + // Safety: there can be no duplicate IDP ext OID by this point. + exts.add_extension(Box::new(idp_ext)).unwrap(); + } + + exts + } } /// A certificate revocation list (CRL) issuing distribution point, to be included in a CRL's /// [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5). +#[derive(Debug, Clone, Eq, PartialEq)] pub struct CrlIssuingDistributionPoint { /// The CRL's distribution point, containing a sequence of URIs the CRL can be retrieved from. pub distribution_point: CrlDistributionPoint, @@ -289,7 +287,7 @@ pub struct CrlIssuingDistributionPoint { } impl CrlIssuingDistributionPoint { - fn write_der(&self, writer: DERWriter) { + pub(crate) fn write_der(&self, writer: DERWriter) { // IssuingDistributionPoint SEQUENCE writer.write_sequence(|writer| { // distributionPoint [0] DistributionPointName OPTIONAL @@ -359,31 +357,23 @@ impl RevokedCertParams { // 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. - let has_reason_code = - matches!(self.reason_code, Some(reason) if reason != RevocationReason::Unspecified); - let has_invalidity_date = self.invalidity_date.is_some(); - if has_reason_code || has_invalidity_date { - writer.next().write_sequence(|writer| { - // Write reason code if present. - self.reason_code.map(|reason_code| { - write_x509_extension(writer.next(), OID_CRL_REASONS, false, |writer| { - writer.write_enum(reason_code as i64); - }); - }); - - // Write invalidity date if present. - self.invalidity_date.map(|invalidity_date| { - write_x509_extension( - writer.next(), - OID_CRL_INVALIDITY_DATE, - false, - |writer| { - write_dt_utc_or_generalized(writer, invalidity_date); - }, - ) - }); - }); - } + self.extensions().write_der(writer.next()); }) } + /// Returns the X.509 extensions that the [RevokedCertParams] describe. + fn extensions(&self) -> Extensions { + let mut exts = Extensions::default(); + + if let Some(reason_code_ext) = ext::ReasonCode::from_params(&self) { + // Safety: there can be no duplicate reason code ext OID by this point. + exts.add_extension(Box::new(reason_code_ext)).unwrap(); + } + + if let Some(invalidity_date_ext) = ext::InvalidityDate::from_params(&self) { + // Safety: there can be no duplicate invalidity date ext OID by this point. + exts.add_extension(Box::new(invalidity_date_ext)).unwrap(); + } + + exts + } } diff --git a/rcgen/src/csr.rs b/rcgen/src/csr.rs index e0d9eb6f..2ba32ae0 100644 --- a/rcgen/src/csr.rs +++ b/rcgen/src/csr.rs @@ -1,5 +1,5 @@ #[cfg(feature = "x509-parser")] -use crate::{DistinguishedName, SanType}; +use crate::{ext, DistinguishedName}; #[cfg(feature = "pem")] use pem::Pem; use std::hash::Hash; @@ -45,6 +45,7 @@ impl CertificateSigningRequest { /// /// Currently, this only supports the `Subject Alternative Name` extension. /// On encountering other extensions, this function will return an error. + // TODO(@cpu): update this doc comment. #[cfg(feature = "x509-parser")] pub fn from_der(csr: &[u8]) -> Result { use x509_parser::prelude::FromDer; @@ -66,27 +67,60 @@ impl CertificateSigningRequest { params.distinguished_name = DistinguishedName::from_name(&info.subject)?; let raw = info.subject_pki.subject_public_key.data.to_vec(); - if let Some(extensions) = csr.requested_extensions() { - for ext in extensions { - match ext { - x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) => { - for name in &san.general_names { - params - .subject_alt_names - .push(SanType::try_from_general(name)?); - } - }, - _ => return Err(Error::UnsupportedExtension), + // Pull out the extension requests attributes from the CSR. + // Note: we avoid using csr.requested_extensions() here because it maps to the parsed + // extension value and we want the raw extension value to handle unknown extensions + // ourselves. + let requested_exts = csr + .certification_request_info + .iter_attributes() + .filter_map(|attr| { + if let x509_parser::prelude::ParsedCriAttribute::ExtensionRequest(requested) = + &attr.parsed_attribute() + { + Some(requested.extensions.iter().collect::>()) + } else { + None } + }) + .flatten() + .collect::>(); + + for ext in requested_exts { + use x509_parser::extensions::ParsedExtension; + + let supported = match ext.parsed_extension() { + ext @ ParsedExtension::SubjectAlternativeName(_) => { + ext::SubjectAlternativeName::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::KeyUsage(_) => ext::KeyUsage::from_parsed(&mut params, ext)?, + ext @ ParsedExtension::ExtendedKeyUsage(_) => { + ext::ExtendedKeyUsage::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::NameConstraints(_) => { + ext::NameConstraints::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::CRLDistributionPoints(_) => { + ext::CrlDistributionPoints::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::SubjectKeyIdentifier(_) => { + ext::SubjectKeyIdentifier::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::BasicConstraints(_) => { + ext::BasicConstraints::from_parsed(&mut params, ext)? + }, + ParsedExtension::AuthorityKeyIdentifier(_) => { + true // We always handle emitting this ourselves - don't copy it as a custom extension. + }, + _ => false, + }; + if !supported { + params + .custom_extensions + .push(ext::CustomExtension::from_parsed(ext)?); } } - // Not yet handled: - // * is_ca - // * extended_key_usages - // * name_constraints - // and any other extensions. - Ok(Self { params, public_key: PublicKey { alg, raw }, diff --git a/rcgen/src/error.rs b/rcgen/src/error.rs index acffe1be..9535b530 100644 --- a/rcgen/src/error.rs +++ b/rcgen/src/error.rs @@ -4,45 +4,58 @@ use std::fmt; #[non_exhaustive] /// The error type of the rcgen crate pub enum Error { + /// The provided certificate's signature algorithm + /// is incompatible with the given key pair + CertificateKeyPairMismatch, /// The given certificate couldn't be parsed CouldNotParseCertificate, /// The given certificate signing request couldn't be parsed CouldNotParseCertificationRequest, /// The given key pair couldn't be parsed CouldNotParseKeyPair, + /// Duplicate extension OID + DuplicateExtension(String), + /// Invalid ACME identifier extension digest length. + InvalidAcmeIdentifierLength, + /// Invalid certificate revocation list (CRL) next update. + InvalidCrlNextUpdate, + /// An IP address was provided as a byte array, but the byte array was an invalid length. + InvalidIpAddressOctetLength(usize), #[cfg(feature = "x509-parser")] /// Invalid subject alternative name type InvalidNameType, - /// An IP address was provided as a byte array, but the byte array was an invalid length. - InvalidIpAddressOctetLength(usize), + /// CRL issuer specifies Key Usages that don't include cRLSign. + IssuerNotCrlSigner, /// There is no support for generating /// keys for the given algorithm KeyGenerationUnavailable, - #[cfg(feature = "x509-parser")] - /// Unsupported extension requested in CSR - UnsupportedExtension, - /// The requested signature algorithm is not supported - UnsupportedSignatureAlgorithm, - /// Unspecified `ring` error - RingUnspecified, - /// The `ring` library rejected the key upon loading - RingKeyRejected(String), - /// The provided certificate's signature algorithm - /// is incompatible with the given key pair - CertificateKeyPairMismatch, - /// Time conversion related errors - Time, #[cfg(feature = "pem")] /// Error from the pem crate PemError(String), /// Error generated by a remote key operation RemoteKeyError, + /// The `ring` library rejected the key upon loading + RingKeyRejected(String), + /// Unspecified `ring` error + RingUnspecified, + /// Time conversion related errors + Time, + /// Unsupported basic constraints extension path length in CSR + #[cfg(feature = "x509-parser")] + UnsupportedBasicConstraintsPathLen, + /// Unsupported CRL distribution point extension in CSR + #[cfg(feature = "x509-parser")] + UnsupportedCrlDistributionPoint, + #[cfg(feature = "x509-parser")] + /// Unsupported extension requested in CSR + UnsupportedExtension, + /// Unsupported general name type in CSR + #[cfg(feature = "x509-parser")] + UnsupportedGeneralName, /// Unsupported field when generating a CSR UnsupportedInCsr, - /// Invalid certificate revocation list (CRL) next update. - InvalidCrlNextUpdate, - /// CRL issuer specifies Key Usages that don't include cRLSign. - IssuerNotCrlSigner, + /// The requested signature algorithm is not supported + UnsupportedSignatureAlgorithm, } impl fmt::Display for Error { @@ -91,6 +104,22 @@ impl fmt::Display for Error { f, "CRL issuer must specify no key usage, or key usage including cRLSign" )?, + DuplicateExtension(oid) => { + write!(f, "Extension with OID {oid} present multiple times")? + }, + #[cfg(feature = "x509-parser")] + UnsupportedGeneralName => write!(f, "Unsupported general name in CSR",)?, + #[cfg(feature = "x509-parser")] + UnsupportedCrlDistributionPoint => write!(f, "Unsupported CRL distribution point in CSR",)?, + #[cfg(feature = "x509-parser")] + UnsupportedBasicConstraintsPathLen => write!( + f, + "Unsupported basic constraints extension path length constraint in CSR" + )?, + InvalidAcmeIdentifierLength => write!( + f, + "Invalid ACME identifier extension digest length. Must be 32 bytes.", + )?, }; Ok(()) } diff --git a/rcgen/src/ext.rs b/rcgen/src/ext.rs new file mode 100644 index 00000000..ddbf0f84 --- /dev/null +++ b/rcgen/src/ext.rs @@ -0,0 +1,1662 @@ +use std::collections::HashSet; +use std::fmt::Debug; +use std::net::IpAddr; +use time::OffsetDateTime; + +use yasna::models::ObjectIdentifier; +use yasna::{DERWriter, DERWriterSeq, Tag}; + +use crate::key_pair::PublicKeyData; +use crate::oid::{OID_PE_ACME, OID_PKCS_9_AT_EXTENSION_REQUEST}; +use crate::{ + crl, oid, write_distinguished_name, write_dt_utc_or_generalized, Certificate, + CertificateParams, CertificateRevocationListParams, CrlIssuingDistributionPoint, Error, + ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, KeyUsagePurpose, RevokedCertParams, SanType, + SerialNumber, +}; + +/// The criticality of an extension. +/// +/// This controls how a certificate-using system should handle an unrecognized or un-parsable +/// extension. +/// +/// See [RFC 5280 Section 4.2] for more information. +/// +/// [RFC 5280 Section 4.2]: +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Criticality { + /// The extension MUST be recognized and parsed correctly. + /// + /// A certificate-using system MUST reject the certificate if it encounters a critical + /// extension it does not recognize or a critical extension that contains information that it + /// cannot process. + Critical, + + /// The extension MAY be ignored if it is not recognized or parsed correctly. + /// + /// A non-critical extension MAY be ignored if it is not recognized, but MUST be + /// processed if it is recognized + NonCritical, +} + +/// A custom extension of a certificate, as specified in +/// [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2) +#[derive(Debug, Clone, PartialEq, Eq)] +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 extensions' OCTET STRING value. + pub der_value: Vec, +} + +impl CustomExtension { + /// Create a new custom extension with the specified content + pub fn from_oid_content(oid: &[u64], criticality: Criticality, der_value: Vec) -> Self { + Self { + oid: oid.to_vec(), + criticality, + der_value, + } + } + + /// Obtains the OID components of the extensions, as u64 pieces + pub fn oid_components(&self) -> impl Iterator + '_ { + self.oid.iter().copied() + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + parsed: &x509_parser::prelude::X509Extension<'_>, + ) -> Result { + Ok(CustomExtension { + oid: parsed + .oid + .iter() + .ok_or(Error::UnsupportedExtension)? + .collect::>(), + criticality: if parsed.critical { + Criticality::Critical + } else { + Criticality::NonCritical + }, + der_value: parsed.value.to_vec(), + }) + } +} + +impl Extension for CustomExtension { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(&self.oid) + } + + fn criticality(&self) -> Criticality { + self.criticality + } + + fn write_value(&self, writer: DERWriter) { + writer.write_der(&self.der_value) + } +} + +/// An ACME TLS-ALPN-01 challenge response certificate extension. +/// +/// See [RFC 8737 Section 3] for more information. +/// +/// [RFC 8737 Section 3]: +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AcmeIdentifier { + // The SHA256 digest of the RFC 8555 key authorization for a TLS-ALPN-01 challenge + // issued by the CA. + key_auth_digest: [u8; 32], +} + +impl AcmeIdentifier { + /// Construct an ACME TLS-ALPN-01 challenge response certificate extension. + /// + /// `key_auth_digest` should be the SHA-256 digest of the key authorization for the + /// TLS-ALPN-01 challenge issued by the CA. + /// + /// If you have a `Vec` or `&[u8]` use `try_from` and handle the potential error + /// if the input length is not 32 bytes. + pub fn new(key_auth_digest: [u8; 32]) -> Self { + Self { + key_auth_digest: key_auth_digest, + } + } +} + +impl TryFrom<&[u8]> for AcmeIdentifier { + type Error = Error; + + fn try_from(key_auth_digest: &[u8]) -> Result { + // All TLS-ALPN-01 challenge response digests are 32 bytes long, + // matching the output of the SHA256 digest algorithm. + if key_auth_digest.len() != 32 { + return Err(Error::InvalidAcmeIdentifierLength); + } + + let mut sha_digest = [0u8; 32]; + sha_digest.copy_from_slice(&key_auth_digest); + Ok(Self { + key_auth_digest: sha_digest, + }) + } +} + +impl Extension for AcmeIdentifier { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(OID_PE_ACME) + } + + fn criticality(&self) -> Criticality { + // The acmeIdentifier extension MUST be critical so that the certificate isn't inadvertently + // used by non-ACME software. + Criticality::Critical + } + + fn write_value(&self, writer: DERWriter) { + // Authorization ::= OCTET STRING (SIZE (32)) + writer.write_bytes(&self.key_auth_digest); + } +} + +/// A trait describing an X.509 Extension. +/// +/// All extensions have an OID, an indicator of whether they are critical or not, and can be +/// encoded to a DER value for inclusion in an X.509 certificate extension SEQUENCE. +pub(crate) trait Extension: Debug { + /// Return the OID of the extension. + fn oid(&self) -> ObjectIdentifier; + + /// Return the criticality of the extension. + fn criticality(&self) -> Criticality; + + /// Write the extension's value to the DER writer. + fn write_value(&self, writer: DERWriter); +} + +/// A collection of X.509 extensions. +/// +/// Preserves the order that extensions were added and maintains the invariant that +/// there are no duplicate extension OIDs. +#[derive(Debug, Default)] +pub(crate) struct Extensions { + exts: Vec>, + oids: HashSet, +} + +impl Extensions { + /// Construct a set of extensions from an iterator of extensions. + /// + /// # Errors + /// + /// Returns [Error::DuplicateExtension] if any of the extensions have the same OID. + #[cfg(test)] + pub(crate) fn new( + extensions: impl IntoIterator>, + ) -> Result { + let mut result = Self::default(); + result.add_extensions(extensions)?; + Ok(result) + } + + /// Add an extension to the collection. + /// + /// # Errors + /// + /// Returns [Error::DuplicateExtension] if the extension's OID is already present in the collection. + pub(crate) fn add_extension(&mut self, extension: Box) -> Result<(), Error> { + if self.oids.get(&extension.oid()).is_some() { + return Err(Error::DuplicateExtension(extension.oid().to_string())); + } + + self.oids.insert(extension.oid()); + self.exts.push(extension); + Ok(()) + } + + pub(crate) fn add_extensions( + &mut self, + extensions: impl IntoIterator>, + ) -> Result<(), Error> { + for ext in extensions { + self.add_extension(ext)? + } + Ok(()) + } + + /// Write the SEQUENCE of extensions to the DER writer, wrapped in the context tag for + /// an optional X.509 V3 certificate extensions field. + /// + /// Nothing will be written to the writer if there were no extensions. + pub(crate) fn write_exts_der(&self, writer: DERWriter) { + // Avoid writing an empty tagged extensions sequence. + if self.exts.is_empty() { + return; + } + + // extensions [3] Extensions OPTIONAL + writer.write_tagged(Tag::context(3), |writer| self.write_der(writer)); + } + + /// Write the SEQUENCE of extensions to the DER writer, wrapped in the PKCS 9 attribute + /// extension request OID and set for a CSR. + /// + /// Nothing will be written to the writer if there were no extensions. + pub(crate) fn write_csr_der(&self, writer: DERWriter) { + // Avoid writing an empty attribute requests sequence. + if self.exts.is_empty() { + return; + } + + writer.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()); + }); + }); + } + + pub(crate) fn write_crl_der(&self, writer: DERWriter) { + // Avoid writing an empty tagged extensions sequence. + if self.exts.is_empty() { + return; + } + + // crlExtensions [0] Extensions OPTIONAL + writer.write_tagged(Tag::context(0), |writer| self.write_der(writer)); + } + + /// Write the SEQUENCE of extensions to the DER writer as a raw SEQUENCE. + /// Usually you will want to use `write_exts_der`, `write_csr_der` or `write_crl_der` instead. + pub(crate) fn write_der(&self, writer: DERWriter) { + debug_assert_eq!(self.exts.len(), self.oids.len()); + + // Avoid writing an empty extension SEQUENCE. + if self.exts.is_empty() { + return; + } + + // Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + writer.write_sequence(|writer| { + for extension in &self.exts { + Self::write_extension(writer, extension); + } + }) + } + + /// Write a single extension SEQUENCE to the DER writer. + fn write_extension(writer: &mut DERWriterSeq, extension: &Box) { + // 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.next().write_sequence(|writer| { + writer.next().write_oid(&extension.oid()); + writer + .next() + .write_bool(matches!(extension.criticality(), Criticality::Critical)); + writer.next().write_bytes(&yasna::construct_der(|writer| { + extension.write_value(writer) + })); + }); + } +} + +/// An X.509v3 authority key identifier extension according to +/// [RFC 5280 4.2.1.1](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1). +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) struct AuthorityKeyIdentifier { + key_identifier: Vec, +} + +impl Extension for AuthorityKeyIdentifier { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_AUTHORITY_KEY_IDENTIFIER) + } + + fn criticality(&self) -> Criticality { + // Conforming CAs MUST mark this extension as non-critical. + Criticality::NonCritical + } + + fn write_value(&self, writer: DERWriter) { + /* + AuthorityKeyIdentifier ::= SEQUENCE { + keyIdentifier [0] KeyIdentifier OPTIONAL, + authorityCertIssuer [1] GeneralNames OPTIONAL, + authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } + KeyIdentifier ::= OCTET STRING + */ + writer.write_sequence(|writer| { + writer + .next() + .write_tagged_implicit(Tag::context(0), |writer| { + writer.write_bytes(&self.key_identifier) + }) + }); + } +} + +impl From<&Certificate> for AuthorityKeyIdentifier { + fn from(cert: &Certificate) -> Self { + Self { + key_identifier: cert.get_key_identifier(), + } + } +} + +/// An X.509v3 subject alternative name extension according to +/// [RFC 5280 4.2.1.6](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct SubjectAlternativeName { + criticality: Criticality, + names: Vec, +} + +impl SubjectAlternativeName { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + match params.subject_alt_names.is_empty() { + true => None, + false => Some(Self { + // TODO(XXX): For now we mark the SAN extension as non-critical, matching the pre-existing + // handling, however per 5280 this extension's criticality is determined based + // on whether or not the subject contains an empty sequence. + criticality: Criticality::NonCritical, + names: params.subject_alt_names.clone(), + }), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + Ok(match ext { + x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) => { + for name in &san.general_names { + params + .subject_alt_names + .push(SanType::try_from_general(name)?); + } + true + }, + _ => false, + }) + } + + fn write_name(writer: DERWriter, san: &SanType) { + writer.write_tagged_implicit(Tag::context(san.tag()), |writer| match san { + SanType::Rfc822Name(name) | SanType::DnsName(name) | SanType::URI(name) => { + writer.write_ia5_string(&name) + }, + SanType::IpAddress(IpAddr::V4(addr)) => writer.write_bytes(&addr.octets()), + SanType::IpAddress(IpAddr::V6(addr)) => writer.write_bytes(&addr.octets()), + }) + } +} + +impl Extension for SubjectAlternativeName { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_SUBJECT_ALT_NAME) + } + + fn criticality(&self) -> Criticality { + // this extension's criticality is determined based on whether or not the subject contains + // an empty sequence. If it does, the SAN MUST be critical. If it has a non-empty subject + // distinguished name, the SAN SHOULD be non-critical. + self.criticality + } + + fn write_value(&self, writer: DERWriter) { + /* + SubjectAltName ::= GeneralNames + GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName + */ + writer.write_sequence(|writer| { + self.names + .iter() + .for_each(|san| Self::write_name(writer.next(), san)); + }); + } +} + +/// An X.509v3 key usage extension according to +/// [RFC 5280 4.2.1.3](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct KeyUsage { + usages: Vec, +} + +impl KeyUsage { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + match params.key_usages.is_empty() { + true => None, + false => Some(Self { + usages: params.key_usages.clone(), + }), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + match ext { + x509_parser::extensions::ParsedExtension::KeyUsage(ku) => { + let mut usages = Vec::new(); + if ku.digital_signature() { + usages.push(KeyUsagePurpose::DigitalSignature); + } + // Note: previous editions of X.509 called ContentCommitment "Non repudiation" + if ku.non_repudiation() { + usages.push(KeyUsagePurpose::ContentCommitment); + } + if ku.key_encipherment() { + usages.push(KeyUsagePurpose::KeyEncipherment); + } + if ku.key_cert_sign() { + usages.push(KeyUsagePurpose::KeyCertSign); + } + if ku.crl_sign() { + usages.push(KeyUsagePurpose::CrlSign); + } + if ku.encipher_only() { + usages.push(KeyUsagePurpose::EncipherOnly); + } + if ku.decipher_only() { + usages.push(KeyUsagePurpose::DecipherOnly); + } + params.key_usages = usages; + Ok(true) + }, + _ => Ok(false), + } + } +} + +impl Extension for KeyUsage { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_KEY_USAGE) + } + + fn criticality(&self) -> Criticality { + // When present, conforming CAs SHOULD mark this extension as critical. + Criticality::Critical + } + + fn write_value(&self, writer: DERWriter) { + use KeyUsagePurpose::*; + + /* + KeyUsage ::= BIT STRING { + digitalSignature (0), + nonRepudiation (1), -- recent editions of X.509 have + -- renamed this bit to contentCommitment + keyEncipherment (2), + dataEncipherment (3), + keyAgreement (4), + keyCertSign (5), + cRLSign (6), + encipherOnly (7), + decipherOnly (8) } + */ + let mut bits: u16 = 0; + + for entry in &self.usages { + // Map the index to a value + let index = match entry { + DigitalSignature => 0, + ContentCommitment => 1, + KeyEncipherment => 2, + DataEncipherment => 3, + KeyAgreement => 4, + KeyCertSign => 5, + CrlSign => 6, + EncipherOnly => 7, + DecipherOnly => 8, + }; + + bits |= 1 << index; + } + + // Compute the 1-based most significant bit + let msb = 16 - bits.leading_zeros(); + let nb = if msb <= 8 { 1 } else { 2 }; + let bits = bits.reverse_bits().to_be_bytes(); + + // Finally take only the bytes != 0 + let bits = &bits[..nb]; + writer.write_bitvec_bytes(&bits, msb as usize) + } +} + +/// An X.509v3 extended key usage extension according to +/// [RFC 5280 4.2.1.12](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct ExtendedKeyUsage { + usages: Vec, + // This extension MAY, at the option of the certificate issuer, be + // either critical or non-critical. + critical: Criticality, +} + +impl ExtendedKeyUsage { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + match params.extended_key_usages.is_empty() { + true => None, + false => Some(Self { + usages: params.extended_key_usages.clone(), + // TODO(xxx): Consider making EKU criticality configurable through params. + critical: Criticality::NonCritical, + }), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + match ext { + x509_parser::extensions::ParsedExtension::ExtendedKeyUsage(eku) => { + use ExtendedKeyUsagePurpose::*; + + let mut usages = Vec::new(); + if eku.any { + usages.push(Any); + } + if eku.server_auth { + usages.push(ServerAuth); + } + if eku.client_auth { + usages.push(ClientAuth); + } + if eku.code_signing { + usages.push(CodeSigning); + } + if eku.email_protection { + usages.push(EmailProtection); + } + if eku.time_stamping { + usages.push(TimeStamping); + } + if eku.ocsp_signing { + usages.push(OcspSigning); + } + params.extended_key_usages = usages; + Ok(true) + }, + _ => Ok(false), + } + } +} + +impl Extension for ExtendedKeyUsage { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_EXT_KEY_USAGE) + } + + fn criticality(&self) -> Criticality { + self.critical + } + + fn write_value(&self, writer: DERWriter) { + /* + ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId + KeyPurposeId ::= OBJECT IDENTIFIER + */ + writer.write_sequence(|writer| { + for usage in self.usages.iter() { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(usage.oid())); + } + }); + } +} + +/// An X.509v3 name constraints extension according to +/// [RFC 5280 4.2.1.10](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.10). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct NameConstraints { + permitted_subtrees: Vec, + excluded_subtrees: Vec, +} + +impl NameConstraints { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + match ¶ms.name_constraints { + Some(nc) if nc.permitted_subtrees.is_empty() && nc.excluded_subtrees.is_empty() => { + return None; // Avoid writing an empty name constraints extension. + }, + Some(nc) => Some(Self { + permitted_subtrees: nc.permitted_subtrees.clone(), + excluded_subtrees: nc.excluded_subtrees.clone(), + }), + _ => None, + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + Ok(match ext { + x509_parser::extensions::ParsedExtension::NameConstraints(ncs) => { + let mut permitted_subtrees = Vec::default(); + if let Some(ncs_permitted) = &ncs.permitted_subtrees { + permitted_subtrees = ncs_permitted + .iter() + .map(GeneralSubtree::from_x509_general_subtree) + .collect::, _>>()?; + } + let mut excluded_subtrees = Vec::default(); + if let Some(ncs_excluded) = &ncs.excluded_subtrees { + excluded_subtrees = ncs_excluded + .iter() + .map(GeneralSubtree::from_x509_general_subtree) + .collect::, _>>()?; + } + if !permitted_subtrees.is_empty() || !excluded_subtrees.is_empty() { + params.name_constraints = Some(crate::NameConstraints { + permitted_subtrees, + excluded_subtrees, + }); + } + true + }, + _ => false, + }) + } + + fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[GeneralSubtree]) { + /* + GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree + GeneralSubtree ::= SEQUENCE { + base GeneralName, + minimum [0] BaseDistance DEFAULT 0, + maximum [1] BaseDistance OPTIONAL } + BaseDistance ::= INTEGER (0..MAX) + */ + writer.write_tagged_implicit(Tag::context(tag), |writer| { + writer.write_sequence(|writer| { + for subtree in general_subtrees.iter() { + writer.next().write_sequence(|writer| { + writer.next().write_tagged_implicit( + Tag::context(subtree.tag()), + |writer| match subtree { + GeneralSubtree::Rfc822Name(name) + | GeneralSubtree::DnsName(name) => writer.write_ia5_string(name), + GeneralSubtree::DirectoryName(name) => { + write_distinguished_name(writer, name) + }, + GeneralSubtree::IpAddress(subnet) => { + writer.write_bytes(&subnet.to_bytes()) + }, + }, + ); + // minimum must be 0 (the default) and maximum must be absent + }); + } + }); + }); + } +} + +impl Extension for NameConstraints { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_NAME_CONSTRAINTS) + } + + fn criticality(&self) -> Criticality { + // Conforming CAs MUST mark this extension as critical + Criticality::Critical + } + + 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 !self.excluded_subtrees.is_empty() { + Self::write_general_subtrees(writer.next(), 1, &self.excluded_subtrees); + } + }); + } +} + +/// An X.509v3 CRL distribution points extension according to +/// [RFC 5280 4.2.1.13](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct CrlDistributionPoints { + distribution_points: Vec, +} + +impl CrlDistributionPoints { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + match params.crl_distribution_points.is_empty() { + true => return None, // Avoid writing an empty CRL distribution points extension. + false => Some(Self { + distribution_points: params.crl_distribution_points.clone(), + }), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + Ok(match ext { + x509_parser::extensions::ParsedExtension::CRLDistributionPoints(crl_dps) => { + let dps = crl_dps + .points + .iter() + .map(|dp| { + // Rcgen does not support CRL DPs with specific reasons, or an indirect issuer. + if dp.reasons.is_some() || dp.crl_issuer.is_some() { + return Err(Error::UnsupportedCrlDistributionPoint); + } + let general_names = match &dp.distribution_point { + Some(x509_parser::extensions::DistributionPointName::FullName( + general_names, + )) => Ok(general_names), + // Rcgen does not support CRL DPs missing a distribution point, + // or that specific a distribution point with a name relative + // to an issuer. + _ => Err(Error::UnsupportedCrlDistributionPoint), + }?; + let uris = general_names + .iter() + .map(|general_name| match general_name { + x509_parser::extensions::GeneralName::URI(uri) => { + Ok(uri.to_string()) + }, + // Rcgen does not support CRL DP general names other than URI. + _ => Err(Error::UnsupportedGeneralName), + }) + .collect::, _>>()?; + Ok(crate::CrlDistributionPoint { uris }) + }) + .collect::, _>>()?; + params.crl_distribution_points = dps; + true + }, + _ => false, + }) + } +} + +impl Extension for CrlDistributionPoints { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_CRL_DISTRIBUTION_POINTS) + } + + fn criticality(&self) -> Criticality { + // The extension SHOULD be non-critical + Criticality::NonCritical + } + + fn write_value(&self, writer: DERWriter) { + writer.write_sequence(|writer| { + for distribution_point in &self.distribution_points { + distribution_point.write_der(writer.next()); + } + }) + } +} + +/// An X.509v3 subject key identifier extension according to +/// [RFC 5280 4.2.1.2](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct SubjectKeyIdentifier { + key_identifier: Vec, +} + +impl SubjectKeyIdentifier { + pub(crate) fn from_params(params: &CertificateParams, pub_key: &K) -> Self { + Self { + key_identifier: params.key_identifier(pub_key), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + Ok(match ext { + x509_parser::extensions::ParsedExtension::SubjectKeyIdentifier(ski) => { + params.key_identifier_method = crate::KeyIdMethod::PreSpecified(ski.0.to_vec()); + true + }, + _ => false, + }) + } +} + +impl Extension for SubjectKeyIdentifier { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_SUBJECT_KEY_IDENTIFIER) + } + + fn criticality(&self) -> Criticality { + // Conforming CAs MUST mark this extension as non-critical. + Criticality::NonCritical + } + + fn write_value(&self, writer: DERWriter) { + // SubjectKeyIdentifier ::= KeyIdentifier + // KeyIdentifier ::= OCTET STRING + writer.write_bytes(&self.key_identifier) + } +} + +/// An X.509v3 basic constraints extension according to +/// [RFC 5280 4.2.1.9](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct BasicConstraints { + is_ca: IsCa, +} + +impl BasicConstraints { + pub(crate) fn from_params(params: &CertificateParams) -> Option { + // For NoCa we don't emit the extension, it is implied not a CA. + // Use ExplicitNoCa when you want the false cA ext emitted. + match params.is_ca { + IsCa::NoCa => None, + _ => Some(Self { + is_ca: params.is_ca.clone(), + }), + } + } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_parsed( + params: &mut CertificateParams, + ext: &x509_parser::extensions::ParsedExtension, + ) -> Result { + Ok(match ext { + x509_parser::extensions::ParsedExtension::BasicConstraints(bc) => { + match (bc.ca, bc.path_len_constraint) { + (true, Some(len)) => { + params.is_ca = IsCa::Ca(crate::BasicConstraints::Constrained( + u8::try_from(len) + .map_err(|_| Error::UnsupportedBasicConstraintsPathLen)?, + )); + }, + (true, None) => { + params.is_ca = IsCa::Ca(crate::BasicConstraints::Unconstrained); + }, + (false, _) => { + params.is_ca = IsCa::ExplicitNoCa; + }, + } + true + }, + _ => false, + }) + } +} + +impl Extension for BasicConstraints { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_BASIC_CONSTRAINTS) + } + + fn criticality(&self) -> Criticality { + // 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 + Criticality::Critical + } + + fn write_value(&self, writer: DERWriter) { + /* + BasicConstraints ::= SEQUENCE { + cA BOOLEAN DEFAULT FALSE, + pathLenConstraint INTEGER (0..MAX) OPTIONAL } + */ + writer.write_sequence(|writer| { + writer.next().write_bool(matches!(self.is_ca, IsCa::Ca(_))); + if let IsCa::Ca(crate::BasicConstraints::Constrained(path_len_constraint)) = self.is_ca + { + writer.next().write_u8(path_len_constraint); + } + }); + } +} + +/// An X.509v3 CRL number extension according to +/// [RFC 5280 5.2.3](https://www.rfc-editor.org/rfc/rfc5280#section-5.2.3) +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct CrlNumber { + number: SerialNumber, +} + +impl CrlNumber { + pub(crate) fn from_params(params: &CertificateRevocationListParams) -> Self { + Self { + number: params.crl_number.clone(), + } + } +} + +impl Extension for CrlNumber { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_CRL_NUMBER) + } + + fn criticality(&self) -> Criticality { + // CRL issuers conforming to this profile MUST include this extension in all + // CRLs and MUST mark this extension as non-critical. + Criticality::NonCritical + } + + fn write_value(&self, writer: DERWriter) { + // CRLNumber ::= INTEGER (0..MAX) + writer.write_bigint_bytes(self.number.as_ref(), true); + } +} + +/// An X.509v3 issuing distribution point extension according to +/// [RFC 5280 5.2.5](https://www.rfc-editor.org/rfc/rfc5280#section-5.2.5) +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct IssuingDistributionPoint { + point: CrlIssuingDistributionPoint, +} + +impl IssuingDistributionPoint { + pub(crate) fn from_params(params: &CertificateRevocationListParams) -> Option { + match ¶ms.issuing_distribution_point { + Some(idp) => Some(Self { point: idp.clone() }), + None => None, + } + } +} + +impl Extension for IssuingDistributionPoint { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_CRL_ISSUING_DISTRIBUTION_POINT) + } + + fn criticality(&self) -> Criticality { + // Although the extension is critical, conforming implementations are not required to support this + // extension. + Criticality::Critical + } + + fn write_value(&self, writer: DERWriter) { + self.point.write_der(writer); + } +} + +/// An X.509v3 reason code extension according to +/// [RFC 5280 5.3.1](https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct ReasonCode { + reason: crl::RevocationReason, +} + +impl ReasonCode { + pub(crate) fn from_params(params: &RevokedCertParams) -> Option { + match ¶ms.reason_code { + Some(reason) => Some(Self { reason: *reason }), + None => None, + } + } +} + +impl Extension for ReasonCode { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_CRL_REASONS) + } + + fn criticality(&self) -> Criticality { + // The reasonCode is a non-critical CRL entry extension + Criticality::NonCritical + } + + 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.reason as i64); + } +} + +/// An X.509v3 invalidity date extension according to +/// [RFC 5280 5.3.2](https://www.rfc-editor.org/rfc/rfc5280#section-5.3.2). +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct InvalidityDate { + date: OffsetDateTime, +} + +impl InvalidityDate { + pub(crate) fn from_params(params: &RevokedCertParams) -> Option { + match ¶ms.invalidity_date { + Some(date) => Some(Self { date: date.clone() }), + None => None, + } + } +} + +impl Extension for InvalidityDate { + fn oid(&self) -> ObjectIdentifier { + ObjectIdentifier::from_slice(oid::OID_CRL_INVALIDITY_DATE) + } + + fn criticality(&self) -> Criticality { + // The invalidity date is a non-critical CRL entry extension + Criticality::NonCritical + } + + fn write_value(&self, writer: DERWriter) { + // InvalidityDate ::= GeneralizedTime + write_dt_utc_or_generalized(writer, self.date); + } +} + +#[cfg(test)] +mod extensions_tests { + use crate::oid; + + use super::Criticality::*; + use super::*; + + #[test] + fn test_no_duplicates() { + let oid = ObjectIdentifier::from_slice(oid::OID_SUBJECT_ALT_NAME); + let ext = Box::new(DummyExt { + oid: oid.clone(), + critical: NonCritical, + der: Vec::default(), + }); + + // It should be an error to add two extensions with the same OID. + let mut exts = Extensions::default(); + exts.add_extension(ext.clone()).unwrap(); + assert_eq!( + exts.add_extension(ext.clone()), + Err(Error::DuplicateExtension(oid.to_string())), + ); + + // Or to construct an extensions set from an iterator containing two extensions with the + // same OID. + assert_eq!( + Extensions::new(vec![ + ext.clone() as Box, + ext.clone() as Box + ]) + .unwrap_err(), + Error::DuplicateExtension(oid.to_string()), + ); + } + + #[test] + fn test_write_der() { + use yasna::construct_der; + + // Construct three dummy extensions. + let ext_a = Box::new(DummyExt { + oid: ObjectIdentifier::from_slice(&[1, 3, 6, 1, 4, 3]), + critical: Critical, + der: b"a".to_vec(), + }); + + let ext_b = Box::new(DummyExt { + oid: ObjectIdentifier::from_slice(&[1, 3, 6, 1, 4, 2]), + critical: NonCritical, + der: b"b".to_vec(), + }); + + let ext_c = Box::new(DummyExt { + oid: ObjectIdentifier::from_slice(&[1, 3, 6, 1, 4, 1]), + critical: Critical, + der: b"c".to_vec(), + }); + + // Items of note: + // - We expect the extensions to be written in the order they were added. + // - The ext_b criticality is elided because it is non-critical - it would be a mis-encoding + // to write a value for a FALSE BOOLEAN in DER. + // - Each extension DER value should have been written unmodified, with no extra tags + // or length bytes. + let expected_der = vec![ + 0x30, 0x2D, // exts SEQUENCE + 0x30, 0xD, // ext_a SEQUENCE + 0x6, 0x5, 0x2B, 0x6, 0x1, 0x4, 0x3, 0x1, 0x1, // ext_a OID + 0xFF, // ext_A CRITICAL = true + 0x4, 0x1, 0x61, // ext_A OCTET SEQUENCE "A" (0x61) + 0x30, 0xD, // ext_b SEQUENCE + 0x6, 0x5, 0x2B, 0x6, 0x1, 0x4, 0x2, 0x1, 0x1, 0x0, // ext_b OID + // ext_b criticality elided + 0x4, 0x1, 0x62, // ext_b OCTET SEQUENCE "B" (0x62) + 0x30, 0xD, // ext_b SEQUENCE + 0x6, 0x5, 0x2B, 0x6, 0x1, 0x4, 0x1, 0x1, 0x1, // ext_c OID + 0xFF, // ext_b CRITICAL = true + 0x4, 0x1, 0x63, // ext_c OCTET SEQUENCE "C" (0x63) + ]; + + // Building the extensions and encoding to DER should result in the expected DER. + let test_exts: Vec> = vec![ext_a.clone(), ext_b.clone(), ext_c.clone()]; + let exts = Extensions::new(test_exts).unwrap(); + assert_eq!(construct_der(|writer| exts.write_der(writer)), expected_der); + } + + /// Mock extension for testing. + #[derive(Debug, Clone)] + struct DummyExt { + oid: ObjectIdentifier, + critical: Criticality, + der: Vec, + } + + impl Extension for DummyExt { + fn oid(&self) -> ObjectIdentifier { + self.oid.clone() + } + + fn criticality(&self) -> Criticality { + self.critical + } + + fn write_value(&self, writer: DERWriter) { + writer.write_der(&self.der); + } + } +} + +#[cfg(test)] +mod san_ext_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + let domain_a = "test.example.com".to_string(); + let domain_b = "example.com".to_string(); + let ip = IpAddr::try_from([127, 0, 0, 1]).unwrap(); + let mut params = CertificateParams::new(vec![domain_a.clone(), domain_b.clone()]); + params + .subject_alt_names + .push(SanType::IpAddress(ip.clone())); + + let ext = SubjectAlternativeName::from_params(¶ms).unwrap(); + let expected_names = vec![ + SanType::DnsName(domain_a), + SanType::DnsName(domain_b), + SanType::IpAddress(ip), + ]; + assert_eq!(ext.names, expected_names); + } + + #[test] + fn test_from_empty_san() { + assert!(SubjectAlternativeName::from_params(&CertificateParams::default()).is_none()); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let domain_a = "test.example.com".to_string(); + let domain_b = "example.com".to_string(); + let ip = IpAddr::try_from([127, 0, 0, 1]).unwrap(); + let mut params = CertificateParams::new(vec![domain_a.clone(), domain_b.clone()]); + params + .subject_alt_names + .push(SanType::IpAddress(ip.clone())); + + let der = yasna::construct_der(|writer| { + SubjectAlternativeName::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::SubjectAlternativeName( + x509_parser::extensions::SubjectAlternativeName::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + SubjectAlternativeName::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!(recovered_params.subject_alt_names, params.subject_alt_names); + } +} + +#[cfg(test)] +mod ku_ext_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + let mut params = CertificateParams::default(); + params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, + KeyUsagePurpose::KeyEncipherment, + KeyUsagePurpose::DataEncipherment, + KeyUsagePurpose::KeyAgreement, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::EncipherOnly, + KeyUsagePurpose::DecipherOnly, + ]; + + let ext = KeyUsage::from_params(¶ms).unwrap(); + assert_eq!(ext.usages, params.key_usages); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let mut params = CertificateParams::default(); + params.key_usages = vec![ + KeyUsagePurpose::ContentCommitment, + KeyUsagePurpose::KeyEncipherment, + ]; + + let der = yasna::construct_der(|writer| { + KeyUsage::from_params(¶ms).unwrap().write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::KeyUsage( + x509_parser::extensions::KeyUsage::from_der(&der).unwrap().1, + ); + + let mut recovered_params = CertificateParams::default(); + KeyUsage::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!(recovered_params.key_usages, params.key_usages); + } +} + +#[cfg(test)] +mod eku_ext_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + let mut params = CertificateParams::default(); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::Any, + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ExtendedKeyUsagePurpose::TimeStamping, + ExtendedKeyUsagePurpose::OcspSigning, + ]; + + let ext = ExtendedKeyUsage::from_params(¶ms).unwrap(); + assert_eq!(ext.usages, params.extended_key_usages); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let mut params = CertificateParams::default(); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::CodeSigning, + ExtendedKeyUsagePurpose::EmailProtection, + ]; + + let der = yasna::construct_der(|writer| { + ExtendedKeyUsage::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::ExtendedKeyUsage( + x509_parser::extensions::ExtendedKeyUsage::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + ExtendedKeyUsage::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!( + recovered_params.extended_key_usages, + params.extended_key_usages + ); + } +} + +#[cfg(test)] +mod name_constraints_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + let constraints = crate::NameConstraints { + permitted_subtrees: vec![GeneralSubtree::DnsName("com".into())], + excluded_subtrees: vec![GeneralSubtree::DnsName("org".into())], + }; + let mut params = CertificateParams::default(); + params.name_constraints = Some(constraints.clone()); + + let ext = NameConstraints::from_params(¶ms).unwrap(); + assert_eq!(ext.permitted_subtrees, constraints.permitted_subtrees); + assert_eq!(ext.excluded_subtrees, constraints.excluded_subtrees); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let mut params = CertificateParams::default(); + let constraints = crate::NameConstraints { + permitted_subtrees: vec![GeneralSubtree::DnsName("com".into())], + excluded_subtrees: Vec::default(), + }; + params.name_constraints = Some(constraints.clone()); + + let der = yasna::construct_der(|writer| { + NameConstraints::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::NameConstraints( + x509_parser::extensions::NameConstraints::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + NameConstraints::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert!(recovered_params.name_constraints.is_some()); + assert_eq!( + recovered_params + .name_constraints + .as_ref() + .unwrap() + .permitted_subtrees, + constraints.permitted_subtrees, + ); + assert_eq!( + recovered_params.name_constraints.unwrap().excluded_subtrees, + constraints.excluded_subtrees, + ); + } +} + +#[cfg(test)] +mod crl_dps_test { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + let crl_dps = vec![ + crate::CrlDistributionPoint { + uris: vec!["http://example.com".into()], + }, + crate::CrlDistributionPoint { + uris: vec!["http://example.org".into(), "ldap://example.com".into()], + }, + ]; + let mut params = CertificateParams::default(); + params.crl_distribution_points = crl_dps.clone(); + + let ext = CrlDistributionPoints::from_params(¶ms).unwrap(); + assert_eq!(ext.distribution_points, crl_dps); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let mut params = CertificateParams::default(); + let crl_dps = vec![crate::CrlDistributionPoint { + uris: vec!["http://example.com".into()], + }]; + params.crl_distribution_points = crl_dps.clone(); + + let der = yasna::construct_der(|writer| { + CrlDistributionPoints::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::CRLDistributionPoints( + x509_parser::extensions::CRLDistributionPoints::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + CrlDistributionPoints::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!(recovered_params.crl_distribution_points, crl_dps,); + } +} + +#[cfg(test)] +mod ski_ext_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::{CertificateParams, KeyIdMethod, KeyPair}; + + #[test] + fn test_from_params() { + let ski = vec![1, 2, 3, 4]; + let mut params = CertificateParams::default(); + params.key_identifier_method = KeyIdMethod::PreSpecified(ski.clone()); + + let keypair = KeyPair::generate(&crate::PKCS_ECDSA_P256_SHA256).unwrap(); + let ext = SubjectKeyIdentifier::from_params(¶ms, &keypair); + assert_eq!(ext.key_identifier, ski); + + let keypair = KeyPair::generate(&crate::PKCS_ECDSA_P256_SHA256).unwrap(); + let mut params = CertificateParams::default(); + params.key_pair = Some(keypair); + + let keypair = params.key_pair.as_ref().unwrap(); + let ext = SubjectKeyIdentifier::from_params(¶ms, keypair); + assert_ne!(ext.key_identifier, ski); + assert_eq!(ext.key_identifier.len(), 20); // SHA-256 digest truncated to SHA-1 length + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed() { + let ski = vec![1, 2, 3, 4]; + let keypair = KeyPair::generate(&crate::PKCS_ECDSA_P256_SHA256).unwrap(); + + let mut params = CertificateParams::default(); + params.key_pair = Some(keypair); + params.key_identifier_method = KeyIdMethod::PreSpecified(ski.clone()); + + let keypair = params.key_pair.as_ref().unwrap(); + let der = yasna::construct_der(|writer| { + SubjectKeyIdentifier::from_params(¶ms, keypair).write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::SubjectKeyIdentifier( + x509_parser::extensions::KeyIdentifier::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + SubjectKeyIdentifier::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!( + recovered_params.key_identifier_method, + KeyIdMethod::PreSpecified(ski) + ); + } +} + +#[cfg(test)] +mod bc_ext_tests { + #[cfg(feature = "x509-parser")] + use x509_parser::prelude::FromDer; + + use super::*; + use crate::CertificateParams; + + #[test] + fn test_from_params() { + // NoCA + let params = CertificateParams::default(); + let ext = BasicConstraints::from_params(¶ms); + assert!(ext.is_none()); // No ext for NoCA. + + // Explicit NoCA + let mut params = CertificateParams::default(); + params.is_ca = IsCa::ExplicitNoCa; + let ext = BasicConstraints::from_params(¶ms); + assert_eq!(ext.unwrap().is_ca, IsCa::ExplicitNoCa); + + // CA unconstrained + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(crate::BasicConstraints::Unconstrained); + let ext = BasicConstraints::from_params(¶ms); + assert_eq!( + ext.unwrap().is_ca, + IsCa::Ca(crate::BasicConstraints::Unconstrained) + ); + + // CA constrained + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(crate::BasicConstraints::Constrained(1)); + let ext = BasicConstraints::from_params(¶ms); + assert_eq!( + ext.unwrap().is_ca, + IsCa::Ca(crate::BasicConstraints::Constrained(1)) + ); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed_explicit_no_ca() { + let mut params = CertificateParams::default(); + params.is_ca = IsCa::ExplicitNoCa; + + let der = yasna::construct_der(|writer| { + BasicConstraints::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::BasicConstraints( + x509_parser::extensions::BasicConstraints::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + BasicConstraints::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!(recovered_params.is_ca, IsCa::ExplicitNoCa); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed_unconstrained() { + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(crate::BasicConstraints::Unconstrained); + + let der = yasna::construct_der(|writer| { + BasicConstraints::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::BasicConstraints( + x509_parser::extensions::BasicConstraints::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + BasicConstraints::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!( + recovered_params.is_ca, + IsCa::Ca(crate::BasicConstraints::Unconstrained) + ); + } + + #[test] + #[cfg(feature = "x509-parser")] + fn test_from_parsed_constrained() { + let mut params = CertificateParams::default(); + let path_len = 5; + params.is_ca = IsCa::Ca(crate::BasicConstraints::Constrained(path_len)); + + let der = yasna::construct_der(|writer| { + BasicConstraints::from_params(¶ms) + .unwrap() + .write_value(writer) + }); + + let parsed_ext = x509_parser::extensions::ParsedExtension::BasicConstraints( + x509_parser::extensions::BasicConstraints::from_der(&der) + .unwrap() + .1, + ); + + let mut recovered_params = CertificateParams::default(); + BasicConstraints::from_parsed(&mut recovered_params, &parsed_ext).unwrap(); + assert_eq!( + recovered_params.is_ca, + IsCa::Ca(crate::BasicConstraints::Constrained(path_len)) + ); + } +} diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 666dae47..b0542a9c 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -45,6 +45,8 @@ use std::net::IpAddr; use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; +#[cfg(feature = "x509-parser")] +use x509_parser::prelude::ParsedExtension; use yasna::models::ObjectIdentifier; use yasna::models::{GeneralizedTime, UTCTime}; use yasna::tags::{TAG_BMPSTRING, TAG_TELETEXSTRING, TAG_UNIVERSALSTRING}; @@ -57,12 +59,15 @@ pub use crate::crl::{ }; pub use crate::csr::{CertificateSigningRequest, PublicKey}; pub use crate::error::Error; +use crate::ext::Extensions; use crate::key_pair::PublicKeyData; pub use crate::key_pair::{KeyPair, RemoteKeyPair}; use crate::oid::*; pub use crate::sign_algo::algo::*; pub use crate::sign_algo::SignatureAlgorithm; +pub use crate::ext::{AcmeIdentifier, Criticality, CustomExtension}; + /// Type-alias for the old name of [`Error`]. #[deprecated( note = "Renamed to `Error`. We recommend to refer to it by fully-qualifying the crate: `rcgen::Error`." @@ -114,6 +119,7 @@ pub fn generate_simple_self_signed( mod crl; mod csr; mod error; +mod ext; mod key_pair; mod oid; mod sign_algo; @@ -218,6 +224,24 @@ impl GeneralSubtree { GeneralSubtree::IpAddress(_addr) => TAG_IP_ADDRESS, } } + + #[cfg(feature = "x509-parser")] + pub(crate) fn from_x509_general_subtree( + general_subtree: &x509_parser::extensions::GeneralSubtree, + ) -> Result { + use x509_parser::extensions::GeneralName as X509GeneralName; + match &general_subtree.base { + X509GeneralName::RFC822Name(name) => Ok(GeneralSubtree::Rfc822Name(name.to_string())), + X509GeneralName::DNSName(name) => Ok(GeneralSubtree::DnsName(name.to_string())), + X509GeneralName::DirectoryName(name) => Ok(GeneralSubtree::DirectoryName( + DistinguishedName::from_name(name)?, + )), + // TODO(XXX): Consider how to handle the &[u8] that x509_parser provides. + // It would need to be mapped into the rcgen CidrSubnet type. + // GeneralName::IPAddress(addr) => GeneralSubtree::IpAddress(...) + _ => Err(Error::UnsupportedGeneralName), + } + } } #[derive(Debug, PartialEq, Eq, Hash, Clone)] @@ -603,321 +627,72 @@ impl CertificateParams { let alg = SignatureAlgorithm::from_oid(&alg_oid.collect::>())?; let dn = DistinguishedName::from_name(&x509.tbs_certificate.subject)?; - let is_ca = Self::convert_x509_is_ca(&x509)?; let validity = x509.validity(); - let subject_alt_names = Self::convert_x509_subject_alternative_name(&x509)?; - let key_usages = Self::convert_x509_key_usages(&x509)?; - let extended_key_usages = Self::convert_x509_extended_key_usages(&x509)?; - let name_constraints = Self::convert_x509_name_constraints(&x509)?; let serial_number = Some(x509.serial.to_bytes_be().into()); - let key_identifier_method = x509 - .iter_extensions() - .find_map(|ext| match ext.parsed_extension() { - x509_parser::extensions::ParsedExtension::SubjectKeyIdentifier(key_id) => { - Some(KeyIdMethod::PreSpecified(key_id.0.into())) - }, - _ => None, - }) - .unwrap_or(KeyIdMethod::Sha256); - - Ok(CertificateParams { + let mut params = CertificateParams { alg, - is_ca, - subject_alt_names, - key_usages, - extended_key_usages, - name_constraints, serial_number, - key_identifier_method, distinguished_name: dn, key_pair: Some(key_pair), not_before: validity.not_before.to_datetime(), not_after: validity.not_after.to_datetime(), ..Default::default() - }) - } - #[cfg(feature = "x509-parser")] - fn convert_x509_is_ca( - x509: &x509_parser::certificate::X509Certificate<'_>, - ) -> Result { - use x509_parser::extensions::BasicConstraints as B; - - let basic_constraints = x509 - .basic_constraints() - .or(Err(Error::CouldNotParseCertificate))? - .map(|ext| ext.value); - - let is_ca = match basic_constraints { - Some(B { - ca: true, - path_len_constraint: Some(n), - }) if *n <= u8::MAX as u32 => IsCa::Ca(BasicConstraints::Constrained(*n as u8)), - Some(B { - ca: true, - path_len_constraint: Some(_), - }) => return Err(Error::CouldNotParseCertificate), - Some(B { - ca: true, - path_len_constraint: None, - }) => IsCa::Ca(BasicConstraints::Unconstrained), - Some(B { ca: false, .. }) => IsCa::ExplicitNoCa, - None => IsCa::NoCa, }; - Ok(is_ca) - } - #[cfg(feature = "x509-parser")] - fn convert_x509_subject_alternative_name( - x509: &x509_parser::certificate::X509Certificate<'_>, - ) -> Result, Error> { - let sans = x509 - .subject_alternative_name() - .or(Err(Error::CouldNotParseCertificate))? - .map(|ext| &ext.value.general_names); - - if let Some(sans) = sans { - let mut subject_alt_names = Vec::with_capacity(sans.len()); - for san in sans { - subject_alt_names.push(SanType::try_from_general(san)?); - } - Ok(subject_alt_names) - } else { - Ok(Vec::new()) - } - } - #[cfg(feature = "x509-parser")] - fn convert_x509_key_usages( - x509: &x509_parser::certificate::X509Certificate<'_>, - ) -> Result, Error> { - let key_usage = x509 - .key_usage() - .or(Err(Error::CouldNotParseCertificate))? - .map(|ext| ext.value); - - let mut key_usages = Vec::new(); - if let Some(key_usage) = key_usage { - if key_usage.digital_signature() { - key_usages.push(KeyUsagePurpose::DigitalSignature); - } - if key_usage.non_repudiation() { - key_usages.push(KeyUsagePurpose::ContentCommitment); - } - if key_usage.key_encipherment() { - key_usages.push(KeyUsagePurpose::KeyEncipherment); - } - if key_usage.data_encipherment() { - key_usages.push(KeyUsagePurpose::DataEncipherment); - } - if key_usage.key_agreement() { - key_usages.push(KeyUsagePurpose::KeyAgreement); - } - if key_usage.key_cert_sign() { - key_usages.push(KeyUsagePurpose::KeyCertSign); - } - if key_usage.crl_sign() { - key_usages.push(KeyUsagePurpose::CrlSign); - } - if key_usage.encipher_only() { - key_usages.push(KeyUsagePurpose::EncipherOnly); - } - if key_usage.decipher_only() { - key_usages.push(KeyUsagePurpose::DecipherOnly); - } - } - Ok(key_usages) - } - #[cfg(feature = "x509-parser")] - fn convert_x509_extended_key_usages( - x509: &x509_parser::certificate::X509Certificate<'_>, - ) -> Result, Error> { - let extended_key_usage = x509 - .extended_key_usage() - .or(Err(Error::CouldNotParseCertificate))? - .map(|ext| ext.value); - - let mut extended_key_usages = Vec::new(); - if let Some(extended_key_usage) = extended_key_usage { - if extended_key_usage.any { - extended_key_usages.push(ExtendedKeyUsagePurpose::Any); - } - if extended_key_usage.server_auth { - extended_key_usages.push(ExtendedKeyUsagePurpose::ServerAuth); - } - if extended_key_usage.client_auth { - extended_key_usages.push(ExtendedKeyUsagePurpose::ClientAuth); - } - if extended_key_usage.code_signing { - extended_key_usages.push(ExtendedKeyUsagePurpose::CodeSigning); - } - if extended_key_usage.email_protection { - extended_key_usages.push(ExtendedKeyUsagePurpose::EmailProtection); - } - if extended_key_usage.time_stamping { - extended_key_usages.push(ExtendedKeyUsagePurpose::TimeStamping); - } - if extended_key_usage.ocsp_signing { - extended_key_usages.push(ExtendedKeyUsagePurpose::OcspSigning); - } - } - Ok(extended_key_usages) - } - #[cfg(feature = "x509-parser")] - fn convert_x509_name_constraints( - x509: &x509_parser::certificate::X509Certificate<'_>, - ) -> Result, Error> { - let constraints = x509 - .name_constraints() - .or(Err(Error::CouldNotParseCertificate))? - .map(|ext| ext.value); - - if let Some(constraints) = constraints { - let permitted_subtrees = if let Some(permitted) = &constraints.permitted_subtrees { - Self::convert_x509_general_subtrees(&permitted)? - } else { - Vec::new() - }; - - let excluded_subtrees = if let Some(excluded) = &constraints.excluded_subtrees { - Self::convert_x509_general_subtrees(&excluded)? - } else { - Vec::new() - }; - - let name_constraints = NameConstraints { - permitted_subtrees, - excluded_subtrees, - }; - - Ok(Some(name_constraints)) - } else { - Ok(None) - } - } - #[cfg(feature = "x509-parser")] - fn convert_x509_general_subtrees( - subtrees: &[x509_parser::extensions::GeneralSubtree<'_>], - ) -> Result, Error> { - use x509_parser::extensions::GeneralName; - - let mut result = Vec::new(); - for subtree in subtrees { - let subtree = match &subtree.base { - GeneralName::RFC822Name(s) => GeneralSubtree::Rfc822Name(s.to_string()), - GeneralName::DNSName(s) => GeneralSubtree::DnsName(s.to_string()), - GeneralName::DirectoryName(n) => { - GeneralSubtree::DirectoryName(DistinguishedName::from_name(&n)?) + for ext in x509.iter_extensions() { + let handled = match ext.parsed_extension() { + ext @ ParsedExtension::SubjectAlternativeName(_) => { + ext::SubjectAlternativeName::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::KeyUsage(_) => ext::KeyUsage::from_parsed(&mut params, ext)?, + ext @ ParsedExtension::ExtendedKeyUsage(_) => { + ext::ExtendedKeyUsage::from_parsed(&mut params, ext)? + }, + ext @ ParsedExtension::NameConstraints(_) => { + ext::NameConstraints::from_parsed(&mut params, ext)? }, - GeneralName::IPAddress(bytes) if bytes.len() == 8 => { - let addr: [u8; 4] = bytes[..4].try_into().unwrap(); - let mask: [u8; 4] = bytes[4..].try_into().unwrap(); - GeneralSubtree::IpAddress(CidrSubnet::V4(addr, mask)) + ext @ ParsedExtension::CRLDistributionPoints(_) => { + ext::CrlDistributionPoints::from_parsed(&mut params, ext)? }, - GeneralName::IPAddress(bytes) if bytes.len() == 32 => { - let addr: [u8; 16] = bytes[..16].try_into().unwrap(); - let mask: [u8; 16] = bytes[16..].try_into().unwrap(); - GeneralSubtree::IpAddress(CidrSubnet::V6(addr, mask)) + ext @ ParsedExtension::SubjectKeyIdentifier(_) => { + ext::SubjectKeyIdentifier::from_parsed(&mut params, ext)? }, - _ => continue, + ext @ ParsedExtension::BasicConstraints(_) => { + ext::BasicConstraints::from_parsed(&mut params, ext)? + }, + ParsedExtension::AuthorityKeyIdentifier(_) => { + true // We always handle emitting this ourselves - don't copy it as a custom extension. + }, + _ => false, }; - result.push(subtree); + if !handled { + params + .custom_extensions + .push(CustomExtension::from_parsed(ext)?); + } } - Ok(result) - } - fn write_subject_alt_names(&self, writer: DERWriter) { - write_x509_extension(writer, OID_SUBJECT_ALT_NAME, false, |writer| { - writer.write_sequence(|writer| { - for san in self.subject_alt_names.iter() { - writer.next().write_tagged_implicit( - Tag::context(san.tag()), - |writer| match san { - SanType::Rfc822Name(name) - | SanType::DnsName(name) - | SanType::URI(name) => writer.write_ia5_string(name), - SanType::IpAddress(IpAddr::V4(addr)) => { - writer.write_bytes(&addr.octets()) - }, - SanType::IpAddress(IpAddr::V6(addr)) => { - writer.write_bytes(&addr.octets()) - }, - }, - ); - } - }); - }); + + Ok(params) } fn write_request(&self, pub_key: &K, writer: DERWriter) -> Result<(), Error> { - // No .. pattern, we use this to ensure every field is used - #[deny(unused)] - let Self { - alg, - not_before, - not_after, - serial_number, - subject_alt_names, - distinguished_name, - is_ca, - key_usages, - extended_key_usages, - name_constraints, - crl_distribution_points, - custom_extensions, - key_pair, - use_authority_key_identifier_extension, - key_identifier_method, - } = self; - // - alg and key_pair will be used by the caller - // - not_before and not_after cannot be put in a CSR - // - 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 - // needed. - let _ = (alg, key_pair, not_before, not_after, key_identifier_method); - if serial_number.is_some() - || *is_ca != IsCa::NoCa - || !key_usages.is_empty() - || !extended_key_usages.is_empty() - || name_constraints.is_some() - || !crl_distribution_points.is_empty() - || *use_authority_key_identifier_extension - { + if self.serial_number.is_some() || self.use_authority_key_identifier_extension { return Err(Error::UnsupportedInCsr); } writer.write_sequence(|writer| { // Write version writer.next().write_u8(0); // Write issuer - write_distinguished_name(writer.next(), &distinguished_name); + write_distinguished_name(writer.next(), &self.distinguished_name); // Write subjectPublicKeyInfo pub_key.serialize_public_key_der(writer.next()); // Write extensions // According to the spec in RFC 2986, even if attributes are empty we need the empty attribute tag writer.next().write_tagged(Tag::context(0), |writer| { - if !subject_alt_names.is_empty() || !custom_extensions.is_empty() { - writer.write_sequence(|writer| { - let oid = ObjectIdentifier::from_slice(OID_PKCS_9_AT_EXTENSION_REQUEST); - writer.next().write_oid(&oid); - writer.next().write_set(|writer| { - writer.next().write_sequence(|writer| { - // Write subject_alt_names - self.write_subject_alt_names(writer.next()); - - // Write custom extensions - for ext in custom_extensions { - write_x509_extension( - writer.next(), - &ext.oid, - ext.critical, - |writer| writer.write_der(ext.content()), - ); - } - }); - }); - }); - } - }); - }); - Ok(()) + self.extensions(pub_key, None)?.write_csr_der(writer); + Ok(()) + }) + }) } fn write_cert( &self, @@ -956,182 +731,8 @@ impl CertificateParams { // Write subjectPublicKeyInfo pub_key.serialize_public_key_der(writer.next()); // write extensions - let should_write_exts = self.use_authority_key_identifier_extension - || !self.subject_alt_names.is_empty() - || !self.extended_key_usages.is_empty() - || self.name_constraints.iter().any(|c| !c.is_empty()) - || matches!(self.is_ca, IsCa::ExplicitNoCa) - || matches!(self.is_ca, IsCa::Ca(_)) - || !self.custom_extensions.is_empty(); - if should_write_exts { - writer.next().write_tagged(Tag::context(3), |writer| { - writer.write_sequence(|writer| { - if self.use_authority_key_identifier_extension { - write_x509_authority_key_identifier(writer.next(), ca) - } - // Write subject_alt_names - if !self.subject_alt_names.is_empty() { - self.write_subject_alt_names(writer.next()); - } - - // Write standard key usage - if !self.key_usages.is_empty() { - write_x509_extension(writer.next(), OID_KEY_USAGE, true, |writer| { - let mut bits: u16 = 0; - - for entry in self.key_usages.iter() { - // Map the index to a value - let index = match entry { - KeyUsagePurpose::DigitalSignature => 0, - KeyUsagePurpose::ContentCommitment => 1, - KeyUsagePurpose::KeyEncipherment => 2, - KeyUsagePurpose::DataEncipherment => 3, - KeyUsagePurpose::KeyAgreement => 4, - KeyUsagePurpose::KeyCertSign => 5, - KeyUsagePurpose::CrlSign => 6, - KeyUsagePurpose::EncipherOnly => 7, - KeyUsagePurpose::DecipherOnly => 8, - }; - - bits |= 1 << index; - } - - // Compute the 1-based most significant bit - let msb = 16 - bits.leading_zeros(); - let nb = if msb <= 8 { 1 } else { 2 }; - - let bits = bits.reverse_bits().to_be_bytes(); - - // Finally take only the bytes != 0 - let bits = &bits[..nb]; - - writer.write_bitvec_bytes(&bits, msb as usize) - }); - } - - // Write extended key usage - if !self.extended_key_usages.is_empty() { - write_x509_extension( - writer.next(), - OID_EXT_KEY_USAGE, - false, - |writer| { - writer.write_sequence(|writer| { - for usage in self.extended_key_usages.iter() { - let oid = ObjectIdentifier::from_slice(usage.oid()); - writer.next().write_oid(&oid); - } - }); - }, - ); - } - if let Some(name_constraints) = &self.name_constraints { - // If both trees are empty, the extension must be omitted. - if !name_constraints.is_empty() { - write_x509_extension( - writer.next(), - OID_NAME_CONSTRAINTS, - true, - |writer| { - writer.write_sequence(|writer| { - if !name_constraints.permitted_subtrees.is_empty() { - write_general_subtrees( - writer.next(), - 0, - &name_constraints.permitted_subtrees, - ); - } - if !name_constraints.excluded_subtrees.is_empty() { - write_general_subtrees( - writer.next(), - 1, - &name_constraints.excluded_subtrees, - ); - } - }); - }, - ); - } - } - if !self.crl_distribution_points.is_empty() { - write_x509_extension( - writer.next(), - OID_CRL_DISTRIBUTION_POINTS, - false, - |writer| { - writer.write_sequence(|writer| { - for distribution_point in &self.crl_distribution_points { - distribution_point.write_der(writer.next()); - } - }) - }, - ); - } - match self.is_ca { - IsCa::Ca(ref constraint) => { - // Write subject_key_identifier - write_x509_extension( - writer.next(), - OID_SUBJECT_KEY_IDENTIFIER, - false, - |writer| { - let key_identifier = self.key_identifier(pub_key); - writer.write_bytes(key_identifier.as_ref()); - }, - ); - // Write basic_constraints - write_x509_extension( - writer.next(), - OID_BASIC_CONSTRAINTS, - true, - |writer| { - writer.write_sequence(|writer| { - writer.next().write_bool(true); // cA flag - if let BasicConstraints::Constrained( - path_len_constraint, - ) = constraint - { - writer.next().write_u8(*path_len_constraint); - } - }); - }, - ); - }, - IsCa::ExplicitNoCa => { - // Write subject_key_identifier - write_x509_extension( - writer.next(), - OID_SUBJECT_KEY_IDENTIFIER, - false, - |writer| { - let key_identifier = self.key_identifier(pub_key); - writer.write_bytes(key_identifier.as_ref()); - }, - ); - // Write basic_constraints - write_x509_extension( - writer.next(), - OID_BASIC_CONSTRAINTS, - true, - |writer| { - writer.write_sequence(|writer| { - writer.next().write_bool(false); // cA flag - }); - }, - ); - }, - IsCa::NoCa => {}, - } - - // Write the custom extensions - for ext in &self.custom_extensions { - write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| { - writer.write_der(ext.content()) - }); - } - }); - }); - } + self.extensions(pub_key, Some(ca))? + .write_exts_der(writer.next()); Ok(()) }) } @@ -1175,6 +776,64 @@ impl CertificateParams { }) }) } + /// Returns the X.509 extensions that the [CertificateParams] describe, or an [Error] + /// if the described extensions are invalid. + /// + /// The returned extensions will include a subject sublic key identifier extension for the + /// provided [PublicKeyData]. + /// + /// If an issuer [Certificate] is provided, additional extensions specific to the issuer will + /// be included (e.g. the authority key identifier). + fn extensions( + &self, + pub_key: &K, + issuer: Option<&Certificate>, + ) -> Result { + let mut exts = Extensions::default(); + + if let Some(issuer) = issuer { + exts.add_extension(Box::new(ext::AuthorityKeyIdentifier::from(issuer)))?; + } + + if let Some(san_ext) = ext::SubjectAlternativeName::from_params(&self) { + exts.add_extension(Box::new(san_ext))?; + } + + if let Some(ku_ext) = ext::KeyUsage::from_params(&self) { + exts.add_extension(Box::new(ku_ext))?; + } + + if let Some(eku_ext) = ext::ExtendedKeyUsage::from_params(&self) { + exts.add_extension(Box::new(eku_ext))?; + } + + if let Some(name_constraints_ext) = ext::NameConstraints::from_params(&self) { + exts.add_extension(Box::new(name_constraints_ext))?; + } + + if let Some(crl_dps) = ext::CrlDistributionPoints::from_params(&self) { + exts.add_extension(Box::new(crl_dps))?; + } + + // RFC 5280 describes emitting a SKI as a MUST for CA certs and a SHOULD for EE, + // so we emit it unconditionally for all certs. + exts.add_extension(Box::new(ext::SubjectKeyIdentifier::from_params( + &self, pub_key, + )))?; + + if let Some(bc_ext) = ext::BasicConstraints::from_params(&self) { + exts.add_extension(Box::new(bc_ext))?; + } + + exts.add_extensions( + self.custom_extensions + .clone() + .into_iter() + .map(|x| Box::new(x) as Box), + )?; + + Ok(exts) + } } /// Whether the certificate is allowed to sign other certificates @@ -1230,12 +889,6 @@ pub struct NameConstraints { pub excluded_subtrees: Vec, } -impl NameConstraints { - fn is_empty(&self) -> bool { - self.permitted_subtrees.is_empty() && self.excluded_subtrees.is_empty() - } -} - /// One of the purposes contained in the [key usage](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3) extension #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum KeyUsagePurpose { @@ -1295,59 +948,6 @@ impl ExtendedKeyUsagePurpose { } } -/// 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() - } -} - /// Method to generate key identifiers from public keys. /// /// This allows choice over methods to generate key identifiers @@ -1447,33 +1047,6 @@ fn write_distinguished_name(writer: DERWriter, dn: &DistinguishedName) { }); } -fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[GeneralSubtree]) { - writer.write_tagged_implicit(Tag::context(tag), |writer| { - writer.write_sequence(|writer| { - for subtree in general_subtrees.iter() { - writer.next().write_sequence(|writer| { - writer - .next() - .write_tagged_implicit( - Tag::context(subtree.tag()), - |writer| match subtree { - GeneralSubtree::Rfc822Name(name) - | GeneralSubtree::DnsName(name) => writer.write_ia5_string(name), - GeneralSubtree::DirectoryName(name) => { - write_distinguished_name(writer, name) - }, - GeneralSubtree::IpAddress(subnet) => { - writer.write_bytes(&subnet.to_bytes()) - }, - }, - ); - // minimum must be 0 (the default) and maximum must be absent - }); - } - }); - }); -} - impl Certificate { /// Generates a new certificate from the given parameters. /// @@ -1567,57 +1140,6 @@ impl Certificate { } } -/// 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); - }) -} - -/// Serializes an X.509v3 authority key identifier extension according to RFC 5280. -fn write_x509_authority_key_identifier(writer: DERWriter, ca: &Certificate) { - // Write Authority Key Identifier - // RFC 5280 states: - // 'The keyIdentifier field of the authorityKeyIdentifier extension MUST - // be included in all certificates generated by conforming CAs to - // facilitate certification path construction. There is one exception; - // where a CA distributes its public key in the form of a "self-signed" - // certificate, the authority key identifier MAY be omitted.' - // In addition, for CRLs: - // 'Conforming CRL issuers MUST use the key identifier method, and MUST - // include this extension in all CRLs issued.' - write_x509_extension(writer, OID_AUTHORITY_KEY_IDENTIFIER, false, |writer| { - writer.write_sequence(|writer| { - writer - .next() - .write_tagged_implicit(Tag::context(0), |writer| { - writer.write_bytes(ca.get_key_identifier().as_ref()) - }) - }); - }); -} - #[cfg(feature = "zeroize")] impl zeroize::Zeroize for KeyPair { fn zeroize(&mut self) { diff --git a/rcgen/tests/generic.rs b/rcgen/tests/generic.rs index 582c6210..166a6153 100644 --- a/rcgen/tests/generic.rs +++ b/rcgen/tests/generic.rs @@ -93,7 +93,7 @@ mod test_convert_x509_subject_alternative_name { mod test_x509_custom_ext { use crate::util; - use rcgen::{Certificate, CustomExtension}; + use rcgen::{Certificate, Criticality, CustomExtension}; use x509_parser::oid_registry::asn1_rs; use x509_parser::prelude::{ FromDer, ParsedCriAttribute, X509Certificate, X509CertificationRequest, @@ -106,11 +106,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::from_oid_content( 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 = util::default_params();