diff --git a/src/cryptography/hazmat/bindings/_rust/x509.pyi b/src/cryptography/hazmat/bindings/_rust/x509.pyi index 196a3c60f7ed..25544a783b1d 100644 --- a/src/cryptography/hazmat/bindings/_rust/x509.pyi +++ b/src/cryptography/hazmat/bindings/_rust/x509.pyi @@ -217,6 +217,11 @@ class PolicyBuilder: def extension_policies( self, *, ca_policy: ExtensionPolicy, ee_policy: ExtensionPolicy ) -> PolicyBuilder: ... + def revocation_checker( + self, + revocation_checker: x509.verification.CRLRevocationChecker + | x509.verification.RevocationChecker, + ) -> PolicyBuilder: ... def build_client_verifier(self) -> ClientVerifier: ... def build_server_verifier( self, subject: x509.verification.Subject @@ -278,6 +283,11 @@ class ExtensionPolicy: validator: PresentExtensionValidatorCallback[T] | None, ) -> ExtensionPolicy: ... +class RevocationChecker: ... + +class CRLRevocationChecker: + def __init__(self, crls: list[x509.CertificateRevocationList]) -> None: ... + class VerifiedClient: @property def subjects(self) -> list[x509.GeneralName] | None: ... diff --git a/src/cryptography/x509/verification.py b/src/cryptography/x509/verification.py index 2db4324d9615..a4c3eda44bf9 100644 --- a/src/cryptography/x509/verification.py +++ b/src/cryptography/x509/verification.py @@ -4,17 +4,20 @@ from __future__ import annotations +import abc import typing from cryptography.hazmat.bindings._rust import x509 as rust_x509 from cryptography.x509.general_name import DNSName, IPAddress __all__ = [ + "CRLRevocationChecker", "ClientVerifier", "Criticality", "ExtensionPolicy", "Policy", "PolicyBuilder", + "RevocationChecker", "ServerVerifier", "Store", "Subject", @@ -32,3 +35,22 @@ ExtensionPolicy = rust_x509.ExtensionPolicy Criticality = rust_x509.Criticality VerificationError = rust_x509.VerificationError +CRLRevocationChecker = rust_x509.CRLRevocationChecker + + +class RevocationChecker(rust_x509.RevocationChecker, metaclass=abc.ABCMeta): + """ + An interface for revocation checkers. + """ + + @abc.abstractmethod + def is_revoked( + self, + leaf: rust_x509.Certificate, + issuer: rust_x509.Certificate, + policy: Policy, + ) -> bool | None: + """ + Returns whether the certificate is revoked. If the revocation status + cannot be determined, the revocation checker may return None. + """ diff --git a/src/rust/cryptography-x509-verification/src/certificate.rs b/src/rust/cryptography-x509-verification/src/certificate.rs index 4c9fc256b5f8..22ecb476c703 100644 --- a/src/rust/cryptography-x509-verification/src/certificate.rs +++ b/src/rust/cryptography-x509-verification/src/certificate.rs @@ -14,8 +14,9 @@ pub(crate) fn cert_is_self_issued(cert: &Certificate<'_>) -> bool { pub(crate) mod tests { use super::cert_is_self_issued; use crate::certificate::Certificate; - use crate::ops::tests::{cert, v1_cert_pem}; + use crate::ops::tests::{cert, crl, v1_cert_pem}; use crate::ops::CryptoOps; + use cryptography_x509::crl::CertificateRevocationList; #[test] fn test_certificate_v1() { @@ -42,6 +43,25 @@ Xw4nMqk= .unwrap() } + fn crl_pem() -> pem::Pem { + // From vectors/cryptography_vectors/x509/custom/crl_empty.pem + pem::parse( + "-----BEGIN X509 CRL----- +MIIBxTCBrgIBATANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJVUzERMA8GA1UE +CAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xETAPBgNVBAoMCHI1MDkgTExD +MRowGAYDVQQDDBFyNTA5IENSTCBEZWxlZ2F0ZRcNMTUxMjIwMjM0NDQ3WhcNMTUx +MjI4MDA0NDQ3WqAZMBcwCgYDVR0UBAMCAQEwCQYDVR0jBAIwADANBgkqhkiG9w0B +AQUFAAOCAQEAXebqoZfEVAC4NcSEB5oGqUviUn/AnY6TzB6hUe8XC7yqEkBcyTgk +G1Zq+b+T/5X1ewTldvuUqv19WAU/Epbbu4488PoH5qMV8Aii2XcotLJOR9OBANp0 +Yy4ir/n6qyw8kM3hXJloE+xgkELhd5JmKCnlXihM1BTl7Xp7jyKeQ86omR+DhItb +CU+9RoqOK9Hm087Z7RurXVrz5RKltQo7VLCp8VmrxFwfALCZENXGEQ+g5VkvoCjc +ph5jqOSyzp7aZy1pnLE/6U6V32ItskrwqA+x4oj2Wvzir/Q23y2zYfqOkuq4fTd2 +lWW+w5mB167fIWmd6efecDn1ZqbdECDPUg== +-----END X509 CRL-----", + ) + .unwrap() + } + #[test] fn test_certificate_ca() { let cert_pem = ca_pem(); @@ -62,6 +82,14 @@ Xw4nMqk= Err(()) } + fn verify_crl_signed_by( + &self, + _crl: &CertificateRevocationList<'_>, + _key: &Self::Key, + ) -> Result<(), Self::Err> { + Ok(()) + } + fn verify_signed_by( &self, _cert: &Certificate<'_>, @@ -98,9 +126,12 @@ Xw4nMqk= // Just to get coverage on the `PublicKeyErrorOps` helper. let cert_pem = ca_pem(); let cert = cert(&cert_pem); + let crl_pem = crl_pem(); + let crl = crl(&crl_pem); let ops = PublicKeyErrorOps {}; assert!(ops.public_key(&cert).is_err()); assert!(ops.verify_signed_by(&cert, &()).is_ok()); + assert!(ops.verify_crl_signed_by(&crl, &()).is_ok()); } } diff --git a/src/rust/cryptography-x509-verification/src/lib.rs b/src/rust/cryptography-x509-verification/src/lib.rs index b995a03505f2..03e04727d4d8 100644 --- a/src/rust/cryptography-x509-verification/src/lib.rs +++ b/src/rust/cryptography-x509-verification/src/lib.rs @@ -9,6 +9,7 @@ pub mod certificate; pub mod ops; pub mod policy; +pub mod revocation; pub mod trust_store; pub mod types; @@ -26,6 +27,7 @@ use cryptography_x509::oid::{NAME_CONSTRAINTS_OID, SUBJECT_ALTERNATIVE_NAME_OID} use crate::certificate::cert_is_self_issued; use crate::ops::{CryptoOps, VerificationCertificate}; use crate::policy::Policy; +use crate::revocation::RevocationChecker; use crate::trust_store::Store; use crate::types::{ DNSConstraint, DNSPattern, IPAddress, IPConstraint, RFC822Constraint, RFC822Name, @@ -40,6 +42,7 @@ pub enum ValidationErrorKind<'chain, B: CryptoOps> { reason: &'static str, }, FatalError(&'static str), + RevocationNotDetermined, Other(String), } @@ -91,6 +94,9 @@ impl Display for ValidationError<'_, B> { write!(f, "invalid extension: {oid}: {reason}") } ValidationErrorKind::FatalError(err) => write!(f, "fatal error: {err}"), + ValidationErrorKind::RevocationNotDetermined => { + write!(f, "unable to determine revocation status") + } ValidationErrorKind::Other(err) => write!(f, "{err}"), } } @@ -272,9 +278,10 @@ pub fn verify<'chain, B: CryptoOps>( leaf: &VerificationCertificate<'chain, B>, intermediates: &[VerificationCertificate<'chain, B>], policy: &Policy<'_, B>, + revocation_checker: Option<&'_ RevocationChecker<'_, B>>, store: &Store<'chain, B>, ) -> ValidationResult<'chain, Chain<'chain, B>, B> { - let builder = ChainBuilder::new(intermediates, policy, store); + let builder = ChainBuilder::new(intermediates, policy, revocation_checker, store); let mut budget = Budget::new(); builder.build_chain(leaf, &mut budget) @@ -283,6 +290,7 @@ pub fn verify<'chain, B: CryptoOps>( struct ChainBuilder<'a, 'chain, B: CryptoOps> { intermediates: &'a [VerificationCertificate<'chain, B>], policy: &'a Policy<'a, B>, + revocation_checker: Option<&'a RevocationChecker<'a, B>>, store: &'a Store<'chain, B>, } @@ -309,11 +317,13 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { fn new( intermediates: &'a [VerificationCertificate<'chain, B>], policy: &'a Policy<'a, B>, + revocation_checker: Option<&'a RevocationChecker<'a, B>>, store: &'a Store<'chain, B>, ) -> Self { Self { intermediates, policy, + revocation_checker, store, } } @@ -410,6 +420,18 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { budget, ) { Ok(mut chain) => { + if let Some(revocation_checker) = self.revocation_checker { + if revocation_checker.is_revoked( + working_cert, + issuing_cert_candidate, + self.policy, + )? { + return Err(ValidationError::new(ValidationErrorKind::Other( + "certificate revoked".to_string(), + ))); + } + } + chain.push(working_cert.clone()); return Ok(chain); } @@ -501,6 +523,10 @@ mod tests { "invalid extension: 2.5.29.17: duplicate extension" ); + let err = + ValidationError::::new(ValidationErrorKind::RevocationNotDetermined); + assert_eq!(err.to_string(), "unable to determine revocation status"); + let err = ValidationError::::new(ValidationErrorKind::FatalError("oops")); assert_eq!(err.to_string(), "fatal error: oops"); diff --git a/src/rust/cryptography-x509-verification/src/ops.rs b/src/rust/cryptography-x509-verification/src/ops.rs index c539d2eaf015..07e201c666f7 100644 --- a/src/rust/cryptography-x509-verification/src/ops.rs +++ b/src/rust/cryptography-x509-verification/src/ops.rs @@ -5,6 +5,7 @@ use std::sync::OnceLock; use cryptography_x509::certificate::Certificate; +use cryptography_x509::crl::CertificateRevocationList; pub struct VerificationCertificate<'a, B: CryptoOps> { cert: &'a Certificate<'a>, @@ -86,6 +87,14 @@ pub trait CryptoOps { /// if the key is malformed. fn public_key(&self, cert: &Certificate<'_>) -> Result; + /// Verifies the signature on `CertificateRevocationList` using + /// the given `Key`. + fn verify_crl_signed_by( + &self, + crl: &CertificateRevocationList<'_>, + key: &Self::Key, + ) -> Result<(), Self::Err>; + /// Verifies the signature on `Certificate` using the given /// `Key`. fn verify_signed_by(&self, cert: &Certificate<'_>, key: &Self::Key) -> Result<(), Self::Err>; @@ -100,6 +109,7 @@ pub trait CryptoOps { #[cfg(test)] pub(crate) mod tests { use cryptography_x509::certificate::Certificate; + use cryptography_x509::crl::CertificateRevocationList; use super::VerificationCertificate; use crate::certificate::tests::PublicKeyErrorOps; @@ -129,6 +139,10 @@ zl9HYIMxATFyqSiD9jsx asn1::parse_single(cert_pem.contents()).unwrap() } + pub(crate) fn crl(crl_pem: &pem::Pem) -> CertificateRevocationList<'_> { + asn1::parse_single(crl_pem.contents()).unwrap() + } + #[test] fn test_verification_certificate_debug() { let p = v1_cert_pem(); diff --git a/src/rust/cryptography-x509-verification/src/revocation.rs b/src/rust/cryptography-x509-verification/src/revocation.rs new file mode 100644 index 000000000000..91200a4457f8 --- /dev/null +++ b/src/rust/cryptography-x509-verification/src/revocation.rs @@ -0,0 +1,52 @@ +// This file is dual licensed under the terms of the Apache License, Version +// 2.0, and the BSD License. See the LICENSE file in the root of this repository +// for complete details. + +use cryptography_x509::crl::CertificateRevocationList; + +use crate::{ + ops::{CryptoOps, VerificationCertificate}, + policy::Policy, + ValidationError, ValidationErrorKind, ValidationResult, +}; + +pub trait CheckRevocation { + fn is_revoked<'chain>( + &self, + cert: &VerificationCertificate<'chain, B>, + issuer: &VerificationCertificate<'chain, B>, + policy: &Policy<'_, B>, + ) -> ValidationResult<'chain, bool, B>; +} + +pub struct CrlRevocationChecker<'a> { + crls: Vec<&'a CertificateRevocationList<'a>>, +} + +impl<'a, B: CryptoOps> CheckRevocation for CrlRevocationChecker<'a> { + fn is_revoked<'chain>( + &self, + cert: &VerificationCertificate<'chain, B>, + issuer: &VerificationCertificate<'chain, B>, + policy: &Policy<'_, B>, + ) -> ValidationResult<'chain, bool, B> { + let _crls = &self.crls; + let _cert = cert; + let _issuer = issuer; + let _policy = policy; + + Err(ValidationError::new(ValidationErrorKind::FatalError( + "unimplemented", + ))) + } +} + +impl<'a> CrlRevocationChecker<'a> { + pub fn new(crls: impl IntoIterator>) -> Self { + Self { + crls: crls.into_iter().collect(), + } + } +} + +pub type RevocationChecker<'a, B> = dyn CheckRevocation + Send + Sync + 'a; diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index e32468df9671..0f918cbe5799 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -179,8 +179,9 @@ mod _rust { use crate::x509::sct::Sct; #[pymodule_export] use crate::x509::verify::{ - PolicyBuilder, PyClientVerifier, PyCriticality, PyExtensionPolicy, PyPolicy, - PyServerVerifier, PyStore, PyVerifiedClient, VerificationError, + PolicyBuilder, PyClientVerifier, PyCriticality, PyCrlRevocationChecker, + PyExtensionPolicy, PyPolicy, PyRevocationChecker, PyServerVerifier, PyStore, + PyVerifiedClient, VerificationError, }; } diff --git a/src/rust/src/x509/crl.rs b/src/rust/src/x509/crl.rs index 791513ca8ad1..18504d4760ba 100644 --- a/src/rust/src/x509/crl.rs +++ b/src/rust/src/x509/crl.rs @@ -10,6 +10,7 @@ use cryptography_x509::crl::{ }; use cryptography_x509::extensions::{Extension, IssuerAlternativeName}; use cryptography_x509::{name, oid}; +use cryptography_x509_verification::ops::CryptoOps; use pyo3::types::{PyAnyMethods, PyListMethods, PySliceMethods}; use crate::asn1::{ @@ -17,6 +18,7 @@ use crate::asn1::{ }; use crate::backend::hashes::Hash; use crate::error::{CryptographyError, CryptographyResult}; +use crate::x509::verify::PyCryptoOps; use crate::x509::{certificate, extensions, sign}; use crate::{exceptions, types, x509}; @@ -72,7 +74,7 @@ pub(crate) fn load_pem_x509_crl( } self_cell::self_cell!( - struct OwnedCertificateRevocationList { + pub(crate) struct OwnedCertificateRevocationList { owner: pyo3::Py, #[covariant] dependent: RawCertificateRevocationList, @@ -81,7 +83,7 @@ self_cell::self_cell!( #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.x509")] pub(crate) struct CertificateRevocationList { - owned: OwnedCertificateRevocationList, + pub(crate) owned: OwnedCertificateRevocationList, revoked_certs: pyo3::sync::PyOnceLock>, cached_extensions: pyo3::sync::PyOnceLock>, @@ -422,14 +424,10 @@ impl CertificateRevocationList { // being an invalid signature. sign::identify_public_key_type(py, public_key.clone())?; - Ok(sign::verify_signature_with_signature_algorithm( - py, - public_key, - &slf.owned.borrow_dependent().signature_algorithm, - slf.owned.borrow_dependent().signature_value.as_bytes(), - &asn1::write_single(&slf.owned.borrow_dependent().tbs_cert_list)?, - ) - .is_ok()) + let ops = PyCryptoOps {}; + Ok(ops + .verify_crl_signed_by(slf.owned.borrow_dependent(), &public_key.unbind()) + .is_ok()) } } diff --git a/src/rust/src/x509/verify/mod.rs b/src/rust/src/x509/verify/mod.rs index 3fce53d6a4b4..26f37437cb25 100644 --- a/src/rust/src/x509/verify/mod.rs +++ b/src/rust/src/x509/verify/mod.rs @@ -3,6 +3,7 @@ // for complete details. use cryptography_x509::certificate::Certificate; +use cryptography_x509::crl::CertificateRevocationList; use cryptography_x509::extensions::SubjectAlternativeName; use cryptography_x509::oid::SUBJECT_ALTERNATIVE_NAME_OID; use cryptography_x509_verification::ops::{CryptoOps, VerificationCertificate}; @@ -13,6 +14,8 @@ use pyo3::types::{PyAnyMethods, PyListMethods}; mod extension_policy; mod policy; +mod revocation; +pub(crate) use crate::x509::verify::revocation::{PyCrlRevocationChecker, PyRevocationChecker}; pub(crate) use extension_policy::{PyCriticality, PyExtensionPolicy}; pub(crate) use policy::PyPolicy; @@ -23,6 +26,7 @@ use crate::types; use crate::x509::certificate::Certificate as PyCertificate; use crate::x509::common::{datetime_now, py_to_datetime}; use crate::x509::sign; +use crate::x509::verify::revocation::build_rust_revocation_checker; #[derive(Clone)] pub(crate) struct PyCryptoOps {} @@ -39,6 +43,22 @@ impl CryptoOps for PyCryptoOps { }) } + fn verify_crl_signed_by( + &self, + crl: &CertificateRevocationList<'_>, + key: &Self::Key, + ) -> Result<(), Self::Err> { + pyo3::Python::attach(|py| -> CryptographyResult<()> { + sign::verify_signature_with_signature_algorithm( + py, + key.bind(py).clone(), + &crl.signature_algorithm, + crl.signature_value.as_bytes(), + &asn1::write_single(&crl.tbs_cert_list)?, + ) + }) + } + fn verify_signed_by(&self, cert: &Certificate<'_>, key: &Self::Key) -> Result<(), Self::Err> { pyo3::Python::attach(|py| -> CryptographyResult<()> { sign::verify_signature_with_signature_algorithm( @@ -87,6 +107,7 @@ pub(crate) struct PolicyBuilder { max_chain_depth: Option, ca_ext_policy: Option>, ee_ext_policy: Option>, + revocation_checker: Option>, } impl PolicyBuilder { @@ -97,6 +118,7 @@ impl PolicyBuilder { max_chain_depth: self.max_chain_depth, ca_ext_policy: self.ca_ext_policy.as_ref().map(|p| p.clone_ref(py)), ee_ext_policy: self.ee_ext_policy.as_ref().map(|p| p.clone_ref(py)), + revocation_checker: self.revocation_checker.as_ref().map(|p| p.clone_ref(py)), } } } @@ -111,6 +133,7 @@ impl PolicyBuilder { max_chain_depth: None, ca_ext_policy: None, ee_ext_policy: None, + revocation_checker: None, } } @@ -170,6 +193,19 @@ impl PolicyBuilder { }) } + fn revocation_checker( + &self, + py: pyo3::Python<'_>, + revocation_checker: pyo3::Py, + ) -> CryptographyResult { + policy_builder_set_once_check!(self, revocation_checker, "revocation checker"); + + Ok(PolicyBuilder { + revocation_checker: Some(revocation_checker), + ..self.py_clone(py) + }) + } + fn build_client_verifier(&self, py: pyo3::Python<'_>) -> CryptographyResult { let store = match self.store.as_ref() { Some(s) => s.clone_ref(py), @@ -209,6 +245,7 @@ impl PolicyBuilder { Ok(PyClientVerifier { py_policy: pyo3::Py::new(py, py_policy)?, + revocation_checker: self.revocation_checker.as_ref().map(|c| c.clone_ref(py)), store, }) } @@ -266,6 +303,7 @@ impl PolicyBuilder { Ok(PyServerVerifier { py_policy: pyo3::Py::new(py, py_policy)?, + revocation_checker: self.revocation_checker.as_ref().map(|c| c.clone_ref(py)), store, }) } @@ -314,6 +352,8 @@ pub(crate) struct PyClientVerifier { #[pyo3(get, name = "policy")] py_policy: pyo3::Py, #[pyo3(get)] + revocation_checker: Option>, + #[pyo3(get)] store: pyo3::Py, } @@ -332,6 +372,10 @@ impl PyClientVerifier { intermediates: Vec>, ) -> CryptographyResult { let policy = Policy::new(self.as_policy_def(), self.py_policy.clone_ref(py)); + let revocation_checker = self + .revocation_checker + .as_ref() + .map(|c| build_rust_revocation_checker(py, c)); let store = self.store.get(); let intermediates = intermediates @@ -345,6 +389,7 @@ impl PyClientVerifier { &v, &intermediates, &policy, + revocation_checker, store.raw.borrow_dependent(), ) .or_else(|e| handle_validation_error(py, e))?; @@ -386,6 +431,8 @@ pub(crate) struct PyServerVerifier { #[pyo3(get, name = "policy")] py_policy: pyo3::Py, #[pyo3(get)] + revocation_checker: Option>, + #[pyo3(get)] store: pyo3::Py, } @@ -404,6 +451,10 @@ impl PyServerVerifier { intermediates: Vec>, ) -> CryptographyResult> { let policy = Policy::new(self.as_policy_def(), self.py_policy.clone_ref(py)); + let revocation_checker = self + .revocation_checker + .as_ref() + .map(|c| build_rust_revocation_checker(py, c)); let store = self.store.get(); let intermediates = intermediates @@ -417,6 +468,7 @@ impl PyServerVerifier { &v, &intermediates, &policy, + revocation_checker, store.raw.borrow_dependent(), ) .or_else(|e| handle_validation_error(py, e))?; diff --git a/src/rust/src/x509/verify/revocation.rs b/src/rust/src/x509/verify/revocation.rs new file mode 100644 index 000000000000..d339e9434781 --- /dev/null +++ b/src/rust/src/x509/verify/revocation.rs @@ -0,0 +1,118 @@ +use cryptography_x509_verification::{ + ops::VerificationCertificate, + policy::Policy, + revocation::{CheckRevocation, CrlRevocationChecker, RevocationChecker}, + ValidationError, ValidationErrorKind, ValidationResult, +}; + +use crate::x509::{crl::CertificateRevocationList, verify::PyCryptoOps}; + +self_cell::self_cell!( + pub(crate) struct RawPyCrlRevocationChecker { + owner: Vec>, + + #[covariant] + dependent: CrlRevocationChecker, + } +); + +#[pyo3::pyclass( + frozen, + module = "cryptography.hazmat.bindings._rust.x509", + name = "CRLRevocationChecker", + extends = PyRevocationChecker, +)] +pub(crate) struct PyCrlRevocationChecker { + pub(crate) raw: RawPyCrlRevocationChecker, +} + +#[pyo3::pymethods] +impl PyCrlRevocationChecker { + #[new] + fn new( + crls: Vec>, + ) -> pyo3::PyResult<(Self, PyRevocationChecker)> { + if crls.is_empty() { + return Err(pyo3::exceptions::PyValueError::new_err( + "can't create an empty CRL revocation checker", + )); + } + + Ok(( + Self { + raw: RawPyCrlRevocationChecker::new(crls, |v| { + CrlRevocationChecker::new(v.iter().map(|i| i.get().owned.borrow_dependent())) + }), + }, + PyRevocationChecker {}, + )) + } +} + +// NO-COVERAGE-START +#[pyo3::pyclass( + subclass, + frozen, + module = "cryptography.hazmat.bindings._rust.x509", + name = "RevocationChecker" +)] +// NO-COVERAGE-END +/// A marker class that Rust and Python revocation checkers subclass from. +pub(crate) struct PyRevocationChecker; + +#[pyo3::pymethods] +impl PyRevocationChecker { + #[new] + #[pyo3(signature = (*_args))] + pub fn new(_args: &pyo3::Bound<'_, pyo3::PyAny>) -> Self { + Self + } +} + +impl CheckRevocation for pyo3::Py { + fn is_revoked<'chain>( + &self, + cert: &VerificationCertificate<'chain, PyCryptoOps>, + issuer: &VerificationCertificate<'chain, PyCryptoOps>, + policy: &Policy<'_, PyCryptoOps>, + ) -> ValidationResult<'chain, bool, PyCryptoOps> { + pyo3::Python::attach(|py| { + let result = self + .call_method1( + py, + pyo3::intern!(py, "is_revoked"), + (cert.extra(), issuer.extra(), &policy.extra), + ) + .map_err(|_e| { + ValidationError::new(ValidationErrorKind::FatalError::( + "the revocation checker raised an exception", + )) + })?; + + if result.is_none(py) { + Err(ValidationError::new( + ValidationErrorKind::RevocationNotDetermined, + )) + } else { + result.extract(py).map_err(|_e| { + ValidationError::new(ValidationErrorKind::FatalError::( + "the revocation checker must return one of True, False, or None", + )) + }) + } + }) + } +} + +/// Retrieves the underlying native RevocationChecker from the PyRevocationChecker if it exists. +pub(crate) fn build_rust_revocation_checker<'a>( + py: pyo3::Python<'a>, + checker: &'a pyo3::Py, +) -> &'a RevocationChecker<'a, PyCryptoOps> { + if let Ok(crl) = checker.cast_bound::(py) { + return crl.get().raw.borrow_dependent(); + } + + // this isn't a Rust-native revocation checker, fallthrough. + checker +} diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index 1d846a118485..3ae3349cd202 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -16,9 +16,11 @@ from cryptography.x509.general_name import DNSName, IPAddress from cryptography.x509.verification import ( Criticality, + CRLRevocationChecker, ExtensionPolicy, Policy, PolicyBuilder, + RevocationChecker, Store, VerificationError, ) @@ -27,6 +29,19 @@ WEBPKI_MINIMUM_RSA_MODULUS = 2048 +class DummyRevocationChecker(RevocationChecker): + def __init__(self, returns, raises=None) -> None: + self._returns = returns + self._raises = raises + + def is_revoked( + self, cert: x509.Certificate, issuer: x509.Certificate, policy: Policy + ) -> bool: + if self._raises is not None: + raise self._raises + return self._returns + + @lru_cache(maxsize=1) def dummy_store() -> Store: cert = _load_cert( @@ -51,6 +66,11 @@ def test_max_chain_depth_already_set(self): with pytest.raises(ValueError): PolicyBuilder().max_chain_depth(8).max_chain_depth(9) + def test_revocation_checker_already_set(self): + with pytest.raises(ValueError): + rc = DummyRevocationChecker(False) + PolicyBuilder().revocation_checker(rc).revocation_checker(rc) + def test_ipaddress_subject(self): verifier = ( PolicyBuilder() @@ -116,6 +136,12 @@ def test_build_server_verifier_missing_store(self): PolicyBuilder().build_server_verifier(DNSName("cryptography.io")) +class TestCRLRevocationChecker: + def test_crl_revocation_checker_rejects_empty_list(self): + with pytest.raises(ValueError): + CRLRevocationChecker([]) + + class TestStore: def test_store_rejects_empty_list(self): with pytest.raises(ValueError): @@ -176,6 +202,82 @@ def test_verify(self): assert x509.DNSName("cryptography.io") in verified_client.subjects assert len(verified_client.subjects) == 2 + def test_verify_crl_checker(self): + # expires 2018-11-16 01:15:03 UTC + leaf = _load_cert( + os.path.join("x509", "cryptography.io.pem"), + x509.load_pem_x509_certificate, + ) + + ca = _load_cert( + os.path.join("x509", "rapidssl_sha256_ca_g3.pem"), + x509.load_pem_x509_certificate, + ) + crl = _load_cert( + os.path.join("x509", "custom", "crl_empty.pem"), + x509.load_pem_x509_crl, + ) + store = Store([ca]) + + validation_time = datetime.datetime.fromisoformat( + "2018-11-16T00:00:00+00:00" + ) + + builder = PolicyBuilder().store(store).time(validation_time) + builder = builder.revocation_checker(CRLRevocationChecker([crl])) + verifier = builder.build_client_verifier() + + with pytest.raises(VerificationError, match="unimplemented"): + verifier.verify(leaf, []) + + @pytest.mark.parametrize( + ("returns", "raises", "error"), + [ + (False, None, None), + (True, None, "certificate revoked"), + ( + "Truthy", + None, + "fatal error: the revocation checker must return one of " + "True, False, or None", + ), + ( + None, + Exception("some exception"), + "fatal error: the revocation checker raised an exception", + ), + (None, None, "unable to determine revocation status"), + ], + ) + def test_verify_revocation(self, returns, raises, error): + # expires 2018-11-16 01:15:03 UTC + leaf = _load_cert( + os.path.join("x509", "cryptography.io.pem"), + x509.load_pem_x509_certificate, + ) + + ca = _load_cert( + os.path.join("x509", "rapidssl_sha256_ca_g3.pem"), + x509.load_pem_x509_certificate, + ) + store = Store([ca]) + + validation_time = datetime.datetime.fromisoformat( + "2018-11-16T00:00:00+00:00" + ) + + builder = PolicyBuilder().store(store).time(validation_time) + builder = builder.revocation_checker( + DummyRevocationChecker(returns, raises) + ) + verifier = builder.build_client_verifier() + + if error is not None: + with pytest.raises(VerificationError, match=error): + verifier.verify(leaf, []) + else: + verifier.verify(leaf, []) + def test_verify_fails_renders_oid(self): leaf = _load_cert( os.path.join("x509", "custom", "ekucrit-testuser-cert.pem"),