diff --git a/src/lib.rs b/src/lib.rs index 0813ff3c..a5e73387 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,9 +140,13 @@ const OID_AUTHORITY_KEY_IDENTIFIER :&[u64] = &[2, 5, 29, 35]; const OID_EXT_KEY_USAGE :&[u64] = &[2, 5, 29, 37]; // id-ce-nameConstraints in -/// https://tools.ietf.org/html/rfc5280#section-4.2.1.10 +// https://tools.ietf.org/html/rfc5280#section-4.2.1.10 const OID_NAME_CONSTRAINTS :&[u64] = &[2, 5, 29, 30]; +// id-ce-cRLDistributionPoints in +// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13 +const OID_CRL_DISTRIBUTION_POINTS :&[u64] = &[2, 5, 29, 31]; + // id-pe-acmeIdentifier in // https://www.iana.org/assignments/smi-numbers/smi-numbers.xhtml#smi-numbers-1.3.6.1.5.5.7.1 const OID_PE_ACME :&[u64] = &[1, 3, 6, 1, 5, 5, 7, 1, 31]; @@ -733,6 +737,12 @@ pub struct CertificateParams { pub key_usages :Vec, pub extended_key_usages :Vec, pub name_constraints :Option, + /// An optional list of certificate revocation list (CRL) distribution points as described + /// in RFC 5280 Section 4.2.1.13[^1]. Each distribution point contains one or more URIs where + /// an up-to-date CRL with scope including this certificate can be retrieved. + /// + /// [^1]: + pub crl_distribution_points :Vec, pub custom_extensions :Vec, /// The certificate's key pair, a new random key pair will be generated if this is `None` pub key_pair :Option, @@ -762,6 +772,7 @@ impl Default for CertificateParams { key_usages : Vec::new(), extended_key_usages : Vec::new(), name_constraints : None, + crl_distribution_points : Vec::new(), custom_extensions : Vec::new(), key_pair : None, use_authority_key_identifier_extension : false, @@ -1020,6 +1031,7 @@ impl CertificateParams { key_usages, extended_key_usages, name_constraints, + crl_distribution_points, custom_extensions, key_pair, use_authority_key_identifier_extension, @@ -1037,6 +1049,7 @@ impl CertificateParams { || !key_usages.is_empty() || !extended_key_usages.is_empty() || name_constraints.is_some() + || !crl_distribution_points.is_empty() || *use_authority_key_identifier_extension { return Err(RcgenError::UnsupportedInCsr); @@ -1230,6 +1243,15 @@ impl CertificateParams { }); } } + 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 @@ -1381,6 +1403,44 @@ impl NameConstraints { } } +/// 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). +#[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, +} + +impl CrlDistributionPoint { + fn write_der(&self, writer :DERWriter) { + // DistributionPoint SEQUENCE + writer.write_sequence(|writer| { + write_distribution_point_name_uris(writer.next(), &self.uris); + }); + } +} + +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) + }); + } + }) + }); + }); + }); +} + /// 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 { diff --git a/tests/generic.rs b/tests/generic.rs index 042a11e2..6a549904 100644 --- a/tests/generic.rs +++ b/tests/generic.rs @@ -123,4 +123,58 @@ mod test_x509_parser_crl { // We should be able to verify the CRL signature with the issuer. assert!(x509_crl.verify_signature(&x509_issuer.public_key()).is_ok()); } -} \ No newline at end of file +} + +#[cfg(feature = "x509-parser")] +mod test_parse_crl_dps { + use x509_parser::extensions::{DistributionPointName, ParsedExtension}; + use crate::util; + + #[test] + fn parse_crl_dps() { + // Generate and parse a certificate that includes two CRL distribution points. + let der = util::cert_with_crl_dps(); + let (_, parsed_cert) = x509_parser::parse_x509_certificate(&der).unwrap(); + + // We should find a CRL DP extension was parsed. + let crl_dps = parsed_cert.get_extension_unique(&x509_parser::oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS) + .expect("malformed CRL distribution points extension") + .expect("missing CRL distribution points extension"); + + // The extension should not be critical. + assert!(!crl_dps.critical); + + // We should be able to parse the definition. + let crl_dps = match crl_dps.parsed_extension() { + ParsedExtension::CRLDistributionPoints(crl_dps) => crl_dps, + _ => panic!("unexpected parsed extension type") + }; + + // There should be two DPs. + assert_eq!(crl_dps.points.len(), 2); + + // Each distribution point should only include a distribution point name holding a sequence + // of general names. + let general_names = crl_dps.points.iter().flat_map(|dp| { + // We shouldn't find a cRLIssuer or onlySomeReasons field. + assert!(dp.crl_issuer.is_none()); + assert!(dp.reasons.is_none()); + + match dp.distribution_point.as_ref().expect("missing distribution point name") { + DistributionPointName::FullName(general_names) => general_names.iter(), + DistributionPointName::NameRelativeToCRLIssuer(_) => panic!("unexpected name relative to cRL issuer") + } + }).collect::>(); + + // All of the general names should be URIs. + let uris = general_names.iter().map(|general_name| { + match general_name { + x509_parser::extensions::GeneralName::URI(uri) => *uri, + _ => panic!("unexpected general name type") + } + }).collect::>(); + + // We should find the expected URIs. + assert_eq!(uris, &["http://example.com/crl.der", "http://crls.example.com/1234", "ldap://example.com/crl.der"]); + } +} diff --git a/tests/openssl.rs b/tests/openssl.rs index eda91f82..203e70a6 100644 --- a/tests/openssl.rs +++ b/tests/openssl.rs @@ -1,5 +1,4 @@ -use rcgen::{Certificate, NameConstraints, GeneralSubtree, IsCa, - BasicConstraints, CertificateParams, DnType, DnValue}; +use rcgen::{Certificate, NameConstraints, GeneralSubtree, IsCa, BasicConstraints, CertificateParams, DnType, DnValue}; use openssl::pkey::PKey; use openssl::x509::{CrlStatus, X509, X509Crl, X509Req, X509StoreContext}; use openssl::x509::store::{X509StoreBuilder, X509Store}; @@ -426,4 +425,35 @@ fn test_openssl_crl_parse() { // We should be able to verify the CRL signature with the issuer's public key. let issuer_pkey = openssl_issuer.public_key().unwrap(); assert!(openssl_crl.verify(&issuer_pkey).expect("failed to verify CRL signature")); -} \ No newline at end of file +} + +#[test] +fn test_openssl_crl_dps_parse() { + // Generate and parse a certificate that includes two CRL distribution points. + let der = util::cert_with_crl_dps(); + let cert = X509::from_der(&der).expect("failed to parse cert DER"); + + // We should find the CRL DPs extension. + let dps = cert.crl_distribution_points().expect("missing crl distribution points extension"); + assert!(!dps.is_empty()); + + // We should find two distribution points, each with a distribution point name containing + // a full name sequence of general names. + let general_names = dps.iter().flat_map(|dp| + dp.distpoint() + .expect("distribution point missing distribution point name") + .fullname() + .expect("distribution point name missing general names") + .iter() + ) + .collect::>(); + + // Each general name should be a URI name. + let uris = general_names.iter().map(|general_name| + general_name.uri().expect("general name is not a directory name") + ) + .collect::>(); + + // We should find the expected URIs. + assert_eq!(uris, &["http://example.com/crl.der", "http://crls.example.com/1234", "ldap://example.com/crl.der"]); +} diff --git a/tests/util.rs b/tests/util.rs index 4dcf3438..4143d6ea 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,5 +1,5 @@ use time::{Duration, OffsetDateTime}; -use rcgen::{BasicConstraints, Certificate, CertificateParams, CertificateRevocationList}; +use rcgen::{BasicConstraints, Certificate, CertificateParams, CertificateRevocationList, CrlDistributionPoint}; use rcgen::{CertificateRevocationListParams, DnType, IsCa, KeyIdMethod}; use rcgen::{KeyUsagePurpose, PKCS_ECDSA_P256_SHA256, RevocationReason, RevokedCertParams, SerialNumber}; @@ -99,3 +99,19 @@ pub fn test_crl() -> (CertificateRevocationList, Certificate) { (crl, issuer) } + +#[allow(unused)] // Used by openssl + x509-parser features. +pub fn cert_with_crl_dps() -> Vec { + let mut params = default_params(); + params.crl_distribution_points = vec![ + CrlDistributionPoint{ + uris: vec!["http://example.com/crl.der".to_string(), "http://crls.example.com/1234".to_string()], + }, + CrlDistributionPoint{ + uris: vec!["ldap://example.com/crl.der".to_string()], + } + ]; + + let cert = Certificate::from_params(params).unwrap(); + cert.serialize_der().unwrap() +} diff --git a/tests/webpki.rs b/tests/webpki.rs index 8f4c41df..ad0fd5fe 100644 --- a/tests/webpki.rs +++ b/tests/webpki.rs @@ -526,3 +526,11 @@ fn test_webpki_crl_revoke() { ); assert!(matches!(result, Err(webpki::Error::CertRevoked))); } + +#[test] +fn test_webpki_cert_crl_dps() { + let der = util::cert_with_crl_dps(); + webpki::EndEntityCert::try_from(der.as_ref()).expect("failed to parse cert with CRL DPs ext"); + // Webpki doesn't expose the parsed CRL distribution extension, so we can't interrogate that + // it matches the expected form. See `openssl.rs` for more extensive coverage. +}