From db7eab9a28517b497275f4c46988de0cfa7e368c Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:27:44 +0200 Subject: [PATCH 1/3] feat: PKCS#7 extension policies added tests accordingly adapted the pkcs7 certificate adapted EE policy do not know if a CA policy is needed! added SAN checking --- .../hazmat/primitives/serialization/pkcs7.py | 121 ++++++++++++++ tests/hazmat/primitives/test_pkcs7.py | 147 +++++++++++++++++- vectors/cryptography_vectors/pkcs7/ca.pem | 11 ++ .../pkcs7/ca_ascii_san.pem | 23 +++ vectors/cryptography_vectors/pkcs7/ca_key.pem | 5 + .../pkcs7/ca_non_ascii_san.pem | 23 +++ 6 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 vectors/cryptography_vectors/pkcs7/ca.pem create mode 100644 vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem create mode 100644 vectors/cryptography_vectors/pkcs7/ca_key.pem create mode 100644 vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem 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----- From ceb8b2a0c1e47fdcf991ac8ac0de12d395567dbd Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:36:49 +0200 Subject: [PATCH 2/3] adapted based on comments & prauscher feedback --- tests/hazmat/primitives/test_pkcs7.py | 25 +++++++++++-------- vectors/cryptography_vectors/pkcs7/ca.pem | 11 -------- .../pkcs7/ca_ascii_san.pem | 23 ----------------- vectors/cryptography_vectors/pkcs7/ca_key.pem | 5 ---- .../pkcs7/ca_non_ascii_san.pem | 23 ----------------- 5 files changed, 15 insertions(+), 72 deletions(-) delete mode 100644 vectors/cryptography_vectors/pkcs7/ca.pem delete mode 100644 vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem delete mode 100644 vectors/cryptography_vectors/pkcs7/ca_key.pem delete mode 100644 vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 8d04a5419810..ec840d9ef015 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -20,7 +20,6 @@ from cryptography.hazmat.primitives.serialization import pkcs7 from cryptography.x509.oid import ( ExtendedKeyUsageOID, - ExtensionOID, ObjectIdentifier, ) from cryptography.x509.verification import ( @@ -135,14 +134,14 @@ def test_load_pkcs7_empty_certificates(self): def _load_cert_key(): key = load_vectors_from_file( - os.path.join("pkcs7", "ca_key.pem"), + os.path.join("x509", "custom", "ca", "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("pkcs7", "ca.pem"), + os.path.join("x509", "custom", "ca", "ca.pem"), loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()), mode="rb", ) @@ -175,19 +174,25 @@ def build_pkcs7_certificate( ) # Add AuthorityKeyIdentifier extension - aki = certificate.extensions.get_extension_for_oid( - ExtensionOID.AUTHORITY_KEY_IDENTIFIER + aki = x509.AuthorityKeyIdentifier( + b"\xfc\xeb\xb4\xd8\x12\xf2\xc9=\x99\xc3 Date: Sat, 13 Sep 2025 19:21:28 +0200 Subject: [PATCH 3/3] PKCS7 signing handling PEM, DER, SMIME formats added tests & documentation accordingly doing assertions for now, to please mypy added more test coverage updated tests to avoid unsupported algorithm first failing code for certificate verification handling mixed types with Cow feat: functions now have optional keyword arguments certificate is now optional feat: handling RSA case feat: No signature parameter adapted tests accordingly fix: adapted docmentation fix: passed into Cow again coverage: added one test case one more test case changing the error message for clarity two more test cases handled loading the load_der func for all backends removed options for now first draft of smime extension policy added back changelog integrated built-in verifier using existing vectors :) minor doc modification removed old vectors --- CHANGELOG.rst | 6 + docs/development/test-vectors.rst | 3 + .../primitives/asymmetric/serialization.rst | 167 ++++++++++ .../hazmat/bindings/_rust/pkcs7.pyi | 25 +- .../hazmat/primitives/serialization/pkcs7.py | 46 +++ src/rust/src/pkcs7.rs | 206 ++++++++++++- src/rust/src/types.rs | 10 + tests/hazmat/primitives/test_pkcs7.py | 287 +++++++++++++++++- .../pkcs7/signature-empty-certs.der | Bin 0 -> 574 bytes .../pkcs7/signed-opaque.msg | 40 +++ 10 files changed, 782 insertions(+), 8 deletions(-) create mode 100644 vectors/cryptography_vectors/pkcs7/signature-empty-certs.der create mode 100644 vectors/cryptography_vectors/pkcs7/signed-opaque.msg diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2385ba5da665..ce51a5ba937c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,12 @@ Changelog * Removed the deprecated ``CAST5``, ``SEED``, ``IDEA``, and ``Blowfish`` classes from the cipher module. These are still available in :doc:`/hazmat/decrepit/index`. +* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm, + in addition to AES-128. +* Added basic support for PKCS7 verification (including S/MIME 3.2) via + :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_der`, + :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_pem`, and + :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_smime`. .. _v45-0-7: diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index f93baa1a29b5..3d56ec321256 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -1008,6 +1008,9 @@ Custom PKCS7 Test Vectors * ``pkcs7/non-ascii-san.pem`` - An invalid certificate adapted for S/MIME signature & verification. It has an non-ASCII subject alternative name stored as `rfc822Name`. +* ``pkcs7/signed-opaque.msg``- A PKCS7 signed message, signed using opaque + signing (``application/pkcs7-mime`` content type), signed under the + private key of ``x509/custom/ca/ca.pem``, ``x509/custom/ca/ca_key.pem``. Custom OpenSSH Test Vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index fb49c7d14fb7..1c879069124f 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1262,6 +1262,28 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, -----END PRIVATE KEY----- """.strip() + verify_cert = b""" + -----BEGIN CERTIFICATE----- + MIIBhjCCASygAwIBAgICAwkwCgYIKoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAW + BgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEw + MTAwMDAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD + QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N + 2CxSJE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjRjBE + MCEGA1UdEQEB/wQXMBWBE2V4YW1wbGVAZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAU + /Ou02BLyyT2Zwzxn9H03feYT7fowCgYIKoZIzj0EAwIDSAAwRQIgUwIdC0Emkd6f + 17DeOXTlmTAhwSDJ2FTuyHESwei7wJcCIQCnr9NpBxbtJfEzxHGGyd7PxgpOLi5u + rk+8QfzGMmg/fw== + -----END CERTIFICATE----- + """.strip() + + verify_key = b""" + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe + jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs + UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF + -----END PRIVATE KEY----- + """.strip() + .. class:: PKCS7SignatureBuilder The PKCS7 signature builder can create both basic PKCS7 signed messages as @@ -1340,6 +1362,150 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :returns bytes: The signed PKCS7 message. +.. function:: pkcs7_verify_der(data, content=None, certificate=None, options=None) + + .. versionadded:: 45.0.0 + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.primitives import hashes, serialization + >>> from cryptography.hazmat.primitives.serialization import pkcs7 + >>> cert = x509.load_pem_x509_certificate(verify_cert) + >>> key = serialization.load_pem_private_key(verify_key, None) + >>> signed = pkcs7.PKCS7SignatureBuilder().set_data( + ... b"data to sign" + ... ).add_signer( + ... cert, key, hashes.SHA256() + ... ).sign( + ... serialization.Encoding.DER, [] + ... ) + >>> pkcs7.pkcs7_verify_der(signed) + + Deserialize and verify a DER-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple + versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the + verification succeeds, does not return anything. If the verification fails, raises an exception. + + :param data: The data, encoded in DER format. + :type data: bytes + + :param content: if specified, the content to verify against the signed message. If the content + is not specified, the function will look for the content in the signed message. Defaults to + None. + :type content: bytes or None + + :param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against + the signed message. If None, the function will look for the signer certificate in the signed + message. Defaults to None. + :type certificate: :class:`~cryptography.x509.Certificate` or None + + :raises ValueError: If the recipient certificate does not match any of the signers in the + PKCS7 data. + + :raises ValueError: If no content is specified and no content is found in the PKCS7 data. + + :raises ValueError: If the PKCS7 data is not of the signed data type. + + +.. function:: pkcs7_verify_pem(data, content=None, certificate=None, options=None) + + .. versionadded:: 45.0.0 + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.primitives import hashes, serialization + >>> from cryptography.hazmat.primitives.serialization import pkcs7 + >>> cert = x509.load_pem_x509_certificate(verify_cert) + >>> key = serialization.load_pem_private_key(verify_key, None) + >>> signed = pkcs7.PKCS7SignatureBuilder().set_data( + ... b"data to sign" + ... ).add_signer( + ... cert, key, hashes.SHA256() + ... ).sign( + ... serialization.Encoding.PEM, [] + ... ) + >>> pkcs7.pkcs7_verify_pem(signed) + + Deserialize and verify a PEM-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple + versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the + verification succeeds, does not return anything. If the verification fails, raises an exception. + + :param data: The data, encoded in PEM format. + :type data: bytes + + :param content: if specified, the content to verify against the signed message. If the content + is not specified, the function will look for the content in the signed message. Defaults to + None. + :type content: bytes or None + + :param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against + the signed message. If None, the function will look for the signer certificate in the signed + message. Defaults to None. + :type certificate: :class:`~cryptography.x509.Certificate` or None + + :raises ValueError: If the PEM data does not have the PKCS7 tag. + + :raises ValueError: If the recipient certificate does not match any of the signers in the + PKCS7 data. + + :raises ValueError: If no content is specified and no content is found in the PKCS7 data. + + :raises ValueError: If the PKCS7 data is not of the signed data type. + + +.. function:: pkcs7_verify_smime(data, content=None, certificate=None, options=None) + + .. versionadded:: 45.0.0 + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.primitives import hashes, serialization + >>> from cryptography.hazmat.primitives.serialization import pkcs7 + >>> cert = x509.load_pem_x509_certificate(verify_cert) + >>> key = serialization.load_pem_private_key(verify_key, None) + >>> signed = pkcs7.PKCS7SignatureBuilder().set_data( + ... b"data to sign" + ... ).add_signer( + ... cert, key, hashes.SHA256() + ... ).sign( + ... serialization.Encoding.SMIME, [] + ... ) + >>> pkcs7.pkcs7_verify_smime(signed) + + Verify a PKCS7 signed message stored in a MIME message, by reading it, extracting the content + (if any) and signature, deserializing the signature and verifying it against the content. PKCS7 + (or S/MIME) has multiple versions, but this supports a subset of :rfc:`5751`, also known as + S/MIME Version 3.2. If the verification succeeds, does not return anything. If the verification + fails, raises an exception. + + :param data: The data, encoded in MIME format. + :type data: bytes + + :param content: if specified, the content to verify against the signed message. If the content + is not specified, the function will look for the content in the MIME message and in the + signature. Defaults to None. + :type content: bytes or None + + :param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against + the signed message. If None, the function will look for the signer certificate in the signed + message. Defaults to None. + :type certificate: :class:`~cryptography.x509.Certificate` or None + + :raises ValueError: If the MIME message is not a S/MIME signed message: content type is + different than ``multipart/signed`` or ``application/pkcs7-mime``. + + :raises ValueError: If the MIME message is a malformed ``multipart/signed`` S/MIME message: not + multipart, or multipart with more than 2 parts (content & signature). + + :raises ValueError: If the recipient certificate does not match any of the signers in the + PKCS7 data. + + :raises ValueError: If no content is specified and no content is found in the PKCS7 data. + + :raises ValueError: If the PKCS7 data is not of the signed data type. + .. class:: PKCS7EnvelopeBuilder The PKCS7 envelope builder can create encrypted S/MIME messages, @@ -1633,6 +1799,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, obtain the signer's certificate by other means (for example from a previously signed message). + Serialization Formats ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi index 358b135865a8..48169dd094d1 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi @@ -19,11 +19,6 @@ def encrypt_and_serialize( encoding: serialization.Encoding, options: Iterable[pkcs7.PKCS7Options], ) -> bytes: ... -def sign_and_serialize( - builder: pkcs7.PKCS7SignatureBuilder, - encoding: serialization.Encoding, - options: Iterable[pkcs7.PKCS7Options], -) -> bytes: ... def decrypt_der( data: bytes, certificate: x509.Certificate, @@ -42,6 +37,26 @@ def decrypt_smime( private_key: rsa.RSAPrivateKey, options: Iterable[pkcs7.PKCS7Options], ) -> bytes: ... +def sign_and_serialize( + builder: pkcs7.PKCS7SignatureBuilder, + encoding: serialization.Encoding, + options: Iterable[pkcs7.PKCS7Options], +) -> bytes: ... +def verify_der( + signature: bytes, + content: bytes | None = None, + certificate: x509.Certificate | None = None, +) -> None: ... +def verify_pem( + signature: bytes, + content: bytes | None = None, + certificate: x509.Certificate | None = None, +) -> None: ... +def verify_smime( + signature: bytes, + content: bytes | None = None, + certificate: x509.Certificate | None = None, +) -> None: ... def load_pem_pkcs7_certificates( data: bytes, ) -> list[x509.Certificate]: ... diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index dc3c290b73e6..38b806e356eb 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -27,6 +27,8 @@ Criticality, ExtensionPolicy, Policy, + PolicyBuilder, + Store, ) load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates @@ -307,6 +309,11 @@ def sign( return rust_pkcs7.sign_and_serialize(self, encoding, options) +pkcs7_verify_der = rust_pkcs7.verify_der +pkcs7_verify_pem = rust_pkcs7.verify_pem +pkcs7_verify_smime = rust_pkcs7.verify_smime + + class PKCS7EnvelopeBuilder: def __init__( self, @@ -479,6 +486,45 @@ def _smime_signed_encode( return fp.getvalue() +def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]: + message = email.message_from_bytes(data) + content_type = message.get_content_type() + if content_type == "multipart/signed": + payload = message.get_payload() + if not isinstance(payload, list): + raise ValueError( + "Malformed multipart/signed message: must be multipart" + ) + assert isinstance(payload[0], email.message.Message), ( + "Malformed multipart/signed message: first part (content) " + "must be a MIME message" + ) + assert isinstance(payload[1], email.message.Message), ( + "Malformed multipart/signed message: second part (signature) " + "must be a MIME message" + ) + return ( + bytes(payload[0].get_payload(decode=True)), + bytes(payload[1].get_payload(decode=True)), + ) + elif content_type == "application/pkcs7-mime": + return None, bytes(message.get_payload(decode=True)) + else: + raise ValueError("Not an S/MIME signed message") + + +def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None: + ca_policy, ee_policy = pkcs7_x509_extension_policies() + verifier = ( + PolicyBuilder() + .store(Store(certificates)) + .extension_policies(ca_policy=ca_policy, ee_policy=ee_policy) + .build_client_verifier() + ) + + verifier.verify(certificates[0], certificates[1:]) + + def _smime_enveloped_encode(data: bytes) -> bytes: m = email.message.Message() m.add_header("MIME-Version", "1.0") diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 3fb5abf154d5..23786f8eef9d 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -25,7 +25,6 @@ use crate::padding::PKCS7UnpaddingContext; use crate::pkcs12::symmetric_encrypt; #[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))] use crate::utils::cstr_from_literal; -#[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))] use crate::x509::certificate::load_der_x509_certificate; use crate::{exceptions, types, x509}; @@ -201,6 +200,7 @@ fn decrypt_smime<'p>( decrypt_der(py, data, certificate, private_key, options) } + #[pyo3::pyfunction] fn decrypt_pem<'p>( py: pyo3::Python<'p>, @@ -672,6 +672,208 @@ fn compute_pkcs7_signature_algorithm<'p>( } } +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content = None, certificate = None))] +fn verify_smime<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: Option>, +) -> CryptographyResult<()> { + // Parse the email + let py_content_and_signature = types::SMIME_SIGNED_DECODE.get(py)?.call1((signature,))?; + + // Extract the signature + let py_signature = py_content_and_signature.get_item(1)?; + let signature = py_signature.extract()?; + + // Extract the content: if content is specified, use it, otherwise use the content from the + // email. It can be None (opaque signing) or bytes (clear signing with multipart/signed email). + // RFC5751 specified that receiving agents MUST be able to handle both cases. + let py_content = py_content_and_signature.get_item(0)?; + let content = match content { + Some(data) => Some(data), + None => { + if !py_content.is_none() { + Some(py_content.extract()?) + } else { + None + } + } + }; + + verify_der(py, signature, content, certificate) +} + +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content = None, certificate = None))] +fn verify_pem<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: Option>, +) -> CryptographyResult<()> { + let pem_str = std::str::from_utf8(signature) + .map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?; + let pem = pem::parse(pem_str) + .map_err(|_| pyo3::exceptions::PyValueError::new_err("Failed to parse PEM data"))?; + + // Raise error if the PEM tag is not PKCS7 + if pem.tag() != "PKCS7" { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The provided PEM data does not have the PKCS7 tag.", + ), + )); + } + + verify_der(py, &pem.into_contents(), content, certificate) +} + +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content = None, certificate = None))] +fn verify_der<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: Option>, +) -> CryptographyResult<()> { + // Verify the data + let content_info = asn1::parse_single::>(signature)?; + match content_info.content { + pkcs7::Content::SignedData(signed_data) => { + // Extract signed data + let signed_data = signed_data.into_inner(); + + // Extract the signer certificate: either from given value, or from signed data + let certificate = match certificate { + Some(cert) => cert, + None => { + let certificates = signed_data.certificates; + match certificates { + Some(certificates) => { + let mut certificates = certificates.unwrap_read().clone(); + match certificates.next() { + Some(cert) => { + let cert_bytes = + pyo3::types::PyBytes::new(py, &asn1::write_single(&cert)?) + .unbind(); + let py_cert = load_der_x509_certificate(py, cert_bytes, None)?; + pyo3::Bound::new(py, py_cert)? + } + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The PKCS7 data has an empty certificates attributes.", + ), + )); + } + } + } + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The PKCS7 data does not contain any certificate.", + ), + )); + } + } + } + }; + + // Get recipients, and find the one matching with the signer infos (if any) + // TODO: what to do for multiple certificates? + let mut signer_infos = signed_data.signer_infos.unwrap_read().clone(); + let signer_certificate = certificate.get().raw.borrow_dependent(); + let signer_serial_number = signer_certificate.tbs_cert.serial; + let signer_issuer = signer_certificate.tbs_cert.issuer.clone(); + let found_signer_info = signer_infos.find(|info| { + info.issuer_and_serial_number.serial_number == signer_serial_number + && info.issuer_and_serial_number.issuer == signer_issuer + }); + + // Raise error when no signer is found + let signer_info = match found_signer_info { + Some(info) => info, + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "No signer found that matches the given certificate.", + ), + )); + } + }; + + // Prepare the content: try to use the authenticated attributes, then the content stored + // in the signed data, then the provided content. If None of these are available, raise + // an error. TODO: what should the order be? + let data = match signer_info.authenticated_attributes { + Some(attrs) => Cow::Owned(asn1::write_single(&attrs)?), + None => match content { + Some(data) => Cow::Borrowed(data), + None => match signed_data.content_info.content { + pkcs7::Content::Data(Some(data)) => Cow::Borrowed(data.into_inner()), + _ => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "No content stored in the signature or provided.", + ), + )); + } + }, + }, + }; + + // For RSA signatures (with no PSS padding), the OID is always the same no matter the + // digest algorithm. We need to modify the algorithm identifier to add the hash + // algorithm information. We are checking for RSA-256, which the S/MIME v3.2 RFC + // specifies as MUST support (https://datatracker.ietf.org/doc/html/rfc5751#section-2.2) + let signature_algorithm = match signer_info.digest_encryption_algorithm.oid() { + &oid::RSA_OID => match signer_info.digest_algorithm.oid() { + &oid::SHA256_OID => common::AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: common::AlgorithmParameters::RsaWithSha256(Some(())), + }, + _ => { + return Err(CryptographyError::from( + exceptions::UnsupportedAlgorithm::new_err(( + "Only SHA-256 is currently supported for content verification with RSA.", + exceptions::Reasons::UNSUPPORTED_SERIALIZATION, + )), + )) + } + }, + _ => signer_info.digest_encryption_algorithm, + }; + + // Verify the signature + x509::sign::verify_signature_with_signature_algorithm( + py, + certificate.call_method0(pyo3::intern!(py, "public_key"))?, + &signature_algorithm, + signer_info.encrypted_digest, + &data, + )?; + + // Verify the certificate + let certificates = pyo3::types::PyList::empty(py); + certificates.append(certificate)?; + types::VERIFY_PKCS7_CERTIFICATES + .get(py)? + .call1((certificates,))?; + } + _ => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The PKCS7 data is not a SignedData structure.", + ), + )); + } + }; + + Ok(()) +} + fn smime_canonicalize(data: &[u8], text_mode: bool) -> (Cow<'_, [u8]>, Cow<'_, [u8]>) { let mut new_data_with_header = vec![]; let mut new_data_without_header = vec![]; @@ -811,7 +1013,7 @@ pub(crate) mod pkcs7_mod { use super::{ decrypt_der, decrypt_pem, decrypt_smime, encrypt_and_serialize, load_der_pkcs7_certificates, load_pem_pkcs7_certificates, serialize_certificates, - sign_and_serialize, + sign_and_serialize, verify_der, verify_pem, verify_smime, }; } diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index c24c7f2a8f7c..554564e15e95 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -374,6 +374,16 @@ pub static SMIME_SIGNED_ENCODE: LazyPyImport = LazyPyImport::new( &["_smime_signed_encode"], ); +pub static SMIME_SIGNED_DECODE: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.serialization.pkcs7", + &["_smime_signed_decode"], +); + +pub static VERIFY_PKCS7_CERTIFICATES: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.serialization.pkcs7", + &["_verify_pkcs7_certificates"], +); + pub static PKCS12KEYANDCERTIFICATES: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs12", &["PKCS12KeyAndCertificates"], diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index ec840d9ef015..8d800027e63e 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -15,7 +15,12 @@ from cryptography.exceptions import _Reasons from cryptography.hazmat.bindings._rust import test_support from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa +from cryptography.hazmat.primitives.asymmetric import ( + ed25519, + padding, + rsa, + types, +) from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.serialization import pkcs7 from cryptography.x509.oid import ( @@ -1008,6 +1013,286 @@ def test_add_multiple_additional_certs(self, backend): ) +@pytest.mark.supported( + only_if=lambda backend: backend.pkcs7_supported(), + skip_message="Requires OpenSSL with PKCS7 support", +) +class TestPKCS7Verify: + @pytest.fixture(name="data") + def fixture_data(self, backend) -> bytes: + return b"Hello world!" + + @pytest.fixture(name="certificate") + def fixture_certificate(self, backend) -> x509.Certificate: + return load_vectors_from_file( + os.path.join("pkcs7", "ca.pem"), + loader=lambda pemfile: x509.load_pem_x509_certificate( + pemfile.read() + ), + mode="rb", + ) + + @pytest.fixture(name="private_key") + def fixture_private_key(self, backend) -> types.PrivateKeyTypes: + return load_vectors_from_file( + 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", + ) + + def test_not_a_cert(self, backend): + with pytest.raises(TypeError): + pkcs7.pkcs7_verify_der(b"", certificate=b"wrong_type") # type: ignore[arg-type] + + @pytest.mark.parametrize( + "signing_options", + [ + [], + [pkcs7.PKCS7Options.NoAttributes], + ], + ) + def test_pkcs7_verify_der( + self, backend, data, certificate, private_key, signing_options + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signature = builder.sign(serialization.Encoding.DER, signing_options) + + # Verification + pkcs7.pkcs7_verify_der(signature) + + def test_pkcs7_verify_der_with_certificate( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [pkcs7.PKCS7Options.NoCerts] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + pkcs7.pkcs7_verify_der(signature, certificate=certificate) + + def test_pkcs7_verify_der_empty_certificates(self, backend): + # Getting a signature without certificates: empty list, not None + signature_empty_certificates = load_vectors_from_file( + os.path.join("pkcs7", "signature-empty-certs.der"), + loader=lambda derfile: derfile.read(), + mode="rb", + ) + + # Verification + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature_empty_certificates) + + def test_pkcs7_verify_der_no_certificates( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [pkcs7.PKCS7Options.NoCerts] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature) + + def test_pkcs7_verify_der_with_content( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [ + pkcs7.PKCS7Options.NoAttributes, + pkcs7.PKCS7Options.DetachedSignature, + ] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + pkcs7.pkcs7_verify_der(signature, content=data) + + def test_pkcs7_verify_der_no_content( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [ + pkcs7.PKCS7Options.NoAttributes, + pkcs7.PKCS7Options.DetachedSignature, + ] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature) + + def test_pkcs7_verify_der_ecdsa_certificate(self, backend, data): + # Getting an ECDSA certificate + certificate, private_key = _load_cert_key() + + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signature = builder.sign(serialization.Encoding.DER, []) + + # Verification with another certificate + pkcs7.pkcs7_verify_der(signature) + + def test_pkcs7_verify_invalid_signature( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [ + pkcs7.PKCS7Options.NoAttributes, + pkcs7.PKCS7Options.DetachedSignature, + ] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + with pytest.raises(exceptions.InvalidSignature): + pkcs7.pkcs7_verify_der(signature, content=b"Different") + + def test_pkcs7_verify_der_wrong_certificate( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signature = builder.sign(serialization.Encoding.DER, []) + + # Verification with another certificate + rsa_certificate, _ = _load_rsa_cert_key() + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature, certificate=rsa_certificate) + + def test_pkcs7_verify_der_unsupported_rsa_digest_algorithm( + self, backend, data + ): + certificate, private_key = _load_rsa_cert_key() + + # Signature with an unsupported digest algorithm + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA384()) + ) + signature = builder.sign(serialization.Encoding.DER, []) + + # Verification + with pytest.raises(exceptions.UnsupportedAlgorithm): + pkcs7.pkcs7_verify_der(signature) + + def test_pkcs7_verify_pem(self, backend, data, certificate, private_key): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signature = builder.sign(serialization.Encoding.PEM, []) + + # Verification + pkcs7.pkcs7_verify_pem(signature, data, certificate) + + def test_pkcs7_verify_pem_with_wrong_tag(self, backend, data, certificate): + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_pem( + certificate.public_bytes(serialization.Encoding.PEM) + ) + + def test_pkcs7_verify_pem_not_signed(self, backend, data, certificate): + # Getting some enveloped data + enveloped = load_vectors_from_file( + os.path.join("pkcs7", "enveloped.pem"), + loader=lambda pemfile: pemfile.read(), + mode="rb", + ) + + # Verification + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_pem(enveloped) + + def test_pkcs7_verify_smime(self, backend, data, certificate, private_key): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signed = builder.sign(serialization.Encoding.SMIME, []) + + # Verification + pkcs7.pkcs7_verify_smime(signed) + + def test_pkcs7_verify_smime_with_content( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signed = builder.sign(serialization.Encoding.SMIME, []) + + # Verification + pkcs7.pkcs7_verify_smime(signed, content=data) + + def test_pkcs7_verify_smime_opaque_signing(self, backend): + # Signature + signed = load_vectors_from_file( + os.path.join("pkcs7", "signed-opaque.msg"), + loader=lambda file: file.read(), + mode="rb", + ) + + # Verification + pkcs7.pkcs7_verify_smime(signed) + + @pytest.mark.parametrize( + "signature", + [ + b"Content-Type: text/plain;\nHello world!", + b"Content-Type: multipart/signed;\nHello world!", + ], + ) + def test_pkcs7_verify_smime_wrong_format(self, backend, signature): + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_smime(signature) + + def _load_rsa_cert_key(): key = load_vectors_from_file( os.path.join("x509", "custom", "ca", "rsa_key.pem"), diff --git a/vectors/cryptography_vectors/pkcs7/signature-empty-certs.der b/vectors/cryptography_vectors/pkcs7/signature-empty-certs.der new file mode 100644 index 0000000000000000000000000000000000000000..05698d83a15d89dc29059848a55e882408a106df GIT binary patch literal 574 zcmXqLVzOf6)N1o+`_9YA&a|M3N!y@_Nu7z2(U9MOmyI)_&4V$OnT3gwmBBz7p^$L_ z9}AC1YEDkRLV12sPKx3J2E!)C4+c$)Z-8126b*O{xeYkkm_u2Zd6=9XnFN%D8Gcq2 zd0$?4X@SgT<4>VzW-n-5XdnSMo|D;-8_r>8G?0OFI9Uz(#d(cP4GaxU4NVM<3=N|U z^bxWwhDt072ZH0gA8nuice181t8?~`PD4L0`*jE6&i~n6`&sJdQz-*8ga&>?O#^iU zpk+{hGHM}n#E>~mASXiuiV+wbEKQ6IOY8&oH0RY>M|v?tF||FZ)7@A2sCnQ2#qU-( z<`?Q7R}Y;j^tE3?^8bMu)&a6>Uh4d1xt;FtoZp=xde@%RR5!1h@{hB{7B1}AQJANB zbRdvV7f<;U7FdOsYe$`evNx|^2xt`b(Vs@1C57fsZ@uAeW>PE m5MIZ}yY0%}v-@UWDBbns)}?35Wv@9-oo}c#;ev_8CVK$ew9Bvn literal 0 HcmV?d00001 diff --git a/vectors/cryptography_vectors/pkcs7/signed-opaque.msg b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg new file mode 100644 index 000000000000..29a805ca186f --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg @@ -0,0 +1,40 @@ +MIME-Version: 1.0 +Content-Type: application/pkcs7-mime; smime-type=signed-data; name=smime.p7m +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=smime.p7m + +MIIGagYJKoZIhvcNAQcCoIIGWzCCBlcCAQExDzANBglghkgBZQMEAgEFADALBgkq +hkiG9w0BBwGgggP7MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkq +hkiG9w0BAQsFADANMQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAx +MDUxMDQ4MjhaMCUxIzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBs +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHX +ZLrstOSSEhiEcUre3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD +4v3A7TnmHzzhvqCsBTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFj +G9xLTayHXOKqCF6dHd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9 +tu/ljFBWYSwPeiNYnYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9D +MHtXMkXYe/C+WSm1MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw +0QIDAQABo4IBOTCCATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6 +z3uk4fJeifohSntD2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29m +dC5jb20ucGwvY3JsMGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDov +L2NhLnRyaXNvZnQuY29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2Nh +LnRyaXNvZnQuY29tLnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5j +b20ucGwwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0 +/QFDFX/CCMsX356GImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQEL +BQADggEBAL3IiscaIqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdR +tqCCiaCKtJED/f/9NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1N +C4FfDfnzyG+DZHusnz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwt +uGaWfNz/w324ukcVpMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+ +bLnHaoozZwJ/UqBl9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxa +IRp5uc8HnUd5e4lXbr+Ixxcl3WHckkkxggIzMIICLwIBATAhMA0xCzAJBgNVBAMM +AkNBAhAjEwD5eHJL067SoBzTM/JVMA0GCWCGSAFlAwQCAQUAoIHkMBgGCSqGSIb3 +DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI1MDExNTEwMTY0Mlow +LwYJKoZIhvcNAQkEMSIEIMBTXkvit5/9kykTBUNr+IkxTko/rsBez/y7ffMa2eUa +MHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglg +hkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMC +AgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIBAG0i +Crj4XL+zSIeEf/xGL3sQ4V1EdgjTXSCNgcUYyDVv/bsv9+C4gt5kwbdGE+zAacwo +MGn64jzkTft5SodG8tJ8Y/NM3+G6NDLzBVS89TYXl2/UFlqfKS8Te8lU+Gg+/5J1 +e04ulJR0UXIFVMHQs/Dn8/koUYOrDhcl9ULohhbMArqdy8BAuaEcOn2F+1ORF85G +T1Ks16qVFaSZIsKulsrMiIdTOu+ww78VTneQH7ITVqE04w8x6yQohLBl3jHAT/lA +RufRRBEjnSkzccM2tNjOXAKGqaoAAZz9IqR5HLR5NGbHzapQo8Ft7Ri0//aZYBZg +aqBoycKshpupsMECEPU= \ No newline at end of file