Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/x509.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ...
Expand Down
22 changes: 22 additions & 0 deletions src/cryptography/x509/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that we now have an ABC that inherits from the Rust class that implements the CheckRevocation trait

"""
An interface for revocation checkers.
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So right now the idea is that we won't actually have a Python API you can implement for revocation checking, it'll be handled internally and these are more markers, is that the idea?

Copy link
Copy Markdown
Contributor Author

@tnytown tnytown Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, sorry that this isn't more clear—I experimented with having an extensible Python API but it was a bit complicated to shim over considering that the implementation for CRL is going to be in Rust. I can sketch out a shim if you'd like? see latest diff which supports Python-extensible 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.
"""
Comment on lines +46 to +56
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature is a bit subtle. There are situations in which a revocation checker cannot determine the revocation status for a given certificate: for the CRL checker, the checker might not contain a relevant CRL; and for OCSP, the responder may be offline. I figure that it's best to signal this instead of collapsing that error state into "not revoked".

33 changes: 32 additions & 1 deletion src/rust/cryptography-x509-verification/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
Expand All @@ -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<'_>,
Expand Down Expand Up @@ -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());
}
}
28 changes: 27 additions & 1 deletion src/rust/cryptography-x509-verification/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod certificate;
pub mod ops;
pub mod policy;
pub mod revocation;
pub mod trust_store;
pub mod types;

Expand All @@ -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,
Expand All @@ -40,6 +42,7 @@ pub enum ValidationErrorKind<'chain, B: CryptoOps> {
reason: &'static str,
},
FatalError(&'static str),
RevocationNotDetermined,
Other(String),
}

Expand Down Expand Up @@ -91,6 +94,9 @@ impl<B: CryptoOps> 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}"),
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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>,
}

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -501,6 +523,10 @@ mod tests {
"invalid extension: 2.5.29.17: duplicate extension"
);

let err =
ValidationError::<PublicKeyErrorOps>::new(ValidationErrorKind::RevocationNotDetermined);
assert_eq!(err.to_string(), "unable to determine revocation status");

let err =
ValidationError::<PublicKeyErrorOps>::new(ValidationErrorKind::FatalError("oops"));
assert_eq!(err.to_string(), "fatal error: oops");
Expand Down
14 changes: 14 additions & 0 deletions src/rust/cryptography-x509-verification/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -86,6 +87,14 @@ pub trait CryptoOps {
/// if the key is malformed.
fn public_key(&self, cert: &Certificate<'_>) -> Result<Self::Key, Self::Err>;

/// 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>;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
52 changes: 52 additions & 0 deletions src/rust/cryptography-x509-verification/src/revocation.rs
Original file line number Diff line number Diff line change
@@ -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<B: CryptoOps> {
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<B> 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<Item = &'a CertificateRevocationList<'a>>) -> Self {
Self {
crls: crls.into_iter().collect(),
}
}
}

pub type RevocationChecker<'a, B> = dyn CheckRevocation<B> + Send + Sync + 'a;
5 changes: 3 additions & 2 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
18 changes: 8 additions & 10 deletions src/rust/src/x509/crl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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::{
big_byte_slice_to_py_int, encode_der_data, oid_to_py_oid, py_uint_to_big_endian_bytes,
};
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};

Expand Down Expand Up @@ -72,7 +74,7 @@ pub(crate) fn load_pem_x509_crl(
}

self_cell::self_cell!(
struct OwnedCertificateRevocationList {
pub(crate) struct OwnedCertificateRevocationList {
owner: pyo3::Py<pyo3::types::PyBytes>,
#[covariant]
dependent: RawCertificateRevocationList,
Expand All @@ -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<Vec<OwnedRevokedCertificate>>,
cached_extensions: pyo3::sync::PyOnceLock<pyo3::Py<pyo3::PyAny>>,
Expand Down Expand Up @@ -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())
}
}

Expand Down
Loading
Loading