diff --git a/src/lib.rs b/src/lib.rs index a5e73387..6cd8389c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,10 @@ const OID_CRL_REASONS :&[u64] = &[2, 5, 29, 21]; // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.2 const OID_CRL_INVALIDITY_DATE :&[u64] = &[2, 5, 29, 24]; +// id-ce-issuingDistributionPoint +// https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5 +const OID_CRL_ISSUING_DISTRIBUTION_POINT :&[u64] = &[2, 5, 29, 28]; + #[cfg(feature = "pem")] const ENCODE_CONFIG: pem::EncodeConfig = match cfg!(target_family = "windows") { true => pem::EncodeConfig { line_ending: pem::LineEnding::CRLF }, @@ -680,6 +684,7 @@ impl CertificateSigningRequest { /// this_update: date_time_ymd(2023, 06, 17), /// next_update: date_time_ymd(2024, 06, 17), /// crl_number: SerialNumber::from(1234), +/// issuing_distribution_point: None, /// revoked_certs: vec![revoked_cert], /// alg: &PKCS_ECDSA_P256_SHA256, /// key_identifier_method: KeyIdMethod::Sha256, @@ -1404,7 +1409,8 @@ 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). +/// [distribution points extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13) or +/// a CRL's [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5) #[derive(Debug, PartialEq, Eq, Clone)] pub struct CrlDistributionPoint { /// One or more URI distribution point names, indicating a place the current CRL can @@ -1596,6 +1602,11 @@ pub struct CertificateRevocationListParams { pub next_update :OffsetDateTime, /// A monotonically increasing sequence number for a given CRL scope and issuer. pub crl_number :SerialNumber, + /// An optional CRL extension identifying the CRL distribution point and scope for a + /// particular CRL as described in RFC 5280 Section 5.2.5[^1]. + /// + /// [^1]: + pub issuing_distribution_point :Option, /// A list of zero or more parameters describing revoked certificates included in the CRL. pub revoked_certs :Vec, /// Signature algorithm to use when signing the serialized CRL. @@ -1693,6 +1704,13 @@ impl CertificateRevocationListParams { 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); + }); + } }); }); @@ -1701,6 +1719,54 @@ impl CertificateRevocationListParams { } } +/// 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). +pub struct CrlIssuingDistributionPoint { + /// The CRL's distribution point, containing a sequence of URIs the CRL can be retrieved from. + pub distribution_point :CrlDistributionPoint, + /// An optional description of the CRL's scope. If omitted, the CRL may contain + /// both user certs and CA certs. + pub scope :Option, +} + +impl CrlIssuingDistributionPoint { + fn write_der(&self, writer :DERWriter) { + // IssuingDistributionPoint SEQUENCE + writer.write_sequence(|writer| { + // distributionPoint [0] DistributionPointName OPTIONAL + write_distribution_point_name_uris(writer.next(), &self.distribution_point.uris); + + // -- at most one of onlyContainsUserCerts, onlyContainsCACerts, + // -- and onlyContainsAttributeCerts may be set to TRUE. + if let Some(scope) = self.scope { + match scope { + // onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE, + CrlScope::UserCertsOnly => { + writer.next().write_tagged_implicit(Tag::context(1), |writer| { + writer.write_bool(true); + }); + } + // onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE, + CrlScope::CaCertsOnly => { + writer.next().write_tagged_implicit(Tag::context(2), |writer| { + writer.write_bool(true); + }); + } + } + } + }); + } +} + +/// Describes the scope of a CRL for an issuing distribution point extension. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum CrlScope { + /// The CRL contains only end-entity user certificates. + UserCertsOnly, + /// The CRL contains only CA certificates. + CaCertsOnly, +} + /// Parameters used for describing a revoked certificate included in a [`CertificateRevocationList`]. pub struct RevokedCertParams { /// Serial number identifying the revoked certificate. diff --git a/tests/botan.rs b/tests/botan.rs index 9c745166..875a0c7f 100644 --- a/tests/botan.rs +++ b/tests/botan.rs @@ -232,6 +232,7 @@ fn test_botan_crl_parse() { this_update: now, next_update: now + Duration::weeks(1), crl_number: rcgen::SerialNumber::from(1234), + issuing_distribution_point: None, revoked_certs: vec![RevokedCertParams{ serial_number: ee.get_params().serial_number.clone().unwrap(), revocation_time: now, diff --git a/tests/generic.rs b/tests/generic.rs index 9537efff..f91c9204 100644 --- a/tests/generic.rs +++ b/tests/generic.rs @@ -109,7 +109,7 @@ mod test_x509_parser_crl { crl.get_params().this_update.unix_timestamp()); assert_eq!(x509_crl.next_update().unwrap().to_datetime().unix_timestamp(), crl.get_params().next_update.unix_timestamp()); - // TODO(XXX): Waiting on https://github.com/rusticata/x509-parser/pull/144 + // TODO(XXX): Waiting on x509-parser 0.15.1 to be released. // let crl_number = BigUint::from_bytes_be(crl.get_params().crl_number.as_ref()); // assert_eq!(x509_crl.crl_number().unwrap(), &crl_number); @@ -120,6 +120,13 @@ mod test_x509_parser_crl { let (_, reason_code) = x509_revoked_cert.reason_code().unwrap(); assert_eq!(reason_code.0, revoked_cert.reason_code.unwrap() as u8); + // The issuing distribution point extension should be present and marked critical. + let issuing_dp_ext = x509_crl.extensions().iter() + .find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_ISSUER_DISTRIBUTION_POINT) + .expect("failed to find issuing distribution point extension"); + assert!(issuing_dp_ext.critical); + // TODO(XXX): x509-parser does not yet parse the CRL issuing DP extension for further examination. + // We should be able to verify the CRL signature with the issuer. assert!(x509_crl.verify_signature(&x509_issuer.public_key()).is_ok()); } diff --git a/tests/util.rs b/tests/util.rs index 4143d6ea..c8f1ebad 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,5 +1,6 @@ use time::{Duration, OffsetDateTime}; -use rcgen::{BasicConstraints, Certificate, CertificateParams, CertificateRevocationList, CrlDistributionPoint}; +use rcgen::{BasicConstraints, Certificate, CertificateParams}; +use rcgen::{CertificateRevocationList, CrlDistributionPoint, CrlIssuingDistributionPoint, CrlScope}; use rcgen::{CertificateRevocationListParams, DnType, IsCa, KeyIdMethod}; use rcgen::{KeyUsagePurpose, PKCS_ECDSA_P256_SHA256, RevocationReason, RevokedCertParams, SerialNumber}; @@ -91,6 +92,10 @@ pub fn test_crl() -> (CertificateRevocationList, Certificate) { this_update: now, next_update: next_week, crl_number: SerialNumber::from(1234), + issuing_distribution_point: Some(CrlIssuingDistributionPoint{ + distribution_point: CrlDistributionPoint { uris: vec!["http://example.com/crl".to_string()] }, + scope: Some(CrlScope::UserCertsOnly), + }), revoked_certs: vec![revoked_cert], alg: &PKCS_ECDSA_P256_SHA256, key_identifier_method: KeyIdMethod::Sha256, diff --git a/tests/webpki.rs b/tests/webpki.rs index ad0fd5fe..d673a62a 100644 --- a/tests/webpki.rs +++ b/tests/webpki.rs @@ -503,6 +503,7 @@ fn test_webpki_crl_revoke() { this_update: now, next_update: now + Duration::weeks(1), crl_number: rcgen::SerialNumber::from(1234), + issuing_distribution_point: None, revoked_certs: vec![RevokedCertParams{ serial_number: ee.get_params().serial_number.clone().unwrap(), revocation_time: now,