From b5f13a3c3aeac358bdd595296510db2645ec171e Mon Sep 17 00:00:00 2001 From: Samuel Judson Date: Thu, 11 Jun 2026 22:09:42 -0400 Subject: [PATCH 1/3] Enforce default key usage when present for end-entity certificates. --- .../src/policy/extension.rs | 6 + tests/x509/verification/test_verification.py | 134 ++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/src/rust/cryptography-x509-verification/src/policy/extension.rs b/src/rust/cryptography-x509-verification/src/policy/extension.rs index 17d694bd094c..48ffc09bfd13 100644 --- a/src/rust/cryptography-x509-verification/src/policy/extension.rs +++ b/src/rust/cryptography-x509-verification/src/policy/extension.rs @@ -640,6 +640,12 @@ mod ee { "EE keyUsage must not assert keyCertSign".to_string(), ))); } + + if !key_usage.digital_signature() { + return Err(ValidationError::new(ValidationErrorKind::Other( + "EE keyUsage must assert digitalSignature when present".to_string(), + ))); + } } Ok(()) diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index 7d7401ad97aa..0490fb032522 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -259,6 +259,140 @@ def test_error_message(self): verifier.verify(leaf, []) +def _key_usage( + *, + digital_signature=False, + key_encipherment=False, + key_cert_sign=False, + crl_sign=False, +) -> x509.KeyUsage: + return x509.KeyUsage( + digital_signature=digital_signature, + content_commitment=False, + key_encipherment=key_encipherment, + data_encipherment=False, + key_agreement=False, + key_cert_sign=key_cert_sign, + crl_sign=crl_sign, + encipher_only=False, + decipher_only=False, + ) + + +def _chain_with_leaf_key_usage( + key_usage: Optional[x509.KeyUsage], +): + ca_key = ec.generate_private_key(ec.SECP256R1()) + leaf_key = ec.generate_private_key(ec.SECP256R1()) + + not_before = datetime.datetime(2024, 1, 1) + not_after = datetime.datetime(2034, 1, 1) + validation_time = datetime.datetime(2025, 1, 1) + + ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test CA")]) + ca = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(not_before) + .not_valid_after(not_after) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + .add_extension( + _key_usage(key_cert_sign=True, crl_sign=True), + critical=True, + ) + .sign(ca_key, hashes.SHA256()) + ) + + leaf_name = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "example.com")] + ) + leaf_builder = ( + x509.CertificateBuilder() + .subject_name(leaf_name) + .issuer_name(ca_name) + .public_key(leaf_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(not_before) + .not_valid_after(not_after) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("example.com")]), + critical=False, + ) + .add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key( + ca_key.public_key() + ), + critical=False, + ) + ) + if key_usage is not None: + leaf_builder = leaf_builder.add_extension(key_usage, critical=True) + + return ca, leaf_builder.sign(ca_key, hashes.SHA256()), validation_time + + +@pytest.mark.parametrize("verifier_kind", ["server", "client"]) +@pytest.mark.parametrize( + ("key_usage", "error"), + [ + (None, None), + (_key_usage(digital_signature=True), None), + ( + _key_usage(key_encipherment=True), + "EE keyUsage must assert digitalSignature when present", + ), + ( + _key_usage(digital_signature=True, key_cert_sign=True), + "EE keyUsage must not assert keyCertSign", + ), + ], +) +def test_default_ee_key_usage(verifier_kind, key_usage, error): + ca, leaf, validation_time = _chain_with_leaf_key_usage(key_usage) + builder = PolicyBuilder().store(Store([ca])).time(validation_time) + + def verify_leaf(): + if verifier_kind == "server": + builder.build_server_verifier(x509.DNSName("example.com")).verify( + leaf, [] + ) + else: + builder.build_client_verifier().verify(leaf, []) + + if error is None: + verify_leaf() + else: + with pytest.raises(VerificationError, match=error): + verify_leaf() + + +def test_default_ee_key_usage_can_be_overridden(): + ca, leaf, validation_time = _chain_with_leaf_key_usage( + _key_usage(key_encipherment=True) + ) + ee_policy = ExtensionPolicy.webpki_defaults_ee().may_be_present( + x509.KeyUsage, Criticality.AGNOSTIC, None + ) + builder = ( + PolicyBuilder() + .store(Store([ca])) + .time(validation_time) + .extension_policies( + ca_policy=ExtensionPolicy.webpki_defaults_ca(), + ee_policy=ee_policy, + ) + ) + + verifier = builder.build_server_verifier(x509.DNSName("example.com")) + verifier.verify(leaf, []) + + SUPPORTED_EXTENSION_TYPES = ( x509.AuthorityInformationAccess, x509.AuthorityKeyIdentifier, From d561cac3b9f70c7a57642dd991893c879a7ab94b Mon Sep 17 00:00:00 2001 From: Samuel Judson Date: Fri, 12 Jun 2026 14:31:32 -0400 Subject: [PATCH 2/3] Add more expansive handling for RSA. --- .../src/policy/extension.rs | 15 ++++-- tests/x509/verification/test_verification.py | 48 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/rust/cryptography-x509-verification/src/policy/extension.rs b/src/rust/cryptography-x509-verification/src/policy/extension.rs index 48ffc09bfd13..2bb5f0fd3466 100644 --- a/src/rust/cryptography-x509-verification/src/policy/extension.rs +++ b/src/rust/cryptography-x509-verification/src/policy/extension.rs @@ -551,6 +551,7 @@ impl<'cb, B: CryptoOps> ExtensionValidator<'cb, B> { } mod ee { + use cryptography_x509::common::AlgorithmParameters; use cryptography_x509::extensions::{BasicConstraints, ExtendedKeyUsage, Extension, KeyUsage}; use crate::ops::{CryptoOps, VerificationCertificate}; @@ -629,7 +630,7 @@ mod ee { pub(crate) fn key_usage<'chain, B: CryptoOps>( _policy: &Policy<'_, B>, - _cert: &VerificationCertificate<'chain, B>, + cert: &VerificationCertificate<'chain, B>, extn: Option<&Extension<'_>>, ) -> ValidationResult<'chain, (), B> { if let Some(extn) = extn { @@ -641,9 +642,17 @@ mod ee { ))); } - if !key_usage.digital_signature() { + let spki_params = &cert.certificate().tbs_cert.spki.algorithm.params; + if let AlgorithmParameters::Rsa(_) | AlgorithmParameters::RsaPss(_) = spki_params { + if !key_usage.digital_signature() && !key_usage.key_encipherment() { + return Err(ValidationError::new(ValidationErrorKind::Other( + "RSA EE keyUsage must assert digitalSignature or keyEncipherment" + .to_string(), + ))); + } + } else if !key_usage.digital_signature() { return Err(ValidationError::new(ValidationErrorKind::Other( - "EE keyUsage must assert digitalSignature when present".to_string(), + "EE keyUsage must assert digitalSignature".to_string(), ))); } } diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index 0490fb032522..8a534ebceb21 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -13,7 +13,10 @@ from cryptography import x509 from cryptography.hazmat._oid import ExtendedKeyUsageOID from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificatePublicKeyTypes, +) from cryptography.x509 import ExtensionType from cryptography.x509.general_name import DNSName, IPAddress from cryptography.x509.oid import NameOID @@ -281,9 +284,19 @@ def _key_usage( def _chain_with_leaf_key_usage( key_usage: Optional[x509.KeyUsage], + *, + leaf_key_type="ec", ): ca_key = ec.generate_private_key(ec.SECP256R1()) - leaf_key = ec.generate_private_key(ec.SECP256R1()) + leaf_public_key: CertificatePublicKeyTypes + if leaf_key_type == "rsa": + leaf_public_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ).public_key() + elif leaf_key_type == "ec": + leaf_public_key = ec.generate_private_key(ec.SECP256R1()).public_key() + else: + raise AssertionError(f"unsupported leaf key type: {leaf_key_type}") not_before = datetime.datetime(2024, 1, 1) not_after = datetime.datetime(2034, 1, 1) @@ -316,7 +329,7 @@ def _chain_with_leaf_key_usage( x509.CertificateBuilder() .subject_name(leaf_name) .issuer_name(ca_name) - .public_key(leaf_key.public_key()) + .public_key(leaf_public_key) .serial_number(x509.random_serial_number()) .not_valid_before(not_before) .not_valid_after(not_after) @@ -339,22 +352,39 @@ def _chain_with_leaf_key_usage( @pytest.mark.parametrize("verifier_kind", ["server", "client"]) @pytest.mark.parametrize( - ("key_usage", "error"), + ("leaf_key_type", "key_usage", "error"), [ - (None, None), - (_key_usage(digital_signature=True), None), + ("ec", None, None), + ("ec", _key_usage(digital_signature=True), None), ( + "ec", _key_usage(key_encipherment=True), - "EE keyUsage must assert digitalSignature when present", + "EE keyUsage must assert digitalSignature", ), ( + "ec", _key_usage(digital_signature=True, key_cert_sign=True), "EE keyUsage must not assert keyCertSign", ), + ("rsa", None, None), + ("rsa", _key_usage(digital_signature=True), None), + ("rsa", _key_usage(key_encipherment=True), None), + ( + "rsa", + _key_usage(), + "RSA EE keyUsage must assert digitalSignature or keyEncipherment", + ), + ( + "rsa", + _key_usage(key_encipherment=True, key_cert_sign=True), + "EE keyUsage must not assert keyCertSign", + ), ], ) -def test_default_ee_key_usage(verifier_kind, key_usage, error): - ca, leaf, validation_time = _chain_with_leaf_key_usage(key_usage) +def test_default_ee_key_usage(verifier_kind, leaf_key_type, key_usage, error): + ca, leaf, validation_time = _chain_with_leaf_key_usage( + key_usage, leaf_key_type=leaf_key_type + ) builder = PolicyBuilder().store(Store([ca])).time(validation_time) def verify_leaf(): From 43bc4ddd2b6e6ccc7087dc2047eaca10cfb50cb1 Mon Sep 17 00:00:00 2001 From: Samuel Judson Date: Fri, 12 Jun 2026 14:41:14 -0400 Subject: [PATCH 3/3] Close coverage gap. --- tests/x509/verification/test_verification.py | 32 +++++++++----------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/x509/verification/test_verification.py b/tests/x509/verification/test_verification.py index 8a534ebceb21..301072ba9f33 100644 --- a/tests/x509/verification/test_verification.py +++ b/tests/x509/verification/test_verification.py @@ -285,18 +285,16 @@ def _key_usage( def _chain_with_leaf_key_usage( key_usage: Optional[x509.KeyUsage], *, - leaf_key_type="ec", + rsa_leaf=False, ): ca_key = ec.generate_private_key(ec.SECP256R1()) leaf_public_key: CertificatePublicKeyTypes - if leaf_key_type == "rsa": + if rsa_leaf: leaf_public_key = rsa.generate_private_key( public_exponent=65537, key_size=2048 ).public_key() - elif leaf_key_type == "ec": - leaf_public_key = ec.generate_private_key(ec.SECP256R1()).public_key() else: - raise AssertionError(f"unsupported leaf key type: {leaf_key_type}") + leaf_public_key = ec.generate_private_key(ec.SECP256R1()).public_key() not_before = datetime.datetime(2024, 1, 1) not_after = datetime.datetime(2034, 1, 1) @@ -352,38 +350,38 @@ def _chain_with_leaf_key_usage( @pytest.mark.parametrize("verifier_kind", ["server", "client"]) @pytest.mark.parametrize( - ("leaf_key_type", "key_usage", "error"), + ("rsa_leaf", "key_usage", "error"), [ - ("ec", None, None), - ("ec", _key_usage(digital_signature=True), None), + (False, None, None), + (False, _key_usage(digital_signature=True), None), ( - "ec", + False, _key_usage(key_encipherment=True), "EE keyUsage must assert digitalSignature", ), ( - "ec", + False, _key_usage(digital_signature=True, key_cert_sign=True), "EE keyUsage must not assert keyCertSign", ), - ("rsa", None, None), - ("rsa", _key_usage(digital_signature=True), None), - ("rsa", _key_usage(key_encipherment=True), None), + (True, None, None), + (True, _key_usage(digital_signature=True), None), + (True, _key_usage(key_encipherment=True), None), ( - "rsa", + True, _key_usage(), "RSA EE keyUsage must assert digitalSignature or keyEncipherment", ), ( - "rsa", + True, _key_usage(key_encipherment=True, key_cert_sign=True), "EE keyUsage must not assert keyCertSign", ), ], ) -def test_default_ee_key_usage(verifier_kind, leaf_key_type, key_usage, error): +def test_default_ee_key_usage(verifier_kind, rsa_leaf, key_usage, error): ca, leaf, validation_time = _chain_with_leaf_key_usage( - key_usage, leaf_key_type=leaf_key_type + key_usage, rsa_leaf=rsa_leaf ) builder = PolicyBuilder().store(Store([ca])).time(validation_time)