diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 54b645e2b507..8781d6c76a6e 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -1003,6 +1003,12 @@ Custom PKCS7 Test Vectors * ``pkcs7/enveloped-no-content.der``- A DER encoded PKCS7 file with enveloped data, without encrypted content, with key encrypted under the public key of ``x509/custom/ca/rsa_ca.pem``. +* ``pkcs7/ca.pem`` - A certificate adapted for S/MIME signature & verification. + Its private key is ``pkcs7/ca_key.pem`` . +* ``pkcs7/ca_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature + & verification. It has an ASCII subject alternative name stored as `otherName`. +* ``pkcs7/ca_non_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature + & verification. It has an non-ASCII subject alternative name stored as `rfc822Name`. Custom OpenSSH Test Vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 456dc5b0831c..dc3c290b73e6 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -21,6 +21,13 @@ algorithms, ) from cryptography.utils import _check_byteslike +from cryptography.x509 import Certificate +from cryptography.x509.oid import ExtendedKeyUsageOID +from cryptography.x509.verification import ( + Criticality, + ExtensionPolicy, + Policy, +) load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates @@ -53,6 +60,120 @@ class PKCS7Options(utils.Enum): NoCerts = "Don't embed signer certificate" +def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]: + """ + Gets the default X.509 extension policy for S/MIME, based on RFC 8550. + Visit https://www.rfc-editor.org/rfc/rfc8550#section-4.4 for more info. + """ + # CA policy + ca_policy = ExtensionPolicy.webpki_defaults_ca() + + # EE policy + def _validate_basic_constraints( + policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None + ) -> None: + """ + We check that Certificates used as EE (i.e., the cert used to sign + a PKCS#7/SMIME message) must not have ca=true in their basic + constraints extension. RFC 5280 doesn't impose this requirement, but we + firmly agree about it being best practice. + """ + if bc is not None and bc.ca: + raise ValueError("Basic Constraints CA must be False.") + + def _validate_key_usage( + policy: Policy, cert: Certificate, ku: x509.KeyUsage | None + ) -> None: + """ + Checks that the Key Usage extension, if present, has at least one of + the digital signature or content commitment (formerly non-repudiation) + bits set. + """ + if ( + ku is not None + and not ku.digital_signature + and not ku.content_commitment + ): + raise ValueError( + "Key Usage, if specified, must have at least one of the " + "digital signature or content commitment (formerly non " + "repudiation) bits set." + ) + + def _validate_subject_alternative_name( + policy: Policy, + cert: Certificate, + san: x509.SubjectAlternativeName, + ) -> None: + """ + For each general name in the SAN, for those which are email addresses: + - If it is an RFC822Name, general part must be ascii. + - If it is an OtherName, general part must be non-ascii. + """ + for general_name in san: + if ( + isinstance(general_name, x509.RFC822Name) + and "@" in general_name.value + and not general_name.value.split("@")[0].isascii() + ): + raise ValueError( + f"RFC822Name {general_name.value} contains non-ASCII " + "characters." + ) + if ( + isinstance(general_name, x509.OtherName) + and "@" in general_name.value.decode() + and general_name.value.decode().split("@")[0].isascii() + ): + raise ValueError( + f"OtherName {general_name.value.decode()} is ASCII, " + "so must be stored in RFC822Name." + ) + + def _validate_extended_key_usage( + policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage | None + ) -> None: + """ + Checks that the Extended Key Usage extension, if present, + includes either emailProtection or anyExtendedKeyUsage bits. + """ + if ( + eku is not None + and ExtendedKeyUsageOID.EMAIL_PROTECTION not in eku + and ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE not in eku + ): + raise ValueError( + "Extended Key Usage, if specified, must include " + "emailProtection or anyExtendedKeyUsage." + ) + + ee_policy = ( + ExtensionPolicy.webpki_defaults_ee() + .may_be_present( + x509.BasicConstraints, + Criticality.AGNOSTIC, + _validate_basic_constraints, + ) + .may_be_present( + x509.KeyUsage, + Criticality.CRITICAL, + _validate_key_usage, + ) + .require_present( + x509.SubjectAlternativeName, + Criticality.AGNOSTIC, + _validate_subject_alternative_name, + ) + .may_be_present( + x509.ExtendedKeyUsage, + Criticality.AGNOSTIC, + _validate_extended_key_usage, + ) + ) + + return ca_policy, ee_policy + + class PKCS7SignatureBuilder: def __init__( self, diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 1496a23e1b2e..8d04a5419810 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -18,6 +18,16 @@ from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.serialization import pkcs7 +from cryptography.x509.oid import ( + ExtendedKeyUsageOID, + ExtensionOID, + ObjectIdentifier, +) +from cryptography.x509.verification import ( + PolicyBuilder, + Store, + VerificationError, +) from tests.x509.test_x509 import _generate_ca_and_leaf from ...hazmat.primitives.fixtures_rsa import ( @@ -125,20 +135,153 @@ def test_load_pkcs7_empty_certificates(self): def _load_cert_key(): key = load_vectors_from_file( - os.path.join("x509", "custom", "ca", "ca_key.pem"), + os.path.join("pkcs7", "ca_key.pem"), lambda pemfile: serialization.load_pem_private_key( pemfile.read(), None, unsafe_skip_rsa_key_validation=True ), mode="rb", ) cert = load_vectors_from_file( - os.path.join("x509", "custom", "ca", "ca.pem"), + os.path.join("pkcs7", "ca.pem"), loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()), mode="rb", ) return cert, key +class TestPKCS7VerifyCertificate: + @staticmethod + def build_pkcs7_certificate( + ca: bool = False, + digital_signature: bool = True, + usages: typing.Optional[typing.List[ObjectIdentifier]] = None, + ) -> x509.Certificate: + """ + This static method is a helper to build certificates allowing us + to test all cases in PKCS#7 certificate verification. + """ + # Load the standard certificate and private key + certificate, private_key = _load_cert_key() + + # Basic certificate builder + certificate_builder = ( + x509.CertificateBuilder() + .serial_number(certificate.serial_number) + .subject_name(certificate.subject) + .issuer_name(certificate.issuer) + .public_key(private_key.public_key()) + .not_valid_before(certificate.not_valid_before) + .not_valid_after(certificate.not_valid_after) + ) + + # Add AuthorityKeyIdentifier extension + aki = certificate.extensions.get_extension_for_oid( + ExtensionOID.AUTHORITY_KEY_IDENTIFIER + ) + certificate_builder = certificate_builder.add_extension( + aki.value, critical=False + ) + + # Add SubjectAlternativeName extension + san = certificate.extensions.get_extension_for_oid( + ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ) + certificate_builder = certificate_builder.add_extension( + san.value, critical=True + ) + + # Add BasicConstraints extension + bc_extension = x509.BasicConstraints(ca=ca, path_length=None) + certificate_builder = certificate_builder.add_extension( + bc_extension, False + ) + + # Add KeyUsage extension + ku_extension = x509.KeyUsage( + digital_signature=digital_signature, + content_commitment=False, + key_encipherment=True, + data_encipherment=True, + key_agreement=True, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ) + certificate_builder = certificate_builder.add_extension( + ku_extension, True + ) + + # Add valid ExtendedKeyUsage extension + usages = usages or [ExtendedKeyUsageOID.EMAIL_PROTECTION] + certificate_builder = certificate_builder.add_extension( + x509.ExtendedKeyUsage(usages), True + ) + + # Build the certificate + return certificate_builder.sign( + private_key, certificate.signature_hash_algorithm, None + ) + + def test_verify_pkcs7_certificate(self): + # Prepare the parameters + certificate = self.build_pkcs7_certificate() + ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies() + + # Verify the certificate + verifier = ( + PolicyBuilder() + .store(Store([certificate])) + .extension_policies(ca_policy=ca_policy, ee_policy=ee_policy) + .build_client_verifier() + ) + verifier.verify(certificate, []) + + @pytest.mark.parametrize( + "arguments", + [ + {"ca": True}, + {"digital_signature": False}, + {"usages": [ExtendedKeyUsageOID.CLIENT_AUTH]}, + ], + ) + def test_verify_invalid_pkcs7_certificate(self, arguments: dict): + # Prepare the parameters + certificate = self.build_pkcs7_certificate(**arguments) + + # Verify the certificate + self.verify_invalid_pkcs7_certificate(certificate) + + @staticmethod + def verify_invalid_pkcs7_certificate(certificate: x509.Certificate): + ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies() + verifier = ( + PolicyBuilder() + .store(Store([certificate])) + .extension_policies(ca_policy=ca_policy, ee_policy=ee_policy) + .build_client_verifier() + ) + + with pytest.raises(VerificationError): + verifier.verify(certificate, []) + + @pytest.mark.parametrize( + "filename", ["ca_non_ascii_san.pem", "ca_ascii_san.pem"] + ) + def test_verify_pkcs7_certificate_wrong_san(self, filename): + # Read a certificate with an invalid SAN + pkcs7_certificate = load_vectors_from_file( + os.path.join("pkcs7", filename), + loader=lambda pemfile: x509.load_pem_x509_certificate( + pemfile.read() + ), + mode="rb", + ) + + # Verify the certificate + self.verify_invalid_pkcs7_certificate(pkcs7_certificate) + + @pytest.mark.supported( only_if=lambda backend: backend.pkcs7_supported(), skip_message="Requires OpenSSL with PKCS7 support", diff --git a/vectors/cryptography_vectors/pkcs7/ca.pem b/vectors/cryptography_vectors/pkcs7/ca.pem new file mode 100644 index 000000000000..d11b0ec59b35 --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhjCCASygAwIBAgICAwkwCgYIKoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAW +BgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEw +MTAwMDAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD +QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N +2CxSJE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjRjBE +MCEGA1UdEQEB/wQXMBWBE2V4YW1wbGVAZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAU +/Ou02BLyyT2Zwzxn9H03feYT7fowCgYIKoZIzj0EAwIDSAAwRQIgUwIdC0Emkd6f +17DeOXTlmTAhwSDJ2FTuyHESwei7wJcCIQCnr9NpBxbtJfEzxHGGyd7PxgpOLi5u +rk+8QfzGMmg/fw== +-----END CERTIFICATE----- diff --git a/vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem b/vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem new file mode 100644 index 000000000000..7e184abcbe3c --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID3DCCAsSgAwIBAgIUGJw032ss5tmRmaY8x41pL5lqqRYwDQYJKoZIhvcNAQEL +BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN +SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTg0 +NzQ1WhcNMjYwNjA5MTg0NzQ1WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs +aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs +ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl +LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLWXuy3atOjhb8g +fa5AC5me9PqRqcqV63e+NIe8IaKioCM5Sl+3jhKb5DdPIjfQYbHbwPtY+rFSP364 +dBZoJpCDG4gcD6H3eS5JGc8Uz62l+oBNuFoU3EZiUNMF0k17vs/6CGeyt53+D9DJ +PG6Wv87nAAoK97r1rLdC8Of97QpUV/st+YDP7/LOH8CxJZOnbiUdekzo0dCQkk7n +17hJCYN1Y98VrlZFY25ny2TURUgK7lIjduEUb0dugYiepjzp7ZV8184kpAD/PtLT +czA1S8e6kySd5wbJSFcKxrk/j/cccUGLMyKPlMZgsHZUm/2DOLWLljxbEjCOxb1G +8+EpR9kCAwEAAaNQME4wLQYDVR0RBCYwJKAiBggrBgEFBQcICaAWDBRyZXRvdXJu +ZUBleGFtcGxlLmNvbTAdBgNVHQ4EFgQUm24AOQAmOInCPZPDUagXXw+BEl0wDQYJ +KoZIhvcNAQELBQADggEBAGgLqsx27sS28t1okxT1MU6QhfAn/Yw07Nhk3cpNKGnh +edrPPTXvJc05qHuQIqOiFIJ4SojbQ2+bVZwo7V3Jhspx9T+Gkb/Dn3rHpAfOXuaJ +RqJ777Cor2seAKv07jerGnEULYW8JcezZDGbv6ViC0oEgazwTzahfynrUMJ2DJRX +tnNdczDsGw+DVMvOBzcSE/aEzhd4ghgVq5aFS05wzhN/fTWKiN4tpEAG6y95gU73 +29O3y1W3dLjblTZJvXNtgCjMT6R3OVeWAsqyXDprFrZWZucCj8opIxRf6jpZlRfJ +qW+57pkefhg3q4MFjn08BOKpYwOdRouGE4l96dGBDwM= +-----END CERTIFICATE----- diff --git a/vectors/cryptography_vectors/pkcs7/ca_key.pem b/vectors/cryptography_vectors/pkcs7/ca_key.pem new file mode 100644 index 000000000000..2fb5394195cb --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/ca_key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe +jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs +UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF +-----END PRIVATE KEY----- diff --git a/vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem b/vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem new file mode 100644 index 000000000000..f590d881e68e --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIUAX/xKTtlMllrK5ng0+OkmnxxIugwDQYJKoZIhvcNAQEL +BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN +SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTgw +NzE4WhcNMjYwNjA5MTgwNzE4WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs +aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs +ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl +LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOxyV/ZsaGn7dOcZ +6ODFcnmwjPCKRASFeDtOMYoGrlALb9zA+UMuMB63dTZ8ofWsDgLLGhw86njfSYad +RslOw8Bki9lKiS1RhS/RbnDSBWB2wJzniyFn/qI2F93WbgqHMOnzzJcAkc/YPU0T +iyvNpjD3Q/xObcp7ouBJJmFSvLybSTJtFrVzkpIbDZYrn0KyKtgTCPc/r9D04u+u +scSACvTRjePsEZIgRkVgfVpdBmy1KeJmx2NqS8Yev+y+0e9q3t8Ga/j/CnPFXlEl +iBHciFtkKdd2HrPLJMXBKhMn2KagLJSSdABNApi8qULIpOnrEE8FepKCzkptFyS1 +5g0H3u0CAwEAAaNDMEEwIAYDVR0RBBkwF4EVcmV0b3VybsOpQGV4YW1wbGUuY29t +MB0GA1UdDgQWBBTthtqdM0IoehNymXnqMPX1joF1LzANBgkqhkiG9w0BAQsFAAOC +AQEApQZ3vOuBgNg1U26c4l0VSCU5q73Lecbgjc42AhEp9FyP7ratj4MyH7RGr4io +vl0wWROFBnzliW5ZA8CP3Ux4AbqgtxcFPBRHACjmrpoSFHmW7bpzRnqwJKwXsOGJ +ZhjA/2o91lEJr0UNhpvSGyR+xCkuvw83mvM1rmE19yNMElv96x/DPVQV2ocsffOb +kS7pIpvXX3pSIj7Up0Xrz+bSyhJlsO3sO5bREshyvuiRivm9AjBVRY/BtbFY6DcV +9javEitCw93BgImIs0CXGpZUrvphX8muWVct5xpKj64/Yo0hIYystX+xVl3EjTRf +B7pH2DE+cXg99p7L6RoYtlOeRA== +-----END CERTIFICATE-----