diff --git a/Cargo.lock b/Cargo.lock index 9ca2434b..9a9071bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,36 @@ dependencies = [ "libc", ] +[[package]] +name = "certval" +version = "0.1.4" +source = "git+https://github.com/wireapp/rust-pki.git?branch=wire/force-send-sync#3123556b689de297af5ce12b1eefaef6e4acb75a" +dependencies = [ + "cfg-if", + "ciborium", + "cms", + "const-oid", + "der 0.7.8", + "ecdsa", + "flagset", + "lazy_static", + "log", + "ndarray", + "p256", + "p384", + "pem-rfc7468 0.7.0", + "pkiprocmacros", + "readonly", + "rsa 0.9.6", + "serde", + "sha-1", + "sha2", + "spki 0.7.3", + "subtle-encoding", + "x509-cert", + "x509-ocsp", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -378,6 +408,33 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -438,6 +495,18 @@ dependencies = [ "cc", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid", + "der 0.7.8", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "coarsetime" version = "0.1.33" @@ -477,15 +546,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -1308,6 +1377,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1820,6 +1895,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.7.1" @@ -1864,6 +1949,19 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -1920,6 +2018,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2428,6 +2535,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +[[package]] +name = "pkiprocmacros" +version = "0.1.2" +source = "git+https://github.com/wireapp/rust-pki.git?branch=wire/force-send-sync#3123556b689de297af5ce12b1eefaef6e4acb75a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "platforms" version = "3.3.0" @@ -2645,6 +2762,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rcgen" version = "0.9.2" @@ -2656,6 +2779,17 @@ dependencies = [ "yasna", ] +[[package]] +name = "readonly" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25d631e41bfb5fdcde1d4e2215f62f7f0afa3ff11e26563765bd6ea1d229aeb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2957,6 +3091,7 @@ dependencies = [ "p384", "pem 3.0.3", "rusty-jwt-tools", + "rusty-x509-check", "serde", "serde_json", "signature", @@ -3023,6 +3158,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "rusty-x509-check" +version = "0.6.1" +dependencies = [ + "certval", + "const-oid", + "flagset", + "fluvio-wasm-timer", + "thiserror", + "x509-cert", +] + [[package]] name = "ryu" version = "1.0.16" @@ -3261,6 +3408,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -3395,6 +3553,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" + [[package]] name = "syn" version = "1.0.109" @@ -4259,6 +4423,17 @@ dependencies = [ "tls_codec", ] +[[package]] +name = "x509-ocsp" +version = "0.2.1" +source = "git+https://github.com/RustCrypto/formats#fb8db96b54bf662b1495ef3462b39c6a6cee936f" +dependencies = [ + "const-oid", + "der 0.7.8", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 1cd614cd..36ede6d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["e2e-identity", "jwt", "ffi", "cli", "acme"] +members = ["e2e-identity", "jwt", "ffi", "cli", "acme", "x509-check"] resolver = "2" [patch.crates-io.biscuit] diff --git a/acme/Cargo.toml b/acme/Cargo.toml index 3da5b6f3..cbbd3ec4 100644 --- a/acme/Cargo.toml +++ b/acme/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0" thiserror = "1.0" rusty-jwt-tools = { version = "0.7.1", path = "../jwt" } jwt-simple = { workspace = true } +rusty-x509-check = { version = "0.6.1", path = "../x509-check" } base64 = "0.21" url = { version = "2.5", features = ["serde"] } time = { version = "0.3", features = ["serde", "serde-well-known", "wasm-bindgen"] } @@ -28,7 +29,6 @@ p256 = "0.13" p384 = "0.13" pem = "3.0" getrandom = { version = "0.2.8", features = ["js"] } - fluvio-wasm-timer = "0.2" [dev-dependencies] diff --git a/acme/src/certificate.rs b/acme/src/certificate.rs index 97a21c79..6279730e 100644 --- a/acme/src/certificate.rs +++ b/acme/src/certificate.rs @@ -33,7 +33,7 @@ impl RustyAcme { let cert = x509_cert::Certificate::from_der(cert_pem.contents())?; // only verify that leaf has the right identity fields if i == 0 { - Self::verify_leaf_certificate(order.clone(), cert)?; + Self::verify_leaf_certificate(&order, cert)?; } acc.push(cert_pem.contents().to_vec()); Ok(acc) @@ -42,11 +42,15 @@ impl RustyAcme { /// Ensure that the generated certificate matches our expectations (i.e. that the acme server is configured the right way) /// We verify that the fields in the certificate match the ones in the ACME order - fn verify_leaf_certificate(mut order: AcmeOrder, cert: Certificate) -> RustyAcmeResult<()> { + fn verify_leaf_certificate(order: &AcmeOrder, cert: Certificate) -> RustyAcmeResult<()> { // TODO: verify that cert is signed by enrollment.sign_kp let cert_identity = cert.extract_identity()?; - let identifier = order.identifiers.pop().ok_or(RustyAcmeError::ImplementationError)?; - let identifier = identifier.to_wire_identifier()?; + let identifier = order + .identifiers + .first() + .ok_or(RustyAcmeError::ImplementationError)? + .to_wire_identifier()?; + let invalid_client_id = ClientId::try_from_qualified(&cert_identity.client_id)? != ClientId::try_from_uri(&identifier.client_id)?; if invalid_client_id { diff --git a/acme/src/error.rs b/acme/src/error.rs index e0614497..40b92b8a 100644 --- a/acme/src/error.rs +++ b/acme/src/error.rs @@ -13,6 +13,9 @@ pub enum RustyAcmeError { /// Error while building a JWT #[error(transparent)] JwtError(#[from] rusty_jwt_tools::prelude::RustyJwtError), + /// Error related to various X509 processing facilities/tools/checks + #[error(transparent)] + X509CheckError(#[from] rusty_x509_check::RustyX509CheckError), /// Failed mapping an ASN.1 ObjectIdentifier #[error(transparent)] OidError(#[from] x509_cert::der::oid::Error), @@ -58,6 +61,7 @@ pub enum RustyAcmeError { /// Error while finalizing an order #[error(transparent)] FinalizeError(#[from] crate::finalize::AcmeFinalizeError), + /// UTF-8 parsing error #[error(transparent)] Utf8(#[from] std::str::Utf8Error), /// Invalid/incomplete certificate diff --git a/acme/src/identity/mod.rs b/acme/src/identity/mod.rs index 9a06d155..28cce2dc 100644 --- a/acme/src/identity/mod.rs +++ b/acme/src/identity/mod.rs @@ -1,11 +1,11 @@ use x509_cert::der::Decode as _; use rusty_jwt_tools::prelude::*; +use rusty_x509_check::IdentityStatus; use crate::error::CertificateError; use crate::prelude::*; -mod status; mod thumbprint; #[derive(Debug, Clone)] @@ -18,16 +18,6 @@ pub struct WireIdentity { pub thumbprint: String, } -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum IdentityStatus { - /// All is fine - Valid, - /// The Certificate is expired - Expired, - /// The Certificate is revoked - Revoked, -} - pub trait WireIdentityReader { /// Verifies a proof of identity, may it be a x509 certificate (or a Verifiable Presentation (later)). /// We do not verify anything else e.g. expiry, it is left to MLS implementation @@ -44,7 +34,7 @@ impl WireIdentityReader for x509_cert::Certificate { fn extract_identity(&self) -> RustyAcmeResult { let (client_id, handle) = try_extract_san(&self.tbs_certificate)?; let (display_name, domain) = try_extract_subject(&self.tbs_certificate)?; - let status = status::extract_status(&self.tbs_certificate); + let status = IdentityStatus::from_cert(self); let thumbprint = thumbprint::try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate)?; Ok(WireIdentity { @@ -105,9 +95,10 @@ fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(Str let mut subjects = cert.subject.0.iter().flat_map(|n| n.0.iter()); subjects.try_for_each(|s| -> RustyAcmeResult<()> { - if s.oid.as_bytes() == oid_registry::OID_X509_ORGANIZATION_NAME.as_bytes() { + let oid = s.oid.as_bytes(); + if oid == oid_registry::OID_X509_ORGANIZATION_NAME.as_bytes() { domain = Some(std::str::from_utf8(s.value.value())?); - } else if s.oid.as_bytes() == oid_registry::OID_X509_COMMON_NAME.as_bytes() { + } else if oid == oid_registry::OID_X509_COMMON_NAME.as_bytes() { display_name = Some(std::str::from_utf8(s.value.value())?); } Ok(()) @@ -123,8 +114,10 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, let san = extensions .iter() - .find(|e| e.extn_id.as_bytes() == oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME.as_bytes()) - .map(|e| x509_cert::ext::pkix::SubjectAltName::from_der(e.extn_value.as_bytes())) + .find_map(|e| { + (e.extn_id.as_bytes() == oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME.as_bytes()) + .then(|| x509_cert::ext::pkix::SubjectAltName::from_der(e.extn_value.as_bytes())) + }) .transpose()? .ok_or(CertificateError::InvalidFormat)?; diff --git a/acme/src/identity/status.rs b/acme/src/identity/status.rs deleted file mode 100644 index cbba5a6e..00000000 --- a/acme/src/identity/status.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::IdentityStatus; - -pub(crate) fn extract_status(cert: &x509_cert::TbsCertificate) -> IdentityStatus { - if is_revoked(cert) { - IdentityStatus::Revoked - } else if !is_time_valid(cert) { - IdentityStatus::Expired - } else { - IdentityStatus::Valid - } -} - -fn is_time_valid(cert: &x509_cert::TbsCertificate) -> bool { - // 'not_before' < now < 'not_after' - let x509_cert::time::Validity { not_before, not_after } = cert.validity; - - let now = fluvio_wasm_timer::SystemTime::now(); - let Ok(now) = now.duration_since(fluvio_wasm_timer::UNIX_EPOCH) else { - return false; - }; - - let is_nbf = now >= not_before.to_unix_duration(); - let is_naf = now < not_after.to_unix_duration(); - is_nbf && is_naf -} - -// TODO -fn is_revoked(_cert: &x509_cert::TbsCertificate) -> bool { - false -} diff --git a/acme/src/lib.rs b/acme/src/lib.rs index 5d5aa7d7..0b53a2e2 100644 --- a/acme/src/lib.rs +++ b/acme/src/lib.rs @@ -20,11 +20,13 @@ pub mod prelude { pub use error::{RustyAcmeError, RustyAcmeResult}; pub use finalize::AcmeFinalize; pub use identifier::{AcmeIdentifier, WireIdentifier}; - pub use identity::{IdentityStatus, WireIdentity, WireIdentityReader}; + pub use identity::{WireIdentity, WireIdentityReader}; pub use jws::AcmeJws; pub use order::AcmeOrder; + pub use rusty_x509_check as x509; pub use directory::AcmeDirectory; + #[cfg(all(feature = "docker", not(target_family = "wasm")))] pub use docker::*; } diff --git a/deny.toml b/deny.toml index ac90cea4..0b72bfab 100644 --- a/deny.toml +++ b/deny.toml @@ -6,6 +6,7 @@ targets = [ [advisories] # TODO: use more conservative values when we come close to production vulnerability = "warn" +ignore = ["RUSTSEC-2023-0071"] [licenses] unlicensed = "deny" @@ -24,6 +25,14 @@ license-files = [ { path = "LICENSE", hash = 0xbd0eed23 } ] +[[licenses.clarify]] +name = "pkiprocmacros" +expression = "Apache-2.0 OR MIT" +license-files = [ + { path = "../certval/LICENSE-APACHE", hash = 0x001c7e6c }, + { path = "../certval/LICENSE-MIT", hash = 0x001c7e6c } +] + [[licenses.clarify]] name = "webpki" expression = "ISC" @@ -38,6 +47,11 @@ license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] private = [ "https://github.com/wireapp", ] +allow-git = [ + "https://github.com/carl-wallace/rust-pki", + "https://github.com/RustCrypto/formats", + "https://github.com/briansmith/ring" +] [bans] multiple-versions = "allow" diff --git a/e2e-identity/src/lib.rs b/e2e-identity/src/lib.rs index 31be27d6..dc20a407 100644 --- a/e2e-identity/src/lib.rs +++ b/e2e-identity/src/lib.rs @@ -14,10 +14,12 @@ mod types; pub mod prelude { pub use rusty_acme::prelude::{ - AcmeDirectory, IdentityStatus, RustyAcme, RustyAcmeError, WireIdentity, WireIdentityReader, + x509::IdentityStatus, AcmeDirectory, RustyAcme, RustyAcmeError, WireIdentity, WireIdentityReader, }; pub use rusty_jwt_tools::prelude::{ClientId as E2eiClientId, HashAlgorithm, JwsAlgorithm, RustyJwtError}; + pub use rusty_acme::prelude::x509; + #[cfg(feature = "identity-builder")] pub use super::builder::*; pub use super::error::{E2eIdentityError, E2eIdentityResult}; diff --git a/x509-check/Cargo.toml b/x509-check/Cargo.toml new file mode 100644 index 00000000..87d48abb --- /dev/null +++ b/x509-check/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rusty-x509-check" +description = "Utilities to assert a X.509 certificate's validity and revocation status" +version = "0.6.1" +edition = "2021" +repository = "https://github.com/wireapp/rusty-jwt-tools" +license = "MPL-2.0" +publish = false + +[dependencies] +fluvio-wasm-timer = "0.2" +flagset = "0.4" +x509-cert = "0.2" +const-oid = "0.9" +thiserror = "1.0" + +[dependencies.certval] +git = "https://github.com/wireapp/rust-pki.git" +branch = "wire/force-send-sync" +package = "certval" +default-features = false +features = ["revocation"] diff --git a/x509-check/src/lib.rs b/x509-check/src/lib.rs new file mode 100644 index 00000000..1ba45d54 --- /dev/null +++ b/x509-check/src/lib.rs @@ -0,0 +1,105 @@ +pub mod revocation; + +#[derive(Debug, thiserror::Error)] +pub enum RustyX509CheckError { + /// DER de/serialization error + #[error(transparent)] + DerError(#[from] x509_cert::der::Error), + /// Poisoned lock error + #[error("A lock has been poisoned and cannot be recovered from.")] + LockPoisonError, + /// Error for when the current UNIX epoch time cannot be determined. + #[error("Cannot determine current UNIX epoch")] + CannotDetermineCurrentTime, + /// Certificate / revocation validation error + #[error("Certificate validation error: {0}")] + CertValError(certval::Error), + /// Error when we have no idea what the cert status is + #[error("Something went wrong, we cannot determine if this certificate is OK. You might want to ignore this")] + CannotDetermineVerificationStatus, +} + +impl From for RustyX509CheckError { + fn from(value: certval::Error) -> Self { + RustyX509CheckError::CertValError(value) + } +} + +pub type RustyX509CheckResult = Result; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum IdentityStatus { + /// All is fine + Valid, + /// The Certificate is expired + Expired, + /// The Certificate is revoked + Revoked, +} + +impl IdentityStatus { + pub fn from_cert(cert: &x509_cert::Certificate) -> Self { + if !is_time_valid(&cert.tbs_certificate) { + IdentityStatus::Expired + } else if is_revoked(cert) { + IdentityStatus::Revoked + } else { + IdentityStatus::Valid + } + } +} + +fn is_time_valid(cert: &x509_cert::TbsCertificate) -> bool { + // 'not_before' < now < 'not_after' + let x509_cert::time::Validity { not_before, not_after } = cert.validity; + + let now = fluvio_wasm_timer::SystemTime::now(); + let Ok(now) = now.duration_since(fluvio_wasm_timer::UNIX_EPOCH) else { + return false; + }; + + let is_nbf = now >= not_before.to_unix_duration(); + let is_naf = now < not_after.to_unix_duration(); + is_nbf && is_naf +} + +/// Extracts the CRL Distribution points that are FullName URIs from the Certificate +pub fn extract_crl_uris( + cert: &x509_cert::Certificate, +) -> RustyX509CheckResult>> { + use certval::validator::{PDVCertificate, PDVExtension}; + use x509_cert::ext::pkix::name::{DistributionPointName, GeneralName}; + + Ok(PDVCertificate::try_from(cert.clone())? + .parsed_extensions + .get(&const_oid::db::rfc5280::ID_CE_CRL_DISTRIBUTION_POINTS) + .and_then(|ext| { + let PDVExtension::CrlDistributionPoints(crl_distribution_points) = ext else { + return None; + }; + + Some(crl_distribution_points.0.iter().fold( + Default::default(), + |mut set: std::collections::HashSet, dp| { + if let Some(DistributionPointName::FullName(dp_full_names)) = dp.distribution_point.as_ref() { + for gn in dp_full_names.iter() { + if let GeneralName::UniformResourceIdentifier(uri) = gn { + set.insert(uri.to_string()); + } + } + } + + set + }, + )) + })) +} + +/// Extracts the expiration date from a parsed CRL +pub fn extract_expiration_from_crl(crl: &x509_cert::crl::CertificateList) -> Option { + crl.tbs_cert_list.next_update.map(|t| t.to_unix_duration().as_secs()) +} + +pub fn is_revoked(_cert: &x509_cert::Certificate) -> bool { + false +} diff --git a/x509-check/src/revocation.rs b/x509-check/src/revocation.rs new file mode 100644 index 00000000..a8d05601 --- /dev/null +++ b/x509-check/src/revocation.rs @@ -0,0 +1,271 @@ +#![allow(dead_code)] + +use certval::{ + check_revocation, get_validation_status, populate_5280_pki_environment, set_require_ta_store, set_time_of_interest, + validate_path_rfc5280, + validator::{path_validator::check_validity, PDVCertificate}, + verify_signatures, CertSource, CertVector, CertificationPathResults, CertificationPathSettings, + ExtensionProcessing, TaSource, EXTS_OF_INTEREST, +}; + +use x509_cert::der::{Decode, DecodePem, Encode}; + +use crate::{revocation::cache::RevocationCache, RustyX509CheckError, RustyX509CheckResult}; +use crl_store::CrlStore; + +mod cache; +mod crl_info; +mod crl_store; +mod misc; + +#[derive(Default)] +pub struct PkiEnvironmentParams<'a> { + /// Intermediate CAs and cross-signed CAs + pub intermediates: &'a [x509_cert::Certificate], + /// Trust Anchor roots + pub trust_roots: &'a [x509_cert::anchor::TrustAnchorChoice], + /// CRLs to add to the revocation check + pub crls: &'a [x509_cert::crl::CertificateList], + /// Time of interest for CRL verfication. If not provided, will default to current UNIX epoch + pub time_of_interest: Option, +} + +pub struct PkiEnvironment { + pe: certval::environment::PkiEnvironment, + toi: u64, +} + +impl std::ops::Deref for PkiEnvironment { + type Target = certval::environment::PkiEnvironment; + + fn deref(&self) -> &Self::Target { + &self.pe + } +} + +impl std::ops::DerefMut for PkiEnvironment { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pe + } +} + +impl std::fmt::Debug for PkiEnvironment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PkiEnvironment") + .field("pe", &"[OPAQUE]") + .field("toi", &self.toi) + .finish() + } +} + +// TODO: extract CRL URI from cert + +fn check_cpr(cpr: CertificationPathResults) -> RustyX509CheckResult<()> { + if let Some(validation_status) = get_validation_status(&cpr) { + return match validation_status { + certval::PathValidationStatus::Valid => Ok(()), + validation_status => Err(RustyX509CheckError::CertValError(certval::Error::PathValidation( + validation_status, + ))), + }; + } + + Err(RustyX509CheckError::CannotDetermineVerificationStatus) +} + +impl PkiEnvironment { + pub fn decode_pem_cert(pem: String) -> RustyX509CheckResult { + Ok(x509_cert::Certificate::from_pem(pem)?) + } + + pub fn decode_der_crl(crl_der: Vec) -> RustyX509CheckResult { + Ok(x509_cert::crl::CertificateList::from_der(&crl_der)?) + } + + pub fn extract_ski_aki_from_cert(cert: &x509_cert::Certificate) -> RustyX509CheckResult<(String, Option)> { + let mut cert = PDVCertificate::try_from(cert.clone())?; + cert.parse_extensions(&[ + const_oid::db::rfc5912::ID_CE_SUBJECT_KEY_IDENTIFIER, + const_oid::db::rfc5912::ID_CE_AUTHORITY_KEY_IDENTIFIER, + ]); + + let ski = cert + .get_extension(&const_oid::db::rfc5912::ID_CE_SUBJECT_KEY_IDENTIFIER)? + .and_then(|ext| match ext { + certval::PDVExtension::SubjectKeyIdentifier(ski) => String::from_utf8(ski.0.clone().into_bytes()).ok(), + _ => None, + }) + .unwrap_or_default(); + + let aki = cert + .get_extension(&const_oid::db::rfc5912::ID_CE_AUTHORITY_KEY_IDENTIFIER)? + .and_then(|ext| match ext { + certval::PDVExtension::AuthorityKeyIdentifier(aki) => aki + .key_identifier + .clone() + .and_then(|ki| String::from_utf8(ki.into_bytes()).ok()), + _ => None, + }); + + Ok((ski, aki)) + } + + pub fn encode_cert_to_der(cert: &x509_cert::Certificate) -> RustyX509CheckResult> { + Ok(cert.to_der()?) + } + + pub fn encode_crl_to_der(crl: &x509_cert::crl::CertificateList) -> RustyX509CheckResult> { + Ok(crl.to_der()?) + } + + /// Initializes a certval PkiEnvironment using the provided params + pub fn init(params: PkiEnvironmentParams) -> RustyX509CheckResult { + let toi = if let Some(toi) = params.time_of_interest { + toi + } else { + fluvio_wasm_timer::SystemTime::now() + .duration_since(fluvio_wasm_timer::SystemTime::UNIX_EPOCH) + .map_err(|_| RustyX509CheckError::CannotDetermineCurrentTime)? + .as_secs() + }; + + // Make a Certificate source for intermediate CA certs + let mut cert_source = CertSource::new(); + for (i, cert) in params.intermediates.iter().enumerate() { + cert_source.push(certval::CertFile { + filename: format!("Intermediate CA #{i} [{}]", cert.tbs_certificate.subject), + bytes: cert.to_der()?, + }); + } + + // Make a TrustAnchor source + let mut trust_anchors = TaSource::new(); + for (i, root) in params.trust_roots.iter().enumerate() { + trust_anchors.push(certval::CertFile { + filename: format!("TrustAnchor #{i}"), + bytes: root.to_der()?, + }); + } + + trust_anchors.initialize()?; + + let revocation_cache = RevocationCache::default(); + + // Make a CRL source + let crl_source = CrlStore::from(params.crls); + crl_source.index_crls(toi)?; + + let mut pe = certval::environment::PkiEnvironment::default(); + populate_5280_pki_environment(&mut pe); + pe.add_certificate_source(Box::new(cert_source)); + pe.add_trust_anchor_source(Box::new(trust_anchors)); + pe.add_crl_source(Box::new(crl_source)); + pe.add_revocation_cache(Box::new(revocation_cache)); + + Ok(Self { pe, toi }) + } + + /// Overrides TIME_OF_INTEREST for certificate verifications based on a moment in the past or future + pub fn set_time_of_interest(&mut self, toi: u64) { + self.toi = toi; + } + + /// Updates the TIME_OF_INTEREST for certificate checks to be `now` + pub fn refresh_time_of_interest(&mut self) -> RustyX509CheckResult<()> { + self.set_time_of_interest( + fluvio_wasm_timer::SystemTime::now() + .duration_since(fluvio_wasm_timer::SystemTime::UNIX_EPOCH) + .map_err(|_| RustyX509CheckError::CannotDetermineCurrentTime)? + .as_secs(), + ); + + Ok(()) + } + + pub fn validate_trust_anchor_cert(&self, cert: &x509_cert::Certificate) -> RustyX509CheckResult<()> { + let mut cps = CertificationPathSettings::default(); + set_time_of_interest(&mut cps, self.toi); + + let mut cert = PDVCertificate::try_from(cert.clone())?; + cert.parse_extensions(EXTS_OF_INTEREST); + let mut paths = vec![]; + self.pe.get_paths_for_target(&self.pe, &cert, &mut paths, 0, self.toi)?; + + for path in &mut paths { + let mut cpr = CertificationPathResults::new(); + check_validity(&self.pe, &cps, path, &mut cpr)?; + verify_signatures(&self.pe, &cps, path, &mut cpr)?; + check_cpr(cpr)?; + } + + Ok(()) + } + + pub fn validate_crl(&self, crl: &x509_cert::crl::CertificateList) -> RustyX509CheckResult<()> { + let spki_list = if let Ok(ta) = self.pe.get_trust_anchor_by_name(&crl.tbs_cert_list.issuer) { + vec![certval::source::ta_source::get_subject_public_key_info_from_trust_anchor(&ta.decoded_ta)] + } else { + self.pe + .get_cert_by_name(&crl.tbs_cert_list.issuer) + .into_iter() + .map(|c| &c.decoded_cert.tbs_certificate.subject_public_key_info) + .collect() + }; + + for spki in spki_list { + if self + .pe + .verify_signature_message( + &self.pe, + &crl.tbs_cert_list.to_der()?, + crl.signature.raw_bytes(), + &crl.signature_algorithm, + spki, + ) + .is_ok() + { + return Ok(()); + } + } + + Err(RustyX509CheckError::CertValError(certval::Error::Unrecognized)) + } + + fn validate_cert_internal( + &self, + end_identity_cert: &x509_cert::Certificate, + perform_revocation_check: bool, + ) -> RustyX509CheckResult<()> { + let mut cps = CertificationPathSettings::default(); + set_time_of_interest(&mut cps, self.toi); + set_require_ta_store(&mut cps, true); + + let mut end_identity_cert = PDVCertificate::try_from(end_identity_cert.clone())?; + end_identity_cert.parse_extensions(EXTS_OF_INTEREST); + + let mut paths = vec![]; + self.pe + .get_paths_for_target(&self.pe, &end_identity_cert, &mut paths, 0, self.toi)?; + + for path in &mut paths { + let mut cpr = CertificationPathResults::new(); + validate_path_rfc5280(&self.pe, &cps, path, &mut cpr)?; + if perform_revocation_check { + check_revocation(&self.pe, &cps, path, &mut cpr)?; + } + check_cpr(cpr)?; + } + + Ok(()) + } + + #[inline] + pub fn validate_cert(&self, end_identity_cert: &x509_cert::Certificate) -> RustyX509CheckResult<()> { + self.validate_cert_internal(end_identity_cert, false) + } + + #[inline] + pub fn validate_cert_and_revocation(&self, end_identity_cert: &x509_cert::Certificate) -> RustyX509CheckResult<()> { + self.validate_cert_internal(end_identity_cert, true) + } +} diff --git a/x509-check/src/revocation/cache.rs b/x509-check/src/revocation/cache.rs new file mode 100644 index 00000000..80acf7e6 --- /dev/null +++ b/x509-check/src/revocation/cache.rs @@ -0,0 +1,67 @@ +use certval::buffer_to_hex; +use certval::name_to_string; +use certval::PDVCertificate; +use certval::PathValidationStatus; +use certval::RevocationStatusCache; +use std::collections::BTreeMap; +use std::sync::Arc; +use std::sync::Mutex; + +#[derive(Clone, Copy)] +struct StatusAndTime { + status: PathValidationStatus, // Valid or Revoked + time: u64, +} + +type CacheMap = BTreeMap<(String, String), StatusAndTime>; + +#[derive(Default)] +pub struct RevocationCache { + cache_map: Arc>, +} + +impl RevocationStatusCache for RevocationCache { + fn get_status(&self, cert: &PDVCertificate, time_of_interest: u64) -> PathValidationStatus { + let name = name_to_string(&cert.decoded_cert.tbs_certificate.issuer); + let serial = buffer_to_hex(cert.decoded_cert.tbs_certificate.serial_number.as_bytes()); + + let Ok(cache_map) = self.cache_map.lock() else { + return PathValidationStatus::RevocationStatusNotDetermined; + }; + + if let Some(status_and_time) = cache_map.get(&(name, serial)) { + if status_and_time.time > time_of_interest { + return status_and_time.status; + } + } + + PathValidationStatus::RevocationStatusNotDetermined + } + + fn add_status(&self, cert: &PDVCertificate, next_update: u64, status: PathValidationStatus) { + if status != PathValidationStatus::Valid && status != PathValidationStatus::CertificateRevoked { + return; + } + + let name = name_to_string(&cert.decoded_cert.tbs_certificate.issuer); + let serial = buffer_to_hex(cert.decoded_cert.tbs_certificate.serial_number.as_bytes()); + + let Ok(mut cache_map) = self.cache_map.lock() else { + return; + }; + + let status_and_time = StatusAndTime { + status, + time: next_update, + }; + + cache_map + .entry((name, serial)) + .and_modify(|old_status_and_time| { + if old_status_and_time.time < next_update { + *old_status_and_time = status_and_time; + } + }) + .or_insert_with(|| status_and_time); + } +} diff --git a/x509-check/src/revocation/crl_info.rs b/x509-check/src/revocation/crl_info.rs new file mode 100644 index 00000000..1039506f --- /dev/null +++ b/x509-check/src/revocation/crl_info.rs @@ -0,0 +1,194 @@ +use certval::{name_to_string, CrlAuthority, CrlCoverage, CrlReasons, CrlScope, CrlType}; + +use x509_cert::{ + crl::CertificateList, + der::{Decode, Encode}, + ext::pkix::{ + name::{DistributionPointName, GeneralName}, + AuthorityKeyIdentifier, IssuingDistributionPoint, + }, +}; + +use const_oid::db::rfc5912::{ + ID_CE_AUTHORITY_KEY_IDENTIFIER, ID_CE_DELTA_CRL_INDICATOR, ID_CE_ISSUING_DISTRIBUTION_POINT, +}; + +use crate::RustyX509CheckError; + +flagset::flags! { + enum CrlQuestions: u8 { + EeOnly, + CaOnly, + AaOnly, + Delta, + Partitioned, + Indirect, + SomeReasons + } +} + +type CrlQuestionaire = flagset::FlagSet; + +#[derive(PartialEq, Eq, Clone)] +pub(crate) struct CrlInfo { + pub type_info: CrlType, + pub this_update: u64, + pub next_update: Option, + pub issuer_name: String, + pub issuer_name_blob: Vec, + pub sig_alg_blob: Vec, + pub exts_blob: Option>, + pub idp_name: Option, + pub idp_blob: Option>, + pub skid: Option>, +} + +impl TryFrom<&CertificateList> for CrlInfo { + type Error = RustyX509CheckError; + + fn try_from(crl: &CertificateList) -> Result { + let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); + let next_update = crl.tbs_cert_list.next_update.map(|nu| nu.to_unix_duration().as_secs()); + let issuer_name_blob = crl + .tbs_cert_list + .issuer + .to_der() + .map_err(|_| certval::Error::Unrecognized)?; + let issuer_name = name_to_string(&crl.tbs_cert_list.issuer); + let sig_alg_blob = crl + .signature_algorithm + .to_der() + .map_err(|_| certval::Error::Unrecognized)?; + let mut exts_blob = None; + if let Some(crl_exts) = &crl.tbs_cert_list.crl_extensions { + exts_blob.replace(crl_exts.to_der().map_err(|_| certval::Error::Unrecognized)?); + } + let mut idp_blob: Option> = None; + let mut idp_name: Option = None; + let mut skid: Option> = None; + + let mut questionnaire = CrlQuestionaire::default(); + + //SKID, delta, idp + if let Some(exts) = &crl.tbs_cert_list.crl_extensions { + for ext in exts.iter() { + match ext.extn_id { + ID_CE_ISSUING_DISTRIBUTION_POINT => { + idp_blob = Some(ext.extn_value.as_bytes().to_vec()); + let idp = IssuingDistributionPoint::from_der(ext.extn_value.as_bytes()) + .map_err(certval::Error::Asn1Error)?; + + match &idp.distribution_point { + Some(DistributionPointName::FullName(gns)) => { + for gn in gns { + if let GeneralName::DirectoryName(dn) = gn { + idp_name = Some(name_to_string(dn)); + break; + } + } + if idp_name.is_none() { + // not supporting non-DN DPs + return Err(certval::Error::Unrecognized.into()); + } + } + Some(DistributionPointName::NameRelativeToCRLIssuer(_unsupported)) => { + // Not supporting name relative to issuer + return Err(certval::Error::Unrecognized.into()); + } + _ => {} + } + + if idp.distribution_point.is_some() { + questionnaire |= CrlQuestions::Partitioned; + } + + if idp.indirect_crl { + questionnaire |= CrlQuestions::Indirect; + } + if idp.only_some_reasons.is_some() { + questionnaire |= CrlQuestions::SomeReasons; + } + if idp.only_contains_user_certs { + questionnaire |= CrlQuestions::EeOnly; + } + if idp.only_contains_ca_certs { + questionnaire |= CrlQuestions::CaOnly; + } + if idp.only_contains_attribute_certs { + questionnaire |= CrlQuestions::AaOnly; + } + } // end ID_CE_ISSUING_DISTRIBUTION_POINT + ID_CE_AUTHORITY_KEY_IDENTIFIER => { + if let Ok(akid) = AuthorityKeyIdentifier::from_der(ext.extn_value.as_bytes()) { + if let Some(kid) = akid.key_identifier { + skid = Some(kid.as_bytes().to_vec()); + } + } + } + ID_CE_DELTA_CRL_INDICATOR => { + questionnaire |= CrlQuestions::Delta; + } + _ => {} + } + } //end iterating over extensions + } + + if questionnaire.contains(CrlQuestions::AaOnly) { + //XXX-DEFER Do work here to support ACRL, AARL, etc. + return Err(certval::Error::CrlIncompatible.into()); + } + + let coverage = if questionnaire.contains(CrlQuestions::EeOnly) { + CrlCoverage::EeOnly + } else if questionnaire.contains(CrlQuestions::CaOnly) { + CrlCoverage::CaOnly + } else { + CrlCoverage::All + }; + + let authority = if questionnaire.contains(CrlQuestions::Indirect) { + CrlAuthority::Indirect + } else { + CrlAuthority::Direct + }; + + let scope = if questionnaire.contains(CrlQuestions::Partitioned) { + if questionnaire.contains(CrlQuestions::Delta) { + CrlScope::DeltaDp + } else { + CrlScope::Dp + } + } else if questionnaire.contains(CrlQuestions::Delta) { + CrlScope::Delta + } else { + CrlScope::Complete + }; + + //determine reasons + let reasons = if questionnaire.contains(CrlQuestions::SomeReasons) { + CrlReasons::SomeReasons + } else { + CrlReasons::AllReasons + }; + + let type_info = CrlType { + scope, + coverage, + authority, + reasons, + }; + + Ok(CrlInfo { + type_info, + skid, + this_update, + next_update, + issuer_name, + issuer_name_blob, + sig_alg_blob, + exts_blob, + idp_name, + idp_blob, + }) + } +} diff --git a/x509-check/src/revocation/crl_store.rs b/x509-check/src/revocation/crl_store.rs new file mode 100644 index 00000000..93bfbb3f --- /dev/null +++ b/x509-check/src/revocation/crl_store.rs @@ -0,0 +1,179 @@ +use crate::{ + revocation::{ + crl_info::CrlInfo, + misc::{check_crl_valid_at_toi, get_dp_from_crl, get_dps_from_cert}, + }, + RustyX509CheckError, RustyX509CheckResult, +}; +use certval::{name_to_string, CrlScope, CrlSource, ExtensionProcessing, PDVCertificate, PDVExtension}; +use const_oid::db::rfc5912::ID_CE_AUTHORITY_KEY_IDENTIFIER; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use x509_cert::{crl::CertificateList, der::Encode}; + +type IssuerMap = BTreeMap>; +type SkidMap = BTreeMap, Vec>; +type DpMap = BTreeMap, Vec>; + +pub(crate) struct CrlStore { + crls: Arc>>, + crl_info: Arc>>, + issuers: Arc>, + sk_ids: Arc>, + dps: Arc>, +} + +impl From<&[CertificateList]> for CrlStore { + fn from(value: &[CertificateList]) -> Self { + Self { + crls: Mutex::new(value.to_vec()).into(), + crl_info: Default::default(), + issuers: Default::default(), + sk_ids: Default::default(), + dps: Default::default(), + } + } +} + +impl CrlStore { + fn add_crl_info_with_guard( + &self, + crl: &CertificateList, + info: CrlInfo, + crl_info: &mut MutexGuard>, + ) -> RustyX509CheckResult<()> { + if crl_info.contains(&info) { + return Ok(()); + } + + let mut is_dp = false; + crl_info.push(info); + let index = crl_info.len() - 1; + + // SAFETY: This unwrap is safe as we just inserted the data above + let info = crl_info.last().unwrap(); + if let Some(dp) = get_dp_from_crl(crl) { + self.dps + .lock() + .map_err(|_| RustyX509CheckError::LockPoisonError)? + .entry(dp) + .or_default() + .push(index); + is_dp = true; + } else if let Some(akid) = info.skid.clone() { + self.sk_ids + .lock() + .map_err(|_| RustyX509CheckError::LockPoisonError)? + .entry(akid) + .or_default() + .push(index); + } + + if !is_dp && info.type_info.scope == CrlScope::Complete { + let issuer_name = name_to_string(&crl.tbs_cert_list.issuer); + self.issuers + .lock() + .map_err(|_| RustyX509CheckError::LockPoisonError)? + .entry(issuer_name) + .or_default() + .push(index); + } + + Ok(()) + } + + #[inline] + fn add_crl_info(&self, crl: &CertificateList, info: CrlInfo) -> RustyX509CheckResult<()> { + self.add_crl_info_with_guard( + crl, + info, + &mut self.crl_info.lock().map_err(|_| RustyX509CheckError::LockPoisonError)?, + ) + } + + pub(crate) fn index_crls(&self, time_of_interest: u64) -> RustyX509CheckResult<()> { + let crls = self.crls.lock().map_err(|_| RustyX509CheckError::LockPoisonError)?; + let mut crl_info = self.crl_info.lock().map_err(|_| RustyX509CheckError::LockPoisonError)?; + for crl in crls.iter() { + match CrlInfo::try_from(crl) { + Ok(info) if check_crl_valid_at_toi(time_of_interest, crl) => { + self.add_crl_info_with_guard(crl, info, &mut crl_info)?; + } + _ => continue, + } + } + + Ok(()) + } +} + +impl CrlSource for CrlStore { + fn get_crls(&self, cert: &PDVCertificate) -> certval::Result>> { + let crls = self.crls.lock().map_err(|_| certval::Error::Unrecognized)?; + // DistributionPoint matching + if let Some(dps) = get_dps_from_cert(cert) { + let source_dps = self.dps.lock().map_err(|_| certval::Error::Unrecognized)?; + for dp in dps { + if let Some(indices) = source_dps.get(&dp) { + let mut retval = vec![]; + for index in indices { + if let Some(crl) = crls.get(*index) { + retval.push(crl.to_der()?); + } + } + return Ok(retval); + } + } + } + + // AKI matching + if let Ok(Some(PDVExtension::AuthorityKeyIdentifier(akid))) = + cert.get_extension(&ID_CE_AUTHORITY_KEY_IDENTIFIER) + { + if let Some(kid) = &akid.key_identifier { + let skids = self.sk_ids.lock().map_err(|_| certval::Error::Unrecognized)?; + let kid_bytes = kid.as_bytes(); + if let Some(indices) = skids.get(kid_bytes) { + let mut retval = vec![]; + for index in indices { + if let Some(crl) = crls.get(*index) { + retval.push(crl.to_der()?); + } + } + return Ok(retval); + } + } + } + + // Issuer fallback + let issuer_name = name_to_string(&cert.decoded_cert.tbs_certificate.issuer); + let issuers = self.issuers.lock().map_err(|_| certval::Error::Unrecognized)?; + if let Some(indices) = issuers.get(&issuer_name) { + let mut retval = vec![]; + for index in indices { + if let Some(crl) = crls.get(*index) { + retval.push(crl.to_der()?); + } + } + return Ok(retval); + } + + Err(certval::Error::NotFound) + } + + fn add_crl(&self, _: &[u8], crl: &CertificateList, _: &str) -> certval::Result<()> { + self.crls + .lock() + .map_err(|_| certval::Error::Unrecognized)? + .push(crl.clone()); + + if let Ok(info) = CrlInfo::try_from(crl) { + self.add_crl_info(crl, info).map_err(|_| certval::Error::Unrecognized)?; + } + + Ok(()) + } +} diff --git a/x509-check/src/revocation/misc.rs b/x509-check/src/revocation/misc.rs new file mode 100644 index 00000000..b71825dd --- /dev/null +++ b/x509-check/src/revocation/misc.rs @@ -0,0 +1,56 @@ +use certval::{ExtensionProcessing, PDVCertificate, PDVExtension}; +use const_oid::db::rfc5912::{ID_CE_CRL_DISTRIBUTION_POINTS, ID_CE_ISSUING_DISTRIBUTION_POINT}; +use x509_cert::{ + crl::CertificateList, + der::{Decode, Encode}, + ext::pkix::IssuingDistributionPoint, +}; + +pub(crate) fn check_crl_valid_at_toi(toi: u64, crl: &CertificateList) -> bool { + if toi == 0 { + return false; + } + + if crl.tbs_cert_list.this_update.to_unix_duration().as_secs() > toi { + return false; + } + + if let Some(nu) = crl.tbs_cert_list.next_update { + if nu.to_unix_duration().as_secs() < toi { + return false; + } + } + + true +} + +pub(crate) fn get_dp_from_crl(crl: &CertificateList) -> Option> { + if let Some(exts) = &crl.tbs_cert_list.crl_extensions { + for ext in exts { + if ext.extn_id == ID_CE_ISSUING_DISTRIBUTION_POINT { + if let Some(enc_dp) = IssuingDistributionPoint::from_der(ext.extn_value.as_bytes()) + .ok() + .and_then(|idp| idp.distribution_point) + .and_then(|dp| dp.to_der().ok()) + { + return Some(enc_dp); + } + } + } + } + None +} + +pub(crate) fn get_dps_from_cert(cert: &PDVCertificate) -> Option>> { + match cert.get_extension(&ID_CE_CRL_DISTRIBUTION_POINTS) { + Ok(Some(PDVExtension::CrlDistributionPoints(crl_dps))) => Some( + crl_dps + .0 + .iter() + .filter_map(|crl_dp| crl_dp.distribution_point.as_ref().and_then(|dp| dp.to_der().ok())) + .collect::>(), + ), + _ => None, + }; + None +}