From 48c2d7b694a3d81c195ca4305255ade704fb5c8c Mon Sep 17 00:00:00 2001 From: TaaviE Date: Mon, 2 Mar 2026 12:31:37 +0000 Subject: [PATCH] Add S/MIME support based on CA/B Forum S/MIME BR 1.0.12 --- limbo-schema.json | 2 + limbo/models.py | 12 +- limbo/testcases/__init__.py | 1 + limbo/testcases/smime/__init__.py | 485 +++++++++++++++++++++++++++ limbo/testcases/smime/eku.py | 493 ++++++++++++++++++++++++++++ limbo/testcases/smime/ku.py | 522 ++++++++++++++++++++++++++++++ limbo/testcases/smime/san.py | 263 +++++++++++++++ 7 files changed, 1777 insertions(+), 1 deletion(-) create mode 100644 limbo/testcases/smime/__init__.py create mode 100644 limbo/testcases/smime/eku.py create mode 100644 limbo/testcases/smime/ku.py create mode 100644 limbo/testcases/smime/san.py diff --git a/limbo-schema.json b/limbo-schema.json index 2a413587..2ada800e 100644 --- a/limbo-schema.json +++ b/limbo-schema.json @@ -19,6 +19,8 @@ "name-constraint-dn", "pedantic-webpki-subscriber-key", "pedantic-webpki-eku", + "pedantic-smime-eku", + "pedantic-smime-subscriber-key", "pedantic-serial-number", "max-chain-depth", "pedantic-rfc5280", diff --git a/limbo/models.py b/limbo/models.py index dd8e3cc4..14996b1d 100644 --- a/limbo/models.py +++ b/limbo/models.py @@ -189,6 +189,16 @@ class Feature(StrEnum): Tests that exercise "pedantic" EKU handling under CABF. """ + pedantic_smime_eku = "pedantic-smime-eku" + """ + Tests that exercise "pedantic" EKU handling under S/MIME BR. + """ + + pedantic_smime_subscriber_key = "pedantic-smime-subscriber-key" + """ + Tests that exercise "pedantic" handling of subscriber key types under S/MIME BR. + """ + pedantic_serial_number = "pedantic-serial-number" """ Tests that exercise "pedantic" serial number handling. @@ -202,7 +212,7 @@ class Feature(StrEnum): pedantic_rfc5280 = "pedantic-rfc5280" """ - Tests that exercise "pednatic" corners of the RFC 5280 certificate profile. + Tests that exercise "pedantic" corners of the RFC 5280 certificate profile. """ rfc5280_incompatible_with_webpki = "rfc5280-incompatible-with-webpki" diff --git a/limbo/testcases/__init__.py b/limbo/testcases/__init__.py index 4d9e3a42..5a14a2dc 100644 --- a/limbo/testcases/__init__.py +++ b/limbo/testcases/__init__.py @@ -10,6 +10,7 @@ from .pathlen import * # noqa: F403 from .pathological import * # noqa: F403 from .rfc5280 import * # noqa: F403 +from .smime import * # noqa: F403 from .webpki import * # noqa: F403 __all__ = [ diff --git a/limbo/testcases/smime/__init__.py b/limbo/testcases/smime/__init__.py new file mode 100644 index 00000000..6d5d3a2e --- /dev/null +++ b/limbo/testcases/smime/__init__.py @@ -0,0 +1,485 @@ +""" +S/MIME (CA/B Forum S/MIME Baseline Requirements) profile tests. +""" + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa +from cryptography.x509.oid import ExtendedKeyUsageOID + +from limbo.assets import ext +from limbo.models import Feature, KnownEKUs, PeerName +from limbo.testcases._core import Builder, testcase + +from .eku import * # noqa: F403 +from .ku import * # noqa: F403 +from .san import * # noqa: F403 + + +@testcase +def valid_smime_chain(builder: Builder) -> None: + """ + Valid S/MIME chain (root -> EE) with emailProtection EKU, + rfc822Name SAN, and digitalSignature KU per S/MIME BR 7.1.2.3. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def valid_smime_chain_with_intermediate(builder: Builder) -> None: + """ + Valid S/MIME chain (root -> ICA -> EE). The ICA has emailProtection + EKU and keyCertSign + cRLSign per S/MIME BR 7.1.2.2(e,g). + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + key_usage=ext( + x509.KeyUsage( + digital_signature=False, + key_cert_sign=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + extra_extension=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def v1_cert(builder: Builder) -> None: + """ + The EE cert is X.509 v1. S/MIME BR 7.1.1: + + > Certificates SHALL be of type X.509 v3. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert(root, unchecked_version=x509.Version.v1, no_extensions=True) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_basicconstraints_ca_true(builder: Builder) -> None: + """ + The EE has basicConstraints.cA=TRUE. S/MIME BR 7.1.2.3(d): + + > The cA field SHALL NOT be true. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + basic_constraints=ext(x509.BasicConstraints(True, None), critical=True), + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_basicconstraints_pathlen_present(builder: Builder) -> None: + """ + The EE has basicConstraints with pathLenConstraint. S/MIME BR 7.1.2.3(d): + + > pathLenConstraint field SHALL NOT be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + basic_constraints=ext(x509.BasicConstraints(True, 0), critical=True), + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ca_as_leaf(builder: Builder) -> None: + """ + A CA certificate (cA=TRUE, keyCertSign) is used as the leaf. + S/MIME BR 7.1.2.3(d): + + > The cA field SHALL NOT be true. + """ + + root = builder.root_ca() + ica_leaf = builder.intermediate_ca( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("ca@example.com")]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(ica_leaf) + .expected_peer_names(PeerName(kind="RFC822", value="ca@example.com")) + .fails() + ) + + +@testcase +def forbidden_p192_leaf(builder: Builder) -> None: + """ + The EE has a P-192 key. S/MIME BR 6.1.5: + + > [ECDSA] Ensure that the key represents a valid point on the NIST P-256, + > NIST P-384, or NIST P-521 elliptic curve. + """ + + root = builder.root_ca() + + leaf_key = ec.generate_private_key(ec.SECP192R1()) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_subscriber_key]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_p192_root(builder: Builder) -> None: + """ + The root has a P-192 key. S/MIME BR 6.1.5: + + > [ECDSA] Ensure that the key represents a valid point on the NIST P-256, + > NIST P-384, or NIST P-521 elliptic curve. + """ + + root_key = ec.generate_private_key(ec.SECP192R1()) + root = builder.root_ca(key=root_key) + + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_dsa_leaf(builder: Builder) -> None: + """ + The EE has a DSA key. S/MIME BR 6.1.5: + + > No other algorithms or key sizes are permitted. + """ + + root = builder.root_ca() + + leaf_key = dsa.generate_private_key(3072) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_subscriber_key]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_dsa_root(builder: Builder) -> None: + """ + The root has a DSA key. S/MIME BR 6.1.5: + + > No other algorithms or key sizes are permitted. + """ + + root_key = dsa.generate_private_key(key_size=3072) + root = builder.root_ca(key=root_key) + + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_weak_rsa_key_in_root(builder: Builder) -> None: + """ + The root has an RSA-1024 key. S/MIME BR 6.1.5: + + > [RSA] Ensure that the modulus size, when encoded, is at least 2048 bits. + """ + + root_key = rsa.generate_private_key(public_exponent=65537, key_size=1024) + root = builder.root_ca(key=root_key) + + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_weak_rsa_in_leaf(builder: Builder) -> None: + """ + The EE has an RSA-1024 key. S/MIME BR 6.1.5: + + > [RSA] Ensure that the modulus size, when encoded, is at least 2048 bits. + """ + + root = builder.root_ca() + + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=1024) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_subscriber_key]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_rsa_not_divisible_by_8_in_root(builder: Builder) -> None: + """ + The root has an RSA-2052 key. S/MIME BR 6.1.5: + + > [RSA] Ensure that the modulus size, in bits, is evenly divisible by 8. + """ + + root_key = rsa.generate_private_key(public_exponent=65537, key_size=2052) + root = builder.root_ca(key=root_key) + + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def forbidden_rsa_key_not_divisible_by_8_in_leaf(builder: Builder) -> None: + """ + The EE has an RSA-2052 key. S/MIME BR 6.1.5: + + > [RSA] Ensure that the modulus size, in bits, is evenly divisible by 8. + """ + + root = builder.root_ca() + + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2052) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_subscriber_key]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) diff --git a/limbo/testcases/smime/eku.py b/limbo/testcases/smime/eku.py new file mode 100644 index 00000000..01b073ec --- /dev/null +++ b/limbo/testcases/smime/eku.py @@ -0,0 +1,493 @@ +""" +S/MIME Extended Key Usage (EKU) tests. +""" + +from cryptography import x509 +from cryptography.x509.oid import ExtendedKeyUsageOID + +from limbo.assets import ext +from limbo.models import Feature, KnownEKUs, PeerName +from limbo.testcases._core import Builder, testcase + + +@testcase +def ee_email_protection(builder: Builder) -> None: + """ + Valid S/MIME EE with emailProtection EKU per S/MIME BR 7.1.2.3(f): + + > id-kp-emailProtection SHALL be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def ee_missing_email_protection(builder: Builder) -> None: + """ + The EE has clientAuth but not emailProtection. S/MIME BR 7.1.2.3(f): + + > id-kp-emailProtection SHALL be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_without_eku(builder: Builder) -> None: + """ + The EE has no EKU extension. S/MIME BR 7.1.2.3(f): + + > extKeyUsage (SHALL be present) + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=None, + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_has_server_auth(builder: Builder) -> None: + """ + The EE has emailProtection + serverAuth. S/MIME BR 7.1.2.3(f): + + > id-kp-serverAuth [...] SHALL NOT be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.SERVER_AUTH] + ), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_has_code_signing(builder: Builder) -> None: + """ + The EE has emailProtection + codeSigning. S/MIME BR 7.1.2.3(f): + + > id-kp-codeSigning [...] SHALL NOT be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.CODE_SIGNING] + ), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_has_time_stamping(builder: Builder) -> None: + """ + The EE has emailProtection + timeStamping. S/MIME BR 7.1.2.3(f): + + > id-kp-timeStamping [...] SHALL NOT be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.TIME_STAMPING] + ), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_has_any_eku(builder: Builder) -> None: + """ + The EE has emailProtection + anyExtendedKeyUsage. S/MIME BR 7.1.2.3(f): + + > anyExtendedKeyUsage SHALL NOT be present. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage( + [ + ExtendedKeyUsageOID.EMAIL_PROTECTION, + ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE, + ] + ), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def root_has_eku(builder: Builder) -> None: + """ + The root has an extKeyUsage extension. S/MIME BR 7.1.2.1(d): + + > extKeyUsage [...] SHALL NOT be present. + """ + + root = builder.root_ca( + extra_extension=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ) + ) + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_missing_email_protection(builder: Builder) -> None: + """ + The ICA has serverAuth but not emailProtection. S/MIME BR 7.1.2.2(g): + + > id-kp-emailProtection SHALL be present. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + extra_extension=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_has_server_auth(builder: Builder) -> None: + """ + The ICA has emailProtection + serverAuth. S/MIME BR 7.1.2.2(g): + + > id-kp-serverAuth [...] SHALL NOT be present. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + extra_extension=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.SERVER_AUTH] + ), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_has_any_eku(builder: Builder) -> None: + """ + The ICA has emailProtection + anyExtendedKeyUsage. S/MIME BR 7.1.2.2(g): + + > anyExtendedKeyUsage [...] SHALL NOT be present. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + extra_extension=ext( + x509.ExtendedKeyUsage( + [ + ExtendedKeyUsageOID.EMAIL_PROTECTION, + ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE, + ] + ), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_has_code_signing(builder: Builder) -> None: + """ + The ICA has emailProtection + codeSigning. S/MIME BR 7.1.2.2(g): + + > id-kp-codeSigning [...] SHALL NOT be present. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + extra_extension=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.CODE_SIGNING] + ), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_has_time_stamping(builder: Builder) -> None: + """ + The ICA has emailProtection + timeStamping. S/MIME BR 7.1.2.2(g): + + > id-kp-timeStamping [...] SHALL NOT be present. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + extra_extension=ext( + x509.ExtendedKeyUsage( + [ExtendedKeyUsageOID.EMAIL_PROTECTION, ExtendedKeyUsageOID.TIME_STAMPING] + ), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) diff --git a/limbo/testcases/smime/ku.py b/limbo/testcases/smime/ku.py new file mode 100644 index 00000000..85237c94 --- /dev/null +++ b/limbo/testcases/smime/ku.py @@ -0,0 +1,522 @@ +""" +S/MIME Key Usage (KU) tests. +""" + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import ExtendedKeyUsageOID + +from limbo.assets import ext +from limbo.models import Feature, KnownEKUs, PeerName +from limbo.testcases._core import Builder, testcase + + +@testcase +def signing_only_rsa_digital_signature(builder: Builder) -> None: + """ + RSA EE with digitalSignature only. S/MIME BR 7.1.2.3(e): + + > For signing only, bit positions SHALL be set for digitalSignature. + """ + + root = builder.root_ca() + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def signing_only_rsa_with_nonrepudiation(builder: Builder) -> None: + """ + RSA EE with digitalSignature + contentCommitment. S/MIME BR 7.1.2.3(e): + + > For signing only, bit positions SHALL be set for digitalSignature + > and MAY be set for nonRepudiation. + """ + + root = builder.root_ca() + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=False, + content_commitment=True, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def key_management_only_rsa(builder: Builder) -> None: + """ + RSA EE with keyEncipherment only. S/MIME BR 7.1.2.3(e): + + > For key management only, bit positions SHALL be set for keyEncipherment. + """ + + root = builder.root_ca() + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=False, + key_cert_sign=False, + content_commitment=False, + key_encipherment=True, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def dual_use_rsa(builder: Builder) -> None: + """ + RSA EE with digitalSignature + keyEncipherment. S/MIME BR 7.1.2.3(e): + + > For dual use, bit positions SHALL be set for digitalSignature + > and keyEncipherment. + """ + + root = builder.root_ca() + leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + leaf = builder.leaf_cert( + root, + key=leaf_key, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=False, + content_commitment=False, + key_encipherment=True, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def signing_only_ec(builder: Builder) -> None: + """ + EC EE with digitalSignature only. S/MIME BR 7.1.2.3(e): + + > For signing only, bit positions SHALL be set for digitalSignature. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def key_management_only_ec(builder: Builder) -> None: + """ + EC EE with keyAgreement only. S/MIME BR 7.1.2.3(e): + + > For key management only, bit positions SHALL be set for keyAgreement. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=False, + key_cert_sign=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=True, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def ee_key_cert_sign(builder: Builder) -> None: + """ + The EE has keyCertSign set. S/MIME BR 7.1.2.3(e): + + > Other bit positions SHALL NOT be set. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_missing_key_usage(builder: Builder) -> None: + """ + The EE has no Key Usage extension. S/MIME BR 7.1.2.3(e): + + > keyUsage (SHALL be present) + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=None, + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def ee_key_usage_not_critical(builder: Builder) -> None: + """ + The EE Key Usage extension is not marked critical. S/MIME BR 7.1.2.3(e): + + > This extension SHOULD be marked critical. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + key_usage=ext( + x509.KeyUsage( + digital_signature=True, + key_cert_sign=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .features([Feature.pedantic_smime_eku]) + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_missing_key_cert_sign(builder: Builder) -> None: + """ + The ICA Key Usage lacks keyCertSign. S/MIME BR 7.1.2.2(e): + + > Bit positions for keyCertSign and cRLSign SHALL be set. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + key_usage=ext( + x509.KeyUsage( + digital_signature=False, + key_cert_sign=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + extra_extension=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def intermediate_missing_crl_sign(builder: Builder) -> None: + """ + The ICA Key Usage lacks cRLSign. S/MIME BR 7.1.2.2(e): + + > Bit positions for keyCertSign and cRLSign SHALL be set. + """ + + root = builder.root_ca() + ica = builder.intermediate_ca( + root, + pathlen=0, + key_usage=ext( + x509.KeyUsage( + digital_signature=False, + key_cert_sign=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ), + extra_extension=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + leaf = builder.leaf_cert( + ica, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .untrusted_intermediates(ica) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) diff --git a/limbo/testcases/smime/san.py b/limbo/testcases/smime/san.py new file mode 100644 index 00000000..f4fbe54f --- /dev/null +++ b/limbo/testcases/smime/san.py @@ -0,0 +1,263 @@ +""" +S/MIME Subject Alternative Name (SAN) tests. +""" + +from cryptography import x509 +from cryptography.x509.oid import ExtendedKeyUsageOID + +from limbo.assets import ext +from limbo.models import KnownEKUs, PeerName +from limbo.testcases._core import Builder, testcase + + +@testcase +def exact_rfc822_san(builder: Builder) -> None: + """ + Valid S/MIME EE with rfc822Name SAN per S/MIME BR 7.1.4.2.1: + + > This extension SHALL contain at least one GeneralName entry + > of [...] Rfc822Name. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def no_san(builder: Builder) -> None: + """ + The EE has no SAN extension. S/MIME BR 7.1.2.3(h): + + > subjectAlternativeName (SHALL be present) + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + subject=x509.Name.from_rfc4514_string("CN=user@example.com"), + san=None, + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def san_without_rfc822name(builder: Builder) -> None: + """ + The EE SAN has only a dNSName, no rfc822Name. S/MIME BR 7.1.4.2.1: + + > This extension SHALL contain at least one GeneralName entry + > of [...] Rfc822Name. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.DNSName("example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def san_critical_with_nonempty_subject(builder: Builder) -> None: + """ + The EE has a critical SAN with a non-empty subject. S/MIME BR 7.1.2.3(h): + + > This extension SHOULD NOT be marked critical unless the subject + > field is an empty sequence. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + subject=x509.Name.from_rfc4514_string("CN=user@example.com"), + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=True, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .fails() + ) + + +@testcase +def san_critical_with_empty_subject(builder: Builder) -> None: + """ + The EE has a critical SAN with an empty subject. S/MIME BR 7.1.2.3(h): + + > This extension SHOULD NOT be marked critical unless the subject + > field is an empty sequence. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + subject=None, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=True, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + ) + + +@testcase +def mismatch_rfc822_san(builder: Builder) -> None: + """ + The EE has rfc822Name "user@example.com" but verification is + against "other@example.com". + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="other@example.com")) + .fails() + ) + + +@testcase +def mismatch_domain_rfc822_san(builder: Builder) -> None: + """ + The EE has rfc822Name "user@example.com" but verification is + against "user@example.org". + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName([x509.RFC822Name("user@example.com")]), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.org")) + .fails() + ) + + +@testcase +def multiple_rfc822_sans(builder: Builder) -> None: + """ + The EE has multiple rfc822Names; verification matches one of them. + """ + + root = builder.root_ca() + leaf = builder.leaf_cert( + root, + san=ext( + x509.SubjectAlternativeName( + [ + x509.RFC822Name("other@example.com"), + x509.RFC822Name("user@example.com"), + ] + ), + critical=False, + ), + eku=ext( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.EMAIL_PROTECTION]), + critical=False, + ), + ) + + builder = ( + builder.client_validation() + .extended_key_usage([KnownEKUs.email_protection]) + .trusted_certs(root) + .peer_certificate(leaf) + .expected_peer_names(PeerName(kind="RFC822", value="user@example.com")) + .succeeds() + )