From d4452997ed290d76bae724cce0a5605b5ae8c243 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 6 Sep 2024 22:43:42 -0400 Subject: [PATCH 1/4] Use uv to build `ci-constraints-requirements.txt` which hopefully makes it more maintainable (#11505) --- ci-constraints-requirements.txt | 232 ++++++++++++++++++++++++-------- pyproject.toml | 7 +- 2 files changed, 184 insertions(+), 55 deletions(-) diff --git a/ci-constraints-requirements.txt b/ci-constraints-requirements.txt index 04f7993764e1..39dd2d6a3cfb 100644 --- a/ci-constraints-requirements.txt +++ b/ci-constraints-requirements.txt @@ -1,76 +1,134 @@ -# This is named ambigiously, but it's a pip constraints file, named like a -# requirements file so dependabot will update the pins. -# It was originally generated with; -# pip-compile --extra=docs --extra=docstest --extra=pep8test --extra=test --extra=test-randomorder --extra=nox --extra=sdist --resolver=backtracking --strip-extras --unsafe-package=cffi --unsafe-package=pycparser --unsafe-package=setuptools pyproject.toml -# and then manually massaged to add version specifiers to packages whose -# versions vary by Python version - -alabaster==1.0.0 +# This file was autogenerated by uv via the following command: +# uv pip compile --universal -p 3.7 --extra=docs --extra=docstest --extra=pep8test --extra=test --extra=test-randomorder --extra=nox --extra=sdist --unsafe-package=cffi --unsafe-package=pycparser --unsafe-package=setuptools --unsafe-package=cryptography-vectors pyproject.toml +alabaster==0.7.13 ; python_full_version < '3.10' + # via sphinx +alabaster==1.0.0 ; python_full_version >= '3.10' # via sphinx -argcomplete==3.5.0; python_version >= "3.8" +argcomplete==3.1.2 ; python_full_version < '3.8' + # via nox +argcomplete==3.5.0 ; python_full_version >= '3.8' # via nox -babel==2.16.0 +babel==2.14.0 ; python_full_version < '3.8' # via sphinx -build==1.2.1 +babel==2.16.0 ; python_full_version >= '3.8' + # via sphinx +bleach==6.0.0 ; python_full_version < '3.8' + # via readme-renderer +build==1.1.1 ; python_full_version < '3.8' + # via cryptography (pyproject.toml) +build==1.2.1 ; python_full_version >= '3.8' # via - # check-sdist # cryptography (pyproject.toml) + # check-sdist certifi==2024.8.30 - # via requests + # via + # cryptography (pyproject.toml) + # requests charset-normalizer==3.3.2 # via requests -check-sdist==0.1.3 +check-sdist==0.1.3 ; python_full_version >= '3.8' # via cryptography (pyproject.toml) click==8.1.7 # via cryptography (pyproject.toml) +colorama==0.4.6 ; (platform_system != 'Windows' and sys_platform == 'win32') or platform_system == 'Windows' or os_name == 'nt' + # via + # build + # click + # colorlog + # pytest + # sphinx colorlog==6.8.2 # via nox -coverage==7.6.1; python_version >= "3.8" - # via - # coverage - # pytest-cov +coverage==7.2.7 ; python_full_version < '3.8' + # via pytest-cov +coverage==7.6.1 ; python_full_version >= '3.8' + # via pytest-cov distlib==0.3.8 # via virtualenv -docutils==0.21.2 +docutils==0.19 ; python_full_version < '3.8' + # via + # readme-renderer + # sphinx +docutils==0.20.1 ; python_full_version >= '3.8' and python_full_version < '3.10' # via # readme-renderer # sphinx # sphinx-rtd-theme -exceptiongroup==1.2.2 +docutils==0.21.2 ; python_full_version >= '3.10' + # via + # readme-renderer + # sphinx + # sphinx-rtd-theme +exceptiongroup==1.2.2 ; python_full_version < '3.11' # via pytest -execnet==2.1.1; python_version >= "3.8" +execnet==2.0.2 ; python_full_version < '3.8' # via pytest-xdist -filelock==3.15.4; python_version >= "3.8" +execnet==2.1.1 ; python_full_version >= '3.8' + # via pytest-xdist +filelock==3.12.2 ; python_full_version < '3.8' + # via virtualenv +filelock==3.15.4 ; python_full_version >= '3.8' # via virtualenv idna==3.8 # via requests imagesize==1.4.1 # via sphinx +importlib-metadata==6.7.0 ; python_full_version < '3.8' + # via + # argcomplete + # build + # click + # nox + # pluggy + # pytest + # pytest-randomly + # sphinx + # sphinxcontrib-spelling + # virtualenv +importlib-metadata==8.4.0 ; python_full_version >= '3.8' and python_full_version < '3.10.2' + # via + # build + # pytest-randomly + # sphinx +importlib-resources==6.4.4 ; python_full_version == '3.8.*' + # via check-sdist iniconfig==2.0.0 # via pytest jinja2==3.1.4 # via sphinx markupsafe==2.1.5 # via jinja2 -mypy==1.11.2 +mypy==1.4.1 ; python_full_version < '3.8' + # via cryptography (pyproject.toml) +mypy==1.11.2 ; python_full_version >= '3.8' # via cryptography (pyproject.toml) mypy-extensions==1.0.0 # via mypy -nh3==0.2.18 +nh3==0.2.18 ; python_full_version >= '3.8' # via readme-renderer nox==2024.4.15 # via cryptography (pyproject.toml) -packaging==24.1; python_version >= "3.8" +packaging==24.0 ; python_full_version < '3.8' + # via + # build + # nox + # pytest + # sphinx +packaging==24.1 ; python_full_version >= '3.8' # via # build # nox # pytest # sphinx -pathspec==0.12.1 +pathspec==0.12.1 ; python_full_version >= '3.8' # via check-sdist -platformdirs==4.2.2; python_version >= "3.8" +platformdirs==4.0.0 ; python_full_version < '3.8' + # via virtualenv +platformdirs==4.2.2 ; python_full_version >= '3.8' # via virtualenv -pluggy==1.5.0; python_version >= "3.8" +pluggy==1.2.0 ; python_full_version < '3.8' + # via pytest +pluggy==1.5.0 ; python_full_version >= '3.8' # via pytest pretend==1.0.9 # via cryptography (pyproject.toml) @@ -80,13 +138,24 @@ pyenchant==3.2.2 # via # cryptography (pyproject.toml) # sphinxcontrib-spelling -pygments==2.18.0 +pygments==2.17.2 ; python_full_version < '3.8' + # via + # readme-renderer + # sphinx +pygments==2.18.0 ; python_full_version >= '3.8' # via # readme-renderer # sphinx pyproject-hooks==1.1.0 # via build -pytest==8.3.2; python_version >= "3.8" +pytest==7.4.4 ; python_full_version < '3.8' + # via + # cryptography (pyproject.toml) + # pytest-benchmark + # pytest-cov + # pytest-randomly + # pytest-xdist +pytest==8.3.2 ; python_full_version >= '3.8' # via # cryptography (pyproject.toml) # pytest-benchmark @@ -95,64 +164,119 @@ pytest==8.3.2; python_version >= "3.8" # pytest-xdist pytest-benchmark==4.0.0 # via cryptography (pyproject.toml) -pytest-cov==5.0.0; python_version >= "3.8" +pytest-cov==4.1.0 ; python_full_version < '3.8' + # via cryptography (pyproject.toml) +pytest-cov==5.0.0 ; python_full_version >= '3.8' + # via cryptography (pyproject.toml) +pytest-randomly==3.12.0 ; python_full_version < '3.8' + # via cryptography (pyproject.toml) +pytest-randomly==3.15.0 ; python_full_version >= '3.8' + # via cryptography (pyproject.toml) +pytest-xdist==3.5.0 ; python_full_version < '3.8' # via cryptography (pyproject.toml) -pytest-randomly==3.15.0 +pytest-xdist==3.6.1 ; python_full_version >= '3.8' # via cryptography (pyproject.toml) -pytest-xdist==3.6.1; python_version >= "3.8" +pytz==2024.1 ; python_full_version < '3.9' + # via babel +readme-renderer==37.3 ; python_full_version < '3.8' # via cryptography (pyproject.toml) -readme-renderer==44.0 +readme-renderer==43.0 ; python_full_version >= '3.8' and python_full_version < '3.10' # via cryptography (pyproject.toml) -requests==2.32.3 +readme-renderer==44.0 ; python_full_version >= '3.10' + # via cryptography (pyproject.toml) +requests==2.31.0 ; python_full_version < '3.8' + # via sphinx +requests==2.32.3 ; python_full_version >= '3.8' # via sphinx ruff==0.6.4 # via cryptography (pyproject.toml) +six==1.16.0 ; python_full_version < '3.8' + # via bleach snowballstemmer==2.2.0 # via sphinx -sphinx==8.0.2 +sphinx==5.3.0 ; python_full_version < '3.8' + # via + # cryptography (pyproject.toml) + # sphinxcontrib-spelling +sphinx==7.1.2 ; python_full_version >= '3.8' and python_full_version < '3.10' # via # cryptography (pyproject.toml) # sphinx-rtd-theme - # sphinxcontrib-applehelp - # sphinxcontrib-devhelp - # sphinxcontrib-htmlhelp # sphinxcontrib-jquery - # sphinxcontrib-qthelp - # sphinxcontrib-serializinghtml # sphinxcontrib-spelling -sphinx-rtd-theme==3.0.0rc1 +sphinx==8.0.2 ; python_full_version >= '3.10' + # via + # cryptography (pyproject.toml) + # sphinx-rtd-theme + # sphinxcontrib-jquery + # sphinxcontrib-spelling +sphinx-rtd-theme==3.0.0rc1 ; python_full_version >= '3.8' # via cryptography (pyproject.toml) -sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-applehelp==1.0.2 ; python_full_version < '3.8' + # via sphinx +sphinxcontrib-applehelp==1.0.4 ; python_full_version >= '3.8' and python_full_version < '3.10' # via sphinx -sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-applehelp==2.0.0 ; python_full_version >= '3.10' # via sphinx -sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-devhelp==1.0.2 ; python_full_version < '3.10' # via sphinx -sphinxcontrib-jquery==4.1 +sphinxcontrib-devhelp==2.0.0 ; python_full_version >= '3.10' + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 ; python_full_version < '3.8' + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 ; python_full_version >= '3.8' and python_full_version < '3.10' + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 ; python_full_version >= '3.10' + # via sphinx +sphinxcontrib-jquery==4.1 ; python_full_version >= '3.8' # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-qthelp==1.0.3 ; python_full_version < '3.10' + # via sphinx +sphinxcontrib-qthelp==2.0.0 ; python_full_version >= '3.10' # via sphinx -sphinxcontrib-serializinghtml==2.0.0 +sphinxcontrib-serializinghtml==1.1.5 ; python_full_version < '3.10' + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 ; python_full_version >= '3.10' # via sphinx sphinxcontrib-spelling==8.0.0 # via cryptography (pyproject.toml) -tomli==2.0.1 +tomli==2.0.1 ; python_full_version <= '3.11' # via # build - # check-manifest + # check-sdist # coverage # mypy - # pyproject-hooks + # nox # pytest -typing-extensions==4.12.2; python_version >= "3.8" + # sphinx +typed-ast==1.5.5 ; python_full_version < '3.8' + # via mypy +typing-extensions==4.7.1 ; python_full_version < '3.8' + # via + # importlib-metadata + # mypy + # nox + # platformdirs +typing-extensions==4.12.2 ; python_full_version >= '3.8' # via mypy -urllib3==2.2.2 +urllib3==2.0.7 ; python_full_version < '3.8' + # via requests +urllib3==2.2.2 ; python_full_version >= '3.8' # via requests virtualenv==20.26.3 # via nox +webencodings==0.5.1 ; python_full_version < '3.8' + # via bleach +zipp==3.15.0 ; python_full_version < '3.8' + # via importlib-metadata +zipp==3.20.1 ; python_full_version >= '3.8' and python_full_version < '3.10.2' + # via + # importlib-metadata + # importlib-resources -# The following packages are considered to be unsafe in a requirements file: +# The following packages were excluded from the output: # cffi # pycparser +# cryptography-vectors diff --git a/pyproject.toml b/pyproject.toml index 44348415061a..4f9fab38d563 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ test = [ "certifi", ] test-randomorder = ["pytest-randomly"] -docs = ["sphinx >=5.3.0", "sphinx-rtd-theme >=3.0.0rc1"] +docs = ["sphinx >=5.3.0", "sphinx-rtd-theme >=3.0.0rc1; python_version >= '3.8'"] docstest = ["pyenchant >=1.6.11", "readme-renderer", "sphinxcontrib-spelling >=4.0.1"] sdist = ["build"] # `click` included because its needed to type check `release.py` @@ -184,3 +184,8 @@ git-only = [ ".gitattributes", ".gitignore", ] + +[tool.uv] +# These cover all Python versions, but by expressing multiple environments we +# force uv's resolver to pick the latest versions of packages for each version. +environments = ["python_version >= '3.10'", "python_version >= '3.8' and python_version < '3.10'", "python_version < '3.8'"] From 0b9082bda5081a0288fad24d1b7c2cf5749d14f3 Mon Sep 17 00:00:00 2001 From: Ivan Desiatov Date: Fri, 6 Sep 2024 17:20:35 +0200 Subject: [PATCH 2/4] Add CustomPolicyBuilder foundation. --- .../hazmat/bindings/_rust/x509.pyi | 16 +- src/cryptography/x509/verification.py | 2 + .../src/policy/extension.rs | 5 +- .../src/policy/mod.rs | 4 +- src/rust/cryptography-x509/src/oid.rs | 11 + src/rust/src/lib.rs | 4 +- src/rust/src/x509/verify.rs | 334 +++++++++++++----- tests/x509/verification/test_limbo.py | 17 +- tests/x509/verification/test_verification.py | 125 +++++-- 9 files changed, 392 insertions(+), 126 deletions(-) diff --git a/src/cryptography/hazmat/bindings/_rust/x509.pyi b/src/cryptography/hazmat/bindings/_rust/x509.pyi index aa85657fcfd8..d689c485b683 100644 --- a/src/cryptography/hazmat/bindings/_rust/x509.pyi +++ b/src/cryptography/hazmat/bindings/_rust/x509.pyi @@ -67,9 +67,23 @@ class PolicyBuilder: self, subject: x509.verification.Subject ) -> ServerVerifier: ... +class CustomPolicyBuilder: + def time(self, new_time: datetime.datetime) -> CustomPolicyBuilder: ... + def store(self, new_store: Store) -> CustomPolicyBuilder: ... + def max_chain_depth( + self, new_max_chain_depth: int + ) -> CustomPolicyBuilder: ... + def eku(self, new_eku: x509.ObjectIdentifier) -> CustomPolicyBuilder: ... + def build_client_verifier(self) -> ClientVerifier: ... + def build_server_verifier( + self, subject: x509.verification.Subject + ) -> ServerVerifier: ... + class VerifiedClient: @property - def subjects(self) -> list[x509.GeneralName]: ... + def subject(self) -> x509.Name: ... + @property + def sans(self) -> list[x509.GeneralName] | None: ... @property def chain(self) -> list[x509.Certificate]: ... diff --git a/src/cryptography/x509/verification.py b/src/cryptography/x509/verification.py index b83650681237..fe0153581f85 100644 --- a/src/cryptography/x509/verification.py +++ b/src/cryptography/x509/verification.py @@ -12,6 +12,7 @@ __all__ = [ "ClientVerifier", "PolicyBuilder", + "CustomPolicyBuilder", "ServerVerifier", "Store", "Subject", @@ -25,4 +26,5 @@ ClientVerifier = rust_x509.ClientVerifier ServerVerifier = rust_x509.ServerVerifier PolicyBuilder = rust_x509.PolicyBuilder +CustomPolicyBuilder = rust_x509.CustomPolicyBuilder VerificationError = rust_x509.VerificationError diff --git a/src/rust/cryptography-x509-verification/src/policy/extension.rs b/src/rust/cryptography-x509-verification/src/policy/extension.rs index a01eb490122b..df53c50507a5 100644 --- a/src/rust/cryptography-x509-verification/src/policy/extension.rs +++ b/src/rust/cryptography-x509-verification/src/policy/extension.rs @@ -14,7 +14,8 @@ use cryptography_x509::{ use crate::{ops::CryptoOps, policy::Policy, ValidationError}; -pub(crate) struct ExtensionPolicy { +#[derive(Clone)] +pub struct ExtensionPolicy { pub(crate) authority_information_access: ExtensionValidator, pub(crate) authority_key_identifier: ExtensionValidator, pub(crate) subject_key_identifier: ExtensionValidator, @@ -123,6 +124,7 @@ impl ExtensionPolicy { } /// Represents different criticality states for an extension. +#[derive(Clone)] pub(crate) enum Criticality { /// The extension MUST be marked as critical. Critical, @@ -151,6 +153,7 @@ type MaybeExtensionValidatorCallback = fn(&Policy<'_, B>, &Certificate<'_>, Option<&Extension<'_>>) -> Result<(), ValidationError>; /// Represents different validation states for an extension. +#[derive(Clone)] pub(crate) enum ExtensionValidator { /// The extension MUST NOT be present. NotPresent, diff --git a/src/rust/cryptography-x509-verification/src/policy/mod.rs b/src/rust/cryptography-x509-verification/src/policy/mod.rs index 5616a83a8ceb..a89511fd6d69 100644 --- a/src/rust/cryptography-x509-verification/src/policy/mod.rs +++ b/src/rust/cryptography-x509-verification/src/policy/mod.rs @@ -25,10 +25,12 @@ use cryptography_x509::oid::{ use once_cell::sync::Lazy; use crate::ops::CryptoOps; -use crate::policy::extension::{ca, common, ee, Criticality, ExtensionPolicy, ExtensionValidator}; +use crate::policy::extension::{ca, common, ee, Criticality, ExtensionValidator}; use crate::types::{DNSName, DNSPattern, IPAddress}; use crate::{ValidationError, VerificationCertificate}; +pub use crate::policy::extension::ExtensionPolicy; + // RSA key constraints, as defined in CA/B 6.1.5. static WEBPKI_MINIMUM_RSA_MODULUS: usize = 2048; diff --git a/src/rust/cryptography-x509/src/oid.rs b/src/rust/cryptography-x509/src/oid.rs index fbc440eea122..c29cd42de2ee 100644 --- a/src/rust/cryptography-x509/src/oid.rs +++ b/src/rust/cryptography-x509/src/oid.rs @@ -148,6 +148,17 @@ pub const EKU_ANY_KEY_USAGE_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 3 pub const EKU_CERTIFICATE_TRANSPARENCY_OID: asn1::ObjectIdentifier = asn1::oid!(1, 3, 6, 1, 4, 1, 11129, 2, 4, 4); +pub const ALL_EKU_OIDS: [asn1::ObjectIdentifier; 8] = [ + EKU_SERVER_AUTH_OID, + EKU_CLIENT_AUTH_OID, + EKU_CODE_SIGNING_OID, + EKU_EMAIL_PROTECTION_OID, + EKU_TIME_STAMPING_OID, + EKU_OCSP_SIGNING_OID, + EKU_ANY_KEY_USAGE_OID, + EKU_CERTIFICATE_TRANSPARENCY_OID, +]; + pub const PBES2_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 5, 13); pub const PBKDF2_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 5, 12); diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index cd7b99f1570a..1c4bc3cb24cd 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -132,8 +132,8 @@ mod _rust { use crate::x509::sct::Sct; #[pymodule_export] use crate::x509::verify::{ - PolicyBuilder, PyClientVerifier, PyServerVerifier, PyStore, PyVerifiedClient, - VerificationError, + CustomPolicyBuilder, PolicyBuilder, PyClientVerifier, PyServerVerifier, PyStore, + PyVerifiedClient, VerificationError, }; } diff --git a/src/rust/src/x509/verify.rs b/src/rust/src/x509/verify.rs index dbc9f18770af..045c2d430a8e 100644 --- a/src/rust/src/x509/verify.rs +++ b/src/rust/src/x509/verify.rs @@ -3,25 +3,31 @@ // for complete details. use cryptography_x509::{ - certificate::Certificate, extensions::SubjectAlternativeName, oid::SUBJECT_ALTERNATIVE_NAME_OID, + certificate::Certificate, + extensions::SubjectAlternativeName, + oid::{ALL_EKU_OIDS, SUBJECT_ALTERNATIVE_NAME_OID}, }; use cryptography_x509_verification::{ ops::{CryptoOps, VerificationCertificate}, - policy::{Policy, Subject}, + policy::{ExtensionPolicy, Policy, Subject}, trust_store::Store, types::{DNSName, IPAddress}, }; -use pyo3::types::{PyAnyMethods, PyListMethods}; +use pyo3::{ + types::{PyAnyMethods, PyListMethods}, + ToPyObject, +}; -use crate::backend::keys; use crate::error::{CryptographyError, CryptographyResult}; use crate::types; use crate::x509::certificate::Certificate as PyCertificate; use crate::x509::common::{datetime_now, datetime_to_py, py_to_datetime}; use crate::x509::sign; +use crate::{asn1::py_oid_to_oid, backend::keys}; use super::parse_general_names; +#[derive(Clone)] pub(crate) struct PyCryptoOps {} impl CryptoOps for PyCryptoOps { @@ -54,6 +60,20 @@ pyo3::create_exception!( pyo3::exceptions::PyException ); +macro_rules! policy_builder_set_once_check { + ($self: ident, $property: ident, $human_readable_name: literal) => { + if $self.$property.is_some() { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err(concat!( + "The ", + $human_readable_name, + " may only be set once." + )), + )); + } + }; +} + #[pyo3::pyclass(frozen, module = "cryptography.x509.verification")] pub(crate) struct PolicyBuilder { time: Option, @@ -77,13 +97,8 @@ impl PolicyBuilder { py: pyo3::Python<'_>, new_time: pyo3::Bound<'_, pyo3::PyAny>, ) -> CryptographyResult { - if self.time.is_some() { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "The validation time may only be set once.", - ), - )); - } + policy_builder_set_once_check!(self, time, "validation time"); + Ok(PolicyBuilder { time: Some(py_to_datetime(py, new_time)?), store: self.store.as_ref().map(|s| s.clone_ref(py)), @@ -92,11 +107,8 @@ impl PolicyBuilder { } fn store(&self, new_store: pyo3::Py) -> CryptographyResult { - if self.store.is_some() { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err("The trust store may only be set once."), - )); - } + policy_builder_set_once_check!(self, store, "trust store"); + Ok(PolicyBuilder { time: self.time.clone(), store: Some(new_store), @@ -109,13 +121,8 @@ impl PolicyBuilder { py: pyo3::Python<'_>, new_max_chain_depth: u8, ) -> CryptographyResult { - if self.max_chain_depth.is_some() { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "The maximum chain depth may only be set once.", - ), - )); - } + policy_builder_set_once_check!(self, max_chain_depth, "maximum chain depth"); + Ok(PolicyBuilder { time: self.time.clone(), store: self.store.as_ref().map(|s| s.clone_ref(py)), @@ -124,25 +131,128 @@ impl PolicyBuilder { } fn build_client_verifier(&self, py: pyo3::Python<'_>) -> CryptographyResult { - let store = match self.store.as_ref() { - Some(s) => s.clone_ref(py), - None => { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "A client verifier must have a trust store.", - ), - )); - } - }; + build_client_verifier_impl(py, &self.store, &self.time, |time| { + Policy::client(PyCryptoOps {}, time, self.max_chain_depth) + }) + } + + fn build_server_verifier( + &self, + py: pyo3::Python<'_>, + subject: pyo3::PyObject, + ) -> CryptographyResult { + build_server_verifier_impl(py, &self.store, &self.time, subject, |subject, time| { + Policy::server(PyCryptoOps {}, subject, time, self.max_chain_depth) + }) + } +} - let time = match self.time.as_ref() { - Some(t) => t.clone(), - None => datetime_now(py)?, - }; +#[pyo3::pyclass(frozen, module = "cryptography.x509.verification")] +pub(crate) struct CustomPolicyBuilder { + time: Option, + store: Option>, + max_chain_depth: Option, + eku: Option, + ca_ext_policy: Option>, + ee_ext_policy: Option>, +} - let policy = PyCryptoPolicy(Policy::client(PyCryptoOps {}, time, self.max_chain_depth)); +impl CustomPolicyBuilder { + /// Clones the builder, requires the GIL token to increase + /// reference count for `self.store`. + fn py_clone(&self, py: pyo3::Python<'_>) -> CustomPolicyBuilder { + CustomPolicyBuilder { + time: self.time.clone(), + store: self.store.as_ref().map(|s| s.clone_ref(py)), + max_chain_depth: self.max_chain_depth, + eku: self.eku.clone(), + ca_ext_policy: self.ca_ext_policy.clone(), + ee_ext_policy: self.ee_ext_policy.clone(), + } + } +} - Ok(PyClientVerifier { policy, store }) +#[pyo3::pymethods] +impl CustomPolicyBuilder { + #[new] + fn new() -> CustomPolicyBuilder { + CustomPolicyBuilder { + time: None, + store: None, + max_chain_depth: None, + eku: None, + ca_ext_policy: None, + ee_ext_policy: None, + } + } + + fn time( + &self, + py: pyo3::Python<'_>, + new_time: pyo3::Bound<'_, pyo3::PyAny>, + ) -> CryptographyResult { + policy_builder_set_once_check!(self, time, "validation time"); + + Ok(CustomPolicyBuilder { + time: Some(py_to_datetime(py, new_time)?), + ..self.py_clone(py) + }) + } + + fn store( + &self, + py: pyo3::Python<'_>, + new_store: pyo3::Py, + ) -> CryptographyResult { + policy_builder_set_once_check!(self, store, "trust store"); + + Ok(CustomPolicyBuilder { + store: Some(new_store), + ..self.py_clone(py) + }) + } + + fn max_chain_depth( + &self, + py: pyo3::Python<'_>, + new_max_chain_depth: u8, + ) -> CryptographyResult { + policy_builder_set_once_check!(self, max_chain_depth, "maximum chain depth"); + + Ok(CustomPolicyBuilder { + max_chain_depth: Some(new_max_chain_depth), + ..self.py_clone(py) + }) + } + + fn eku( + &self, + py: pyo3::Python<'_>, + new_eku: pyo3::Bound<'_, pyo3::PyAny>, + ) -> CryptographyResult { + policy_builder_set_once_check!(self, eku, "EKU"); + + let oid = py_oid_to_oid(new_eku)?; + + if !ALL_EKU_OIDS.contains(&oid) { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "Unknown EKU OID. Only EKUs from x509.ExtendedKeyUsageOID are supported.", + ), + )); + } + + Ok(CustomPolicyBuilder { + eku: Some(oid), + ..self.py_clone(py) + }) + } + + fn build_client_verifier(&self, py: pyo3::Python<'_>) -> CryptographyResult { + build_client_verifier_impl(py, &self.store, &self.time, |time| { + // TODO: Replace with a custom policy once it's implemented in cryptography-x509-verification + Policy::client(PyCryptoOps {}, time, self.max_chain_depth) + }) } fn build_server_verifier( @@ -150,42 +260,80 @@ impl PolicyBuilder { py: pyo3::Python<'_>, subject: pyo3::PyObject, ) -> CryptographyResult { - let store = match self.store.as_ref() { - Some(s) => s.clone_ref(py), - None => { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "A server verifier must have a trust store.", - ), - )); - } - }; - - let time = match self.time.as_ref() { - Some(t) => t.clone(), - None => datetime_now(py)?, - }; - let subject_owner = build_subject_owner(py, &subject)?; - - let policy = OwnedPolicy::try_new(subject_owner, |subject_owner| { - let subject = build_subject(py, subject_owner)?; - Ok::, pyo3::PyErr>(PyCryptoPolicy(Policy::server( - PyCryptoOps {}, - subject, - time, - self.max_chain_depth, - ))) - })?; - - Ok(PyServerVerifier { - py_subject: subject, - policy, - store, + build_server_verifier_impl(py, &self.store, &self.time, subject, |subject, time| { + // TODO: Replace with a custom policy once it's implemented in cryptography-x509-verification + Policy::server(PyCryptoOps {}, subject, time, self.max_chain_depth) }) } } -struct PyCryptoPolicy<'a>(Policy<'a, PyCryptoOps>); +/// This is a helper to avoid code duplication between PolicyBuilder and CustomPolicyBuilder. +fn build_server_verifier_impl( + py: pyo3::Python<'_>, + store: &Option>, + time: &Option, + subject: pyo3::PyObject, + make_policy: impl Fn(Subject<'_>, asn1::DateTime) -> PyCryptoPolicy<'_>, +) -> CryptographyResult { + let store = match store { + Some(s) => s.clone_ref(py), + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "A server verifier must have a trust store.", + ), + )); + } + }; + + let time = match time.as_ref() { + Some(t) => t.clone(), + None => datetime_now(py)?, + }; + let subject_owner = build_subject_owner(py, &subject)?; + + let policy = OwnedPolicy::try_new(subject_owner, |subject_owner| { + let subject = build_subject(py, subject_owner)?; + Ok::, pyo3::PyErr>(make_policy(subject, time)) + })?; + + Ok(PyServerVerifier { + py_subject: subject, + policy, + store, + }) +} + +/// This is a helper to avoid code duplication between PolicyBuilder and CustomPolicyBuilder. +fn build_client_verifier_impl( + py: pyo3::Python<'_>, + store: &Option>, + time: &Option, + make_policy: impl Fn(asn1::DateTime) -> PyCryptoPolicy<'static>, +) -> CryptographyResult { + let store = match store.as_ref() { + Some(s) => s.clone_ref(py), + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "A client verifier must have a trust store.", + ), + )); + } + }; + + let time = match time.as_ref() { + Some(t) => t.clone(), + None => datetime_now(py)?, + }; + + Ok(PyClientVerifier { + policy: make_policy(time), + store, + }) +} + +type PyCryptoPolicy<'a> = Policy<'a, PyCryptoOps>; /// This enum exists solely to provide heterogeneously typed ownership for `OwnedPolicy`. enum SubjectOwner { @@ -214,7 +362,9 @@ self_cell::self_cell!( )] pub(crate) struct PyVerifiedClient { #[pyo3(get)] - subjects: pyo3::Py, + subject: pyo3::Py, + #[pyo3(get)] + sans: Option>, #[pyo3(get)] chain: pyo3::Py, } @@ -232,7 +382,7 @@ pub(crate) struct PyClientVerifier { impl PyClientVerifier { fn as_policy(&self) -> &Policy<'_, PyCryptoOps> { - &self.policy.0 + &self.policy } } @@ -289,22 +439,32 @@ impl PyClientVerifier { py_chain.append(c.extra())?; } - // NOTE: These `unwrap()`s cannot fail, since the underlying policy - // enforces the presence of a SAN and the well-formedness of the - // extension set. - let leaf_san = &chain[0] - .certificate() - .extensions() - .ok() - .unwrap() - .get_extension(&SUBJECT_ALTERNATIVE_NAME_OID) - .unwrap(); + let cert = &chain[0].certificate(); + + let py_sans = || -> pyo3::PyResult> { + let leaf_san_ext = cert + .extensions() + .ok() + .unwrap() + .get_extension(&SUBJECT_ALTERNATIVE_NAME_OID); + + match leaf_san_ext { + Some(leaf_san) => { + let leaf_gns = leaf_san + .value::>() + .map_err(|e| -> CryptographyError { e.into() })?; + let py_gns = parse_general_names(py, &leaf_gns)?; + Ok(Some(py_gns)) + } + None => Ok(None), + } + }()?; - let leaf_gns = leaf_san.value::>()?; - let py_gns = parse_general_names(py, &leaf_gns)?; + let py_subject = crate::x509::parse_name(py, cert.subject())?; Ok(PyVerifiedClient { - subjects: py_gns, + subject: py_subject.to_object(py), + sans: py_sans, chain: py_chain.unbind(), }) } @@ -325,7 +485,7 @@ pub(crate) struct PyServerVerifier { impl PyServerVerifier { fn as_policy(&self) -> &Policy<'_, PyCryptoOps> { - &self.policy.borrow_dependent().0 + self.policy.borrow_dependent() } } diff --git a/tests/x509/verification/test_limbo.py b/tests/x509/verification/test_limbo.py index d0402c4ce30a..114405f2393e 100644 --- a/tests/x509/verification/test_limbo.py +++ b/tests/x509/verification/test_limbo.py @@ -6,6 +6,7 @@ import ipaddress import json import os +from typing import Type, Union import pytest @@ -13,6 +14,7 @@ from cryptography.x509 import load_pem_x509_certificate from cryptography.x509.verification import ( ClientVerifier, + CustomPolicyBuilder, PolicyBuilder, ServerVerifier, Store, @@ -96,7 +98,11 @@ def _get_limbo_peer(expected_peer): return x509.RFC822Name(value) -def _limbo_testcase(id_, testcase): +def _limbo_testcase( + id_, + testcase, + builder_type: Union[Type[PolicyBuilder], Type[CustomPolicyBuilder]], +): if id_ in LIMBO_SKIP_TESTCASES: pytest.skip(f"explicitly skipped testcase: {id_}") @@ -127,7 +133,7 @@ def _limbo_testcase(id_, testcase): max_chain_depth = testcase["max_chain_depth"] should_pass = testcase["expected_result"] == "SUCCESS" - builder = PolicyBuilder().store(Store(trusted_certs)) + builder = builder_type().store(Store(trusted_certs)) if validation_time is not None: builder = builder.time(validation_time) if max_chain_depth is not None: @@ -163,7 +169,7 @@ def _limbo_testcase(id_, testcase): expected_subjects = [ _get_limbo_peer(p) for p in testcase["expected_peer_names"] ] - assert expected_subjects == verified_client.subjects + assert expected_subjects == verified_client.sans built_chain = verified_client.chain @@ -177,7 +183,8 @@ def _limbo_testcase(id_, testcase): verifier.verify(peer_certificate, untrusted_intermediates) -def test_limbo(subtests, pytestconfig): +@pytest.mark.parametrize("builder_type", [PolicyBuilder, CustomPolicyBuilder]) +def test_limbo(subtests, pytestconfig, builder_type): limbo_root = pytestconfig.getoption("--x509-limbo-root", skip=True) limbo_path = os.path.join(limbo_root, "limbo.json") with open(limbo_path, mode="rb") as limbo_file: @@ -187,4 +194,4 @@ def test_limbo(subtests, pytestconfig): with subtests.test(): # NOTE: Pass in the id separately to make pytest # error renderings slightly nicer. - _limbo_testcase(testcase["id"], testcase) + _limbo_testcase(testcase["id"], testcase, builder_type) diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index f5e70bab3538..c9875b930b12 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -6,12 +6,18 @@ import os from functools import lru_cache from ipaddress import IPv4Address +from typing import Type, Union import pytest from cryptography import x509 from cryptography.x509.general_name import DNSName, IPAddress +from cryptography.x509.oid import ( + AuthorityInformationAccessOID, + ExtendedKeyUsageOID, +) from cryptography.x509.verification import ( + CustomPolicyBuilder, PolicyBuilder, Store, VerificationError, @@ -28,60 +34,71 @@ def dummy_store() -> Store: return Store([cert]) -class TestPolicyBuilder: - def test_time_already_set(self): +AnyPolicyBuilder = Union[PolicyBuilder, CustomPolicyBuilder] + + +@pytest.mark.parametrize("builder_type", [PolicyBuilder, CustomPolicyBuilder]) +class TestPolicyBuilderCommon: + """ + Tests functionality that is identical between + PolicyBuilder and CustomPolicyBuilder. + """ + + def test_time_already_set(self, builder_type: Type[AnyPolicyBuilder]): with pytest.raises(ValueError): - PolicyBuilder().time(datetime.datetime.now()).time( + builder_type().time(datetime.datetime.now()).time( datetime.datetime.now() ) - def test_store_already_set(self): + def test_store_already_set(self, builder_type: Type[AnyPolicyBuilder]): with pytest.raises(ValueError): - PolicyBuilder().store(dummy_store()).store(dummy_store()) + builder_type().store(dummy_store()).store(dummy_store()) - def test_max_chain_depth_already_set(self): + def test_max_chain_depth_already_set( + self, builder_type: Type[AnyPolicyBuilder] + ): with pytest.raises(ValueError): - PolicyBuilder().max_chain_depth(8).max_chain_depth(9) + builder_type().max_chain_depth(8).max_chain_depth(9) - def test_ipaddress_subject(self): + def test_ipaddress_subject(self, builder_type: Type[AnyPolicyBuilder]): policy = ( - PolicyBuilder() + builder_type() .store(dummy_store()) .build_server_verifier(IPAddress(IPv4Address("0.0.0.0"))) ) assert policy.subject == IPAddress(IPv4Address("0.0.0.0")) - def test_dnsname_subject(self): + def test_dnsname_subject(self, builder_type: Type[AnyPolicyBuilder]): policy = ( - PolicyBuilder() + builder_type() .store(dummy_store()) .build_server_verifier(DNSName("cryptography.io")) ) assert policy.subject == DNSName("cryptography.io") - def test_subject_bad_types(self): + def test_subject_bad_types(self, builder_type: Type[AnyPolicyBuilder]): # Subject must be a supported GeneralName type with pytest.raises(TypeError): - PolicyBuilder().store(dummy_store()).build_server_verifier( + builder_type().store(dummy_store()).build_server_verifier( "cryptography.io" # type: ignore[arg-type] ) with pytest.raises(TypeError): - PolicyBuilder().store(dummy_store()).build_server_verifier( + builder_type().store(dummy_store()).build_server_verifier( "0.0.0.0" # type: ignore[arg-type] ) with pytest.raises(TypeError): - PolicyBuilder().store(dummy_store()).build_server_verifier( + builder_type().store(dummy_store()).build_server_verifier( IPv4Address("0.0.0.0") # type: ignore[arg-type] ) with pytest.raises(TypeError): - PolicyBuilder().store(dummy_store()).build_server_verifier(None) # type: ignore[arg-type] + builder_type().store(dummy_store()).build_server_verifier(None) # type: ignore[arg-type] - def test_builder_pattern(self): + def test_builder_pattern(self, builder_type: Type[AnyPolicyBuilder]): now = datetime.datetime.now().replace(microsecond=0) store = dummy_store() max_chain_depth = 16 - builder = PolicyBuilder() + builder = builder_type() builder = builder.time(now) builder = builder.store(store) builder = builder.max_chain_depth(max_chain_depth) @@ -92,11 +109,29 @@ def test_builder_pattern(self): assert verifier.store == store assert verifier.max_chain_depth == max_chain_depth - def test_build_server_verifier_missing_store(self): + def test_build_server_verifier_missing_store( + self, builder_type: Type[AnyPolicyBuilder] + ): with pytest.raises( ValueError, match="A server verifier must have a trust store" ): - PolicyBuilder().build_server_verifier(DNSName("cryptography.io")) + builder_type().build_server_verifier(DNSName("cryptography.io")) + + +class TestCustomPolicyBuilder: + def test_eku_already_set(self): + with pytest.raises(ValueError): + CustomPolicyBuilder().eku(ExtendedKeyUsageOID.IPSEC_IKE).eku( + ExtendedKeyUsageOID.IPSEC_IKE + ) + + def test_eku_bad_type(self): + with pytest.raises(TypeError): + CustomPolicyBuilder().eku("not an OID") # type: ignore[arg-type] + + def test_eku_non_eku_oid(self): + with pytest.raises(ValueError): + CustomPolicyBuilder().eku(AuthorityInformationAccessOID.OCSP) class TestStore: @@ -109,14 +144,23 @@ def test_store_rejects_non_certificates(self): Store(["not a cert"]) # type: ignore[list-item] +@pytest.mark.parametrize( + "builder_type", + [ + PolicyBuilder, + CustomPolicyBuilder, + ], +) class TestClientVerifier: - def test_build_client_verifier_missing_store(self): + def test_build_client_verifier_missing_store( + self, builder_type: Type[AnyPolicyBuilder] + ): with pytest.raises( ValueError, match="A client verifier must have a trust store" ): - PolicyBuilder().build_client_verifier() + builder_type().build_client_verifier() - def test_verify(self): + def test_verify(self, builder_type: Type[AnyPolicyBuilder]): # expires 2018-11-16 01:15:03 UTC leaf = _load_cert( os.path.join("x509", "cryptography.io.pem"), @@ -128,7 +172,7 @@ def test_verify(self): validation_time = datetime.datetime.fromisoformat( "2018-11-16T00:00:00+00:00" ) - builder = PolicyBuilder().store(store) + builder = builder_type().store(store) builder = builder.time(validation_time).max_chain_depth(16) verifier = builder.build_client_verifier() @@ -139,11 +183,34 @@ def test_verify(self): verified_client = verifier.verify(leaf, []) assert verified_client.chain == [leaf] - assert x509.DNSName("www.cryptography.io") in verified_client.subjects - assert x509.DNSName("cryptography.io") in verified_client.subjects - assert len(verified_client.subjects) == 2 + expected_subject = x509.Name( + [ + x509.NameAttribute( + x509.NameOID.ORGANIZATIONAL_UNIT_NAME, "GT48742965" + ), + x509.NameAttribute( + x509.NameOID.ORGANIZATIONAL_UNIT_NAME, + "See www.rapidssl.com/resources/cps (c)14", + ), + x509.NameAttribute( + x509.NameOID.ORGANIZATIONAL_UNIT_NAME, + "Domain Control Validated - RapidSSL(R)", + ), + x509.NameAttribute( + x509.NameOID.COMMON_NAME, "www.cryptography.io" + ), + ] + ) + assert verified_client.subject == expected_subject + assert verified_client.sans is not None + assert x509.DNSName("www.cryptography.io") in verified_client.sans + assert x509.DNSName("cryptography.io") in verified_client.sans + + assert len(verified_client.sans) == 2 - def test_verify_fails_renders_oid(self): + def test_verify_fails_renders_oid( + self, builder_type: Type[AnyPolicyBuilder] + ): leaf = _load_cert( os.path.join("x509", "custom", "ekucrit-testuser-cert.pem"), x509.load_pem_x509_certificate, @@ -155,7 +222,7 @@ def test_verify_fails_renders_oid(self): "2024-06-26T00:00:00+00:00" ) - builder = PolicyBuilder().store(store) + builder = builder_type().store(store) builder = builder.time(validation_time) verifier = builder.build_client_verifier() From adc39ba3233ce091f029b653913c6e10d67af009 Mon Sep 17 00:00:00 2001 From: Ivan Desiatov Date: Sat, 7 Sep 2024 10:05:19 +0200 Subject: [PATCH 3/4] Add EKU getters to ClientVerifier and ServerVerifier. --- src/cryptography/hazmat/bindings/_rust/x509.pyi | 4 ++++ src/rust/src/x509/verify.rs | 17 ++++++++++++++++- tests/x509/verification/test_verification.py | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/cryptography/hazmat/bindings/_rust/x509.pyi b/src/cryptography/hazmat/bindings/_rust/x509.pyi index d689c485b683..bad8c9c285cc 100644 --- a/src/cryptography/hazmat/bindings/_rust/x509.pyi +++ b/src/cryptography/hazmat/bindings/_rust/x509.pyi @@ -94,6 +94,8 @@ class ClientVerifier: def store(self) -> Store: ... @property def max_chain_depth(self) -> int: ... + @property + def eku(self) -> x509.ObjectIdentifier: ... def verify( self, leaf: x509.Certificate, @@ -109,6 +111,8 @@ class ServerVerifier: def store(self) -> Store: ... @property def max_chain_depth(self) -> int: ... + @property + def eku(self) -> x509.ObjectIdentifier: ... def verify( self, leaf: x509.Certificate, diff --git a/src/rust/src/x509/verify.rs b/src/rust/src/x509/verify.rs index 045c2d430a8e..7a8026b51865 100644 --- a/src/rust/src/x509/verify.rs +++ b/src/rust/src/x509/verify.rs @@ -18,11 +18,14 @@ use pyo3::{ ToPyObject, }; -use crate::error::{CryptographyError, CryptographyResult}; use crate::types; use crate::x509::certificate::Certificate as PyCertificate; use crate::x509::common::{datetime_now, datetime_to_py, py_to_datetime}; use crate::x509::sign; +use crate::{ + asn1::oid_to_py_oid, + error::{CryptographyError, CryptographyResult}, +}; use crate::{asn1::py_oid_to_oid, backend::keys}; use super::parse_general_names; @@ -401,6 +404,12 @@ impl PyClientVerifier { self.as_policy().max_chain_depth } + #[getter] + fn eku(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { + let eku = &self.as_policy().extended_key_usage; + return Ok(oid_to_py_oid(py, eku)?.as_unbound().clone_ref(py)); + } + fn verify( &self, py: pyo3::Python<'_>, @@ -504,6 +513,12 @@ impl PyServerVerifier { self.as_policy().max_chain_depth } + #[getter] + fn eku(&self, py: pyo3::Python<'_>) -> pyo3::PyResult> { + let eku = &self.as_policy().extended_key_usage; + return Ok(oid_to_py_oid(py, eku)?.as_unbound().clone_ref(py)); + } + fn verify<'p>( &self, py: pyo3::Python<'p>, diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index c9875b930b12..8b89112b6654 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -108,6 +108,7 @@ def test_builder_pattern(self, builder_type: Type[AnyPolicyBuilder]): assert verifier.validation_time == now assert verifier.store == store assert verifier.max_chain_depth == max_chain_depth + assert verifier.eku == ExtendedKeyUsageOID.SERVER_AUTH def test_build_server_verifier_missing_store( self, builder_type: Type[AnyPolicyBuilder] @@ -179,6 +180,7 @@ def test_verify(self, builder_type: Type[AnyPolicyBuilder]): assert verifier.validation_time == validation_time.replace(tzinfo=None) assert verifier.max_chain_depth == 16 assert verifier.store is store + assert verifier.eku == ExtendedKeyUsageOID.CLIENT_AUTH verified_client = verifier.verify(leaf, []) assert verified_client.chain == [leaf] From 085263030990f3c958742140e9567dc415a11b87 Mon Sep 17 00:00:00 2001 From: Ivan Desiatov Date: Sat, 7 Sep 2024 10:58:36 +0200 Subject: [PATCH 4/4] Document the implemented part of custom verification. --- docs/spelling_wordlist.txt | 1 + docs/x509/verification.rst | 121 ++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 6a0282266821..f8e6d4232ae0 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -140,6 +140,7 @@ unencrypted unicode unpadded unpadding +validator Ventura verifier Verifier diff --git a/docs/x509/verification.rst b/docs/x509/verification.rst index b0e1daee2994..36ca253e7930 100644 --- a/docs/x509/verification.rst +++ b/docs/x509/verification.rst @@ -111,12 +111,22 @@ the root of trust: .. versionadded:: 43.0.0 - .. attribute:: subjects + .. versionchanged:: 44.0.0 + Renamed `subjects` to :attr:`sans`. + Made `sans` optional, added :attr:`subject`. - :type: list of :class:`~cryptography.x509.GeneralName` + .. attribute:: subject + + :type: :class:`~cryptography.x509.Name` + + The subject presented in the verified client's certificate. + + .. attribute:: sans + + :type: list of :class:`~cryptography.x509.GeneralName` or None The subjects presented in the verified client's Subject Alternative Name - extension. + extension or `None` if the extension is not present. .. attribute:: chain @@ -129,6 +139,8 @@ the root of trust: .. class:: ClientVerifier .. versionadded:: 43.0.0 + .. versionchanged:: 44.0.0 + Added :attr:`eku`. A ClientVerifier verifies client certificates. @@ -156,6 +168,18 @@ the root of trust: :type: :class:`Store` The verifier's trust store. + + .. attribute:: eku + + :type: :class:`~cryptography.x509.ObjectIdentifier` or None + + The value of the Extended Key Usage extension required by this verifier + If the verifier was built using :meth:`PolicyBuilder.build_client_verifier`, + this will always be :attr:`~cryptography.x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH`. + + :note: + See :meth:`CustomPolicyBuilder.eku` documentation for how verification is affected + when changing the required EKU or using a custom extension policy. .. method:: verify(leaf, intermediates) @@ -212,6 +236,18 @@ the root of trust: The verifier's trust store. + .. attribute:: eku + + :type: :class:`~cryptography.x509.ObjectIdentifier` + + The value of the Extended Key Usage extension required by this verifier + If the verifier was built using :meth:`PolicyBuilder.build_server_verifier`, + this will always be :attr:`~cryptography.x509.oid.ExtendedKeyUsageOID.SERVER_AUTH`. + + :note: + See :meth:`CustomPolicyBuilder.eku` documentation for how verification is affected + when changing the required EKU or using a custom extension policy. + .. method:: verify(leaf, intermediates) Performs path validation on ``leaf``, returning a valid path @@ -294,3 +330,82 @@ the root of trust: for server verification. :returns: An instance of :class:`ClientVerifier` + +.. class:: CustomPolicyBuilder + + .. versionadded:: 44.0.0 + + A CustomPolicyBuilder provides a builder-style interface for constructing a + Verifier, but provides additional control over the verification policy compared to :class:`PolicyBuilder`. + + .. method:: time(new_time) + + Sets the verifier's verification time. + + If not called explicitly, this is set to :meth:`datetime.datetime.now` + when :meth:`build_server_verifier` or :meth:`build_client_verifier` + is called. + + :param new_time: The :class:`datetime.datetime` to use in the verifier + + :returns: A new instance of :class:`PolicyBuilder` + + .. method:: store(new_store) + + Sets the verifier's trust store. + + :param new_store: The :class:`Store` to use in the verifier + + :returns: A new instance of :class:`PolicyBuilder` + + .. method:: max_chain_depth(new_max_chain_depth) + + Sets the verifier's maximum chain building depth. + + This depth behaves tracks the length of the intermediate CA + chain: a maximum depth of zero means that the leaf must be directly + issued by a member of the store, a depth of one means no more than + one intermediate CA, and so forth. Note that self-issued intermediates + don't count against the chain depth, per RFC 5280. + + :param new_max_chain_depth: The maximum depth to allow in the verifier + + :returns: A new instance of :class:`PolicyBuilder` + + .. method:: eku(new_eku) + + Sets the Extended Key Usage required by the verifier's policy. + + If this method is not called, the EKU defaults to :attr:`~cryptography.x509.oid.ExtendedKeyUsageOID.SERVER_AUTH` + if :meth:`build_server_verifier` is called, and :attr:`~cryptography.x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH` if + :meth:`build_client_verifier` is called. + + When using the default extension policies, only certificates + with the Extended Key Usage extension containing the specified value + will be accepted. To accept more than one EKU or any EKU, use an extension policy + with a custom validator. The EKU set via this method is accessible to custom extension validator + callbacks via the `policy` argument. + + :param ~cryptography.x509.ObjectIdentifier new_eku: + + :returns: A new instance of :class:`PolicyBuilder` + + .. method:: build_server_verifier(subject) + + Builds a verifier for verifying server certificates. + + :param subject: A :class:`Subject` to use in the verifier + + :returns: An instance of :class:`ServerVerifier` + + .. method:: build_client_verifier() + + Builds a verifier for verifying client certificates. + + .. warning:: + + This API is not suitable for website (i.e. server) certificate + verification. You **must** use :meth:`build_server_verifier` + for server verification. + + :returns: An instance of :class:`ClientVerifier`