From c422302544a444b3989f8c451243085c6f02c7cb Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Thu, 17 Aug 2023 13:40:04 -0400 Subject: [PATCH] lib: add CRL issuing distribution point ext. support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extends rcgen to allow generating certificate revocation lists (CRLs) that contain an RFC 5280 CRL issuing distribution point extension. This is a useful mechanism for helping ensure CRL coverage when performing revocation checks, and is newly supported by rustls/webpki. See this upstream webpki issue[0] and RFC 5280 ยง5.2.5[1] for more background. Using the new optional `issuing_distribution_point` field of the `CertificateRevocationListParams` struct it's possible to encode a issuing distribution point specifying URI general names where up-to-date CRL information for the CRL can be found. Similar to existing rcgen CRL generation, the support for this extension is not extensive, but instead tailored towards usage in the web PKI with a RFC 5280 profile. Notably this means: * There's no support for specifying the `indirectCRL` bool - neither rcgen's existing CRL generation code or webpki's parsing/validation supports these. * There's no support for specifying the `onlySomeReasons` field - RFC 5280 "RECOMMENDS against segmenting CRLs by reason code". * There's no support for specifying a `onlyContainsAttributeCerts` bool - RFC 5280 says "Conforming CRLs issuers MUST set the onlyContainsAttributeCerts boolean to FALSE." and the DER encoding of 'false' requires eliding the value. * There's no support for specifying a 'nameRelativeToCrlIssuer' in the DP name instead of a sequence of general names for similar reasons as above: 5280 says: "Conforming CAs SHOULD NOT use nameRelativeToCRLIssuer to specify distribution point names." * There's no support for specifying general names of type other than URI within a DP name's full name. Other name types either don't make sense in the context of this extension, or are rarely useful in practice (e.g. directory name). Compared to test coverage of the certificate CRL distribution points extension this commit can't offer too much. OpenSSL (openssl-rs) doesn't expose arbitrary CRL extensions, or the issuing distribution point. The `x509-parser` crate can pull out the extension, but doesn't decompose the value (I may attempt to land code for this upstream in the future, stay tuned). Webpki (v/0.102.0-alpha.0) recognizes this extension for use during revocation checking, but doesn't expose it externally. Botan's rust bindings do not recognize the extension or offer a way to pull out arbitrary extensions, so no test coverage is added there. [0] rustls/webpki#121 [1] https://www.rfc-editor.org/rfc/rfc5280#section-5.2.5 --- src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++- tests/botan.rs | 1 + tests/generic.rs | 9 ++++++- tests/util.rs | 7 +++++- tests/webpki.rs | 1 + 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a5e73387..bcfdcdf5 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,49 @@ 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 { + let tag = match scope { + // onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE, + CrlScope::UserCertsOnly => Tag::context(1), + // onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE, + CrlScope::CaCertsOnly => Tag::context(2), + }; + writer.next().write_tagged_implicit(tag, |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 6a549904..33cc57e2 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: 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: 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,