From 748700807989a0eaa5d95a2bc71569d94dd387bc Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:52:27 +0200 Subject: [PATCH 01/20] 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 --- docs/development/test-vectors.rst | 8 + .../hazmat/primitives/serialization/pkcs7.py | 112 +++++++++++++ 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 +++ 7 files changed, 327 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/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 54b645e2b507..f019e2567a31 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -1003,6 +1003,14 @@ 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.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..d9a066e87291 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,111 @@ 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. Some specifications + that differ from the standard ones: + - Certificates used as end entities (i.e., the cert used to sign + a PKCS#7/SMIME message) should not have ca=true in their basic + constraints extension. + - EKU_CLIENT_AUTH_OID is not required + - EKU_EMAIL_PROTECTION_OID is required + """ + + # CA policy - TODO: is default CA policy sufficient? Too much? + ca_policy = ExtensionPolicy.webpki_defaults_ca() + + # EE policy + def _validate_basic_constraints( + policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None + ) -> None: + 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: + 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: + 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 2a1fb54e0bb99c123b39b5d99996fc8a938225c7 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 11:56:46 +0200 Subject: [PATCH 02/20] minor changes based on comments --- docs/development/test-vectors.rst | 4 +-- .../hazmat/primitives/serialization/pkcs7.py | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index f019e2567a31..8781d6c76a6e 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -1004,9 +1004,7 @@ Custom PKCS7 Test Vectors 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.pem`` - A certificate adapted for S/MIME signature & verification. - Its private key is ``pkcs7/ca_key.pem`` . + 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 diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index d9a066e87291..dc3c290b73e6 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -62,28 +62,33 @@ class PKCS7Options(utils.Enum): def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]: """ - Gets the default X.509 extension policy for S/MIME. Some specifications - that differ from the standard ones: - - Certificates used as end entities (i.e., the cert used to sign - a PKCS#7/SMIME message) should not have ca=true in their basic - constraints extension. - - EKU_CLIENT_AUTH_OID is not required - - EKU_EMAIL_PROTECTION_OID is required + 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 - TODO: is default CA policy sufficient? Too much? + # 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 @@ -128,6 +133,10 @@ def _validate_subject_alternative_name( 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 From 8586b73f0ee9af4f62ed904234433d6651bfbbc5 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:02:59 +0200 Subject: [PATCH 03/20] PKCS7 signing (first version) no certificate verification as of now handling PEM, DER, SMIME formats added tests & documentation accordingly --- docs/development/test-vectors.rst | 3 + .../primitives/asymmetric/serialization.rst | 148 +++++++++++++++ .../hazmat/bindings/_rust/pkcs7.pyi | 28 ++- .../hazmat/primitives/serialization/pkcs7.py | 34 ++++ src/rust/src/pkcs7.rs | 167 ++++++++++++++++- src/rust/src/types.rs | 5 + tests/hazmat/primitives/test_pkcs7.py | 169 ++++++++++++++++++ .../pkcs7/signed-opaque.msg | 21 +++ 8 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 vectors/cryptography_vectors/pkcs7/signed-opaque.msg diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 8781d6c76a6e..d01c6dcbb578 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -1009,6 +1009,9 @@ Custom PKCS7 Test Vectors & 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`. +* ``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..6f84d74890fa 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1340,6 +1340,154 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :returns bytes: The signed PKCS7 message. +.. function:: pkcs7_verify_der(data, content, certificate, options) + + .. 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(ca_cert) + >>> key = serialization.load_pem_private_key(ca_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, None, cert, []) + + 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. + :type data: bytes or None + + :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed + message. + + :param options: A list of + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For + this operation, no options are supported as of now. + + :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, certificate, options) + + .. 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(ca_cert) + >>> key = serialization.load_pem_private_key(ca_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, None, cert, []) + + 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. + :type data: bytes or None + + :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed + message. + + :param options: A list of + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For + this operation, no options are supported as of now. + + :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, certificate, options) + + .. 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(ca_cert) + >>> key = serialization.load_pem_private_key(ca_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, None, cert, []) + + 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. + :type data: bytes or None + + :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed + message. + + :param options: A list of + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For + this operation, no options are supported as of now. + + :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, diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi index 358b135865a8..ac063d0dcfca 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,29 @@ 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, + certificate: x509.Certificate, + options: Iterable[pkcs7.PKCS7Options], +) -> None: ... +def verify_pem( + signature: bytes, + content: bytes | None, + certificate: x509.Certificate, + options: Iterable[pkcs7.PKCS7Options], +) -> None: ... +def verify_smime( + signature: bytes, + content: bytes | None, + certificate: x509.Certificate, + options: Iterable[pkcs7.PKCS7Options], +) -> 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..2f91f07877fc 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -307,6 +307,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 +484,35 @@ 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" + ) + if not isinstance(payload[0], email.message.Message): + raise ValueError( + "Malformed multipart/signed message: first part (content) " + "must be a MIME message" + ) + if not isinstance(payload[1], email.message.Message): + raise ValueError( + "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 _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 5090ea27619e..c855c45fc9dc 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -201,6 +201,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 +673,170 @@ fn compute_pkcs7_signature_algorithm<'p>( } } +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content, certificate, options))] +fn verify_smime<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: pyo3::Bound<'p, x509::certificate::Certificate>, + options: &pyo3::Bound<'p, pyo3::types::PyList>, +) -> 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, options) +} + +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content, certificate, options))] +fn verify_pem<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: pyo3::Bound<'p, x509::certificate::Certificate>, + options: &pyo3::Bound<'p, pyo3::types::PyList>, +) -> 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, options) +} + +#[pyo3::pyfunction] +#[pyo3(signature = (signature, content, certificate, options))] +fn verify_der<'p>( + py: pyo3::Python<'p>, + signature: &[u8], + content: Option<&[u8]>, + certificate: pyo3::Bound<'p, x509::certificate::Certificate>, + options: &pyo3::Bound<'p, pyo3::types::PyList>, +) -> CryptographyResult<()> { + // Check the verify options + check_verify_options(py, options)?; + + // 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(); + + // 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) => &asn1::write_single(&attrs)?, + None => match content { + Some(data) => data, + None => match signed_data.content_info.content { + pkcs7::Content::Data(Some(data)) => data.into_inner(), + _ => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "No content stored in the signature or provided.", + ), + )); + } + }, + }, + }; + + // Verify the signature + x509::sign::verify_signature_with_signature_algorithm( + py, + certificate.call_method0(pyo3::intern!(py, "public_key"))?, + &signer_info.digest_encryption_algorithm, + signer_info.encrypted_digest, + data, + )?; + + // TODO: verify the certificates? + } + _ => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The PKCS7 data is not an SignedData structure.", + ), + )); + } + }; + + Ok(()) +} + +fn check_verify_options<'p>( + py: pyo3::Python<'p>, + options: &pyo3::Bound<'p, pyo3::types::PyList>, +) -> Result<(), CryptographyError> { + // Check if all options are from the PKCS7Options enum + let pkcs7_options = types::PKCS7_OPTIONS.get(py)?; + for opt in options.iter() { + if !opt.is_instance(&pkcs7_options)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "options must be from the PKCS7Options enum", + ), + )); + } + } + + 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 +976,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 304d7b421d70..efd020b905aa 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -374,6 +374,11 @@ 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 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 8d04a5419810..2fb03f7b159f 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1003,6 +1003,175 @@ 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: + certificate, _ = _load_cert_key() + return certificate + + @pytest.fixture(name="private_key") + def fixture_private_key(self, backend) -> rsa.RSAPrivateKey: + _, private_key = _load_cert_key() + return private_key + + def test_not_a_cert(self, backend): + with pytest.raises(TypeError): + pkcs7.pkcs7_verify_der(b"", b"", b"wrong_type", []) # type: ignore[arg-type] + + @pytest.mark.parametrize( + "invalid_options", + [ + [b"invalid"], + ], + ) + def test_pkcs7_verify_invalid_options( + self, backend, invalid_options, certificate + ): + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(b"", b"", certificate, invalid_options) + + @pytest.mark.parametrize( + "signing_options", + [ + [], + [pkcs7.PKCS7Options.NoAttributes], + [ + pkcs7.PKCS7Options.NoAttributes, + pkcs7.PKCS7Options.DetachedSignature, + ], + ], + ) + 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, data, certificate, []) + + def test_pkcs7_verify_der_no_data( + 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, None, certificate, []) + + 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, None, rsa_certificate, []) + + 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), + data, + certificate, + [], + ) + + 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, data, certificate, []) + + def test_pkcs7_verify_smime_without_content_argument( + 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, None, certificate, []) + + def test_pkcs7_verify_smime_opaque_signing( + self, backend, data, certificate, private_key + ): + # 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, None, certificate, []) + + @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, data, certificate, signature + ): + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_smime(signature, data, certificate, []) + + 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/signed-opaque.msg b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg new file mode 100644 index 000000000000..e998dfbbf99e --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg @@ -0,0 +1,21 @@ +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 + +MIIC4AYJKoZIhvcNAQcCoIIC0TCCAs0CAQExDzANBglghkgBZQMEAgEFADAbBgkq +hkiG9w0BBwGgDgQMSGVsbG8gd29ybGQhoIIBVzCCAVMwgfmgAwIBAgICAwkwCgYI +KoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD +QTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEwMTAwMDAwMFowJzELMAkGA1UEBhMC +VVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N2CxSJE4f4BBGZ7VfFblivTvPDG++ +Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYI +KoZIzj0EAwIDSQAwRgIhAL22GUvxR8zbSxhsrR0TVQ3XMC1iEljgc+qfWb0uguNr +AiEA04Udtg7mGT1DEN/wBMpcWJzDfsgR4lFH1O4Q7/iCr0wxggE9MIIBOQIBATAt +MCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQDDA9jcnlwdG9ncmFwaHkgQ0ECAgMJMA0G +CWCGSAFlAwQCAQUAoIGhMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI +hvcNAQkFMQ8XDTI1MDExMDEwMjQzMFowLwYJKoZIhvcNAQkEMSIEIMBTXkvit5/9 +kykTBUNr+IkxTko/rsBez/y7ffMa2eUaMDYGCSqGSIb3DQEJDzEpMCcwCwYJYIZI +AWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIzj0EAwIERjBE +AiAfusGb8f8j3wWBbvqTqS2WFLZYPDq8lOTQuWvzOsJjKwIgLkYBzQna4RPaFOP4 +ilRirp50A+Qss54wVH9J1InpZjA= \ No newline at end of file From 9c1bd29a344b4f6ad0161e63d2448ca8d9793931 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:00 +0200 Subject: [PATCH 04/20] doing assertions for now, to please mypy --- .../hazmat/primitives/serialization/pkcs7.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 2f91f07877fc..80fcf97f8aa8 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -493,16 +493,14 @@ def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]: raise ValueError( "Malformed multipart/signed message: must be multipart" ) - if not isinstance(payload[0], email.message.Message): - raise ValueError( - "Malformed multipart/signed message: first part (content) " - "must be a MIME message" - ) - if not isinstance(payload[1], email.message.Message): - raise ValueError( - "Malformed multipart/signed message: second part (signature) " - "must be a MIME message" - ) + 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)), From c2ce2cd862e21d8efe1aeaf575c6c08eb88b1e27 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:00 +0200 Subject: [PATCH 05/20] added more test coverage --- src/rust/src/pkcs7.rs | 2 +- tests/hazmat/primitives/test_pkcs7.py | 33 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index c855c45fc9dc..68fec3a8a786 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -809,7 +809,7 @@ fn verify_der<'p>( _ => { return Err(CryptographyError::from( pyo3::exceptions::PyValueError::new_err( - "The PKCS7 data is not an SignedData structure.", + "The PKCS7 data is not a SignedData structure.", ), )); } diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 2fb03f7b159f..0a1fffdb0966 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1063,6 +1063,25 @@ def test_pkcs7_verify_der( # Verification pkcs7.pkcs7_verify_der(signature, data, certificate, []) + def test_pkcs7_verify_der_no_content( + self, backend, data, certificate, private_key + ): + """ + Tests verification when needing the content stored in the PKCS7 signed + data structure. + """ + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [pkcs7.PKCS7Options.NoAttributes] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + pkcs7.pkcs7_verify_der(signature, None, certificate, []) + def test_pkcs7_verify_der_no_data( self, backend, data, certificate, private_key ): @@ -1082,6 +1101,20 @@ def test_pkcs7_verify_der_no_data( with pytest.raises(ValueError): pkcs7.pkcs7_verify_der(signature, None, certificate, []) + def test_pkcs7_verify_der_not_signed(self, backend, data): + # Encryption of data with a text/html content type header + certificate, _ = _load_rsa_cert_key() + builder = ( + pkcs7.PKCS7EnvelopeBuilder() + .set_data(b"Hello world!") + .add_recipient(certificate) + ) + enveloped = builder.encrypt(serialization.Encoding.DER, []) + + # Verification + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(enveloped, None, certificate, []) + def test_pkcs7_verify_der_wrong_certificate( self, backend, data, certificate, private_key ): From 41c3a336e0d1403c20bbdcdc065b7358e6440150 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:00 +0200 Subject: [PATCH 06/20] updated tests to avoid unsupported algorithm --- tests/hazmat/primitives/test_pkcs7.py | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 0a1fffdb0966..cc4619873e9a 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1101,19 +1101,33 @@ def test_pkcs7_verify_der_no_data( with pytest.raises(ValueError): pkcs7.pkcs7_verify_der(signature, None, certificate, []) - def test_pkcs7_verify_der_not_signed(self, backend, data): - # Encryption of data with a text/html content type header - certificate, _ = _load_rsa_cert_key() + def test_pkcs7_verify_invalid_signature( + self, backend, data, certificate, private_key + ): + # Signature builder = ( - pkcs7.PKCS7EnvelopeBuilder() - .set_data(b"Hello world!") - .add_recipient(certificate) + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + options = [pkcs7.PKCS7Options.NoAttributes] + signature = builder.sign(serialization.Encoding.DER, options) + + # Verification + with pytest.raises(exceptions.InvalidSignature): + pkcs7.pkcs7_verify_der(signature, b"Different", certificate, []) + + def test_pkcs7_verify_der_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", ) - enveloped = builder.encrypt(serialization.Encoding.DER, []) # Verification with pytest.raises(ValueError): - pkcs7.pkcs7_verify_der(enveloped, None, certificate, []) + pkcs7.pkcs7_verify_pem(enveloped, None, certificate, []) def test_pkcs7_verify_der_wrong_certificate( self, backend, data, certificate, private_key From e202bbfa67233869d76b30e12970965bc93ff655 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:30 +0200 Subject: [PATCH 07/20] first failing code for certificate verification --- .../primitives/asymmetric/serialization.rst | 6 ++++++ .../hazmat/primitives/serialization/pkcs7.py | 7 +++++++ src/rust/src/pkcs7.rs | 21 ++++++++++++++++++- src/rust/src/types.rs | 9 ++++++++ tests/hazmat/primitives/test_pkcs7.py | 15 +++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 6f84d74890fa..15aa99bf35d5 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1781,6 +1781,12 @@ 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). + .. attribute:: NoVerify + + For S/MIME verification only. Don't verify signers certificate. This is + useful when the signer's certificate is not available or when the signer's + certificate is not trusted. + Serialization Formats ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 80fcf97f8aa8..7a847e543294 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -58,6 +58,7 @@ class PKCS7Options(utils.Enum): NoCapabilities = "Don't embed SMIME capabilities" NoAttributes = "Don't embed authenticatedAttributes" NoCerts = "Don't embed signer certificate" + NoVerify = "Don't verify signers certificate" def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]: @@ -511,6 +512,12 @@ def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]: raise ValueError("Not an S/MIME signed message") +def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None: + builder = PolicyBuilder().store(Store(certificates)) + verifier = builder.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 68fec3a8a786..314479936019 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -804,7 +804,14 @@ fn verify_der<'p>( data, )?; - // TODO: verify the certificates? + // Verify the certificate + if !options.contains(types::PKCS7_NO_VERIFY.get(py)?)? { + let certificates = pyo3::types::PyList::empty(py); + certificates.append(certificate)?; + types::VERIFY_PKCS7_CERTIFICATES + .get(py)? + .call1((certificates,))?; + } } _ => { return Err(CryptographyError::from( @@ -834,6 +841,18 @@ fn check_verify_options<'p>( } } + // Check if any option is not PKCS7Options::NoVerify + let no_verify_option = types::PKCS7_NO_VERIFY.get(py)?; + for opt in options.iter() { + if !opt.eq(no_verify_option.clone())? { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "Only the following options are supported for verification: NoVerify", + ), + )); + } + } + Ok(()) } diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index efd020b905aa..cdc3ac9a89c5 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -353,6 +353,10 @@ pub static PKCS7_DETACHED_SIGNATURE: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", &["PKCS7Options", "DetachedSignature"], ); +pub static PKCS7_NO_VERIFY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.serialization.pkcs7", + &["PKCS7Options", "NoVerify"], +); pub static SMIME_ENVELOPED_ENCODE: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", @@ -379,6 +383,11 @@ pub static SMIME_SIGNED_DECODE: LazyPyImport = LazyPyImport::new( &["_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 cc4619873e9a..e74da6fa67f1 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1082,6 +1082,21 @@ def test_pkcs7_verify_der_no_content( # Verification pkcs7.pkcs7_verify_der(signature, None, certificate, []) + def test_pkcs7_verify_der_no_verify( + 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 + options = [pkcs7.PKCS7Options.NoVerify] + pkcs7.pkcs7_verify_der(signature, data, certificate, options) + def test_pkcs7_verify_der_no_data( self, backend, data, certificate, private_key ): From 89e84e7c0e179e5ff3b13601d0e42c3403262a70 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:30 +0200 Subject: [PATCH 08/20] handling mixed types with Cow --- src/rust/src/pkcs7.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 314479936019..45a89ce6c78a 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -779,11 +779,11 @@ fn verify_der<'p>( // 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) => &asn1::write_single(&attrs)?, + Some(attrs) => Cow::Owned(asn1::write_single(&attrs)?), None => match content { - Some(data) => data, + Some(data) => Cow::Borrowed(data), None => match signed_data.content_info.content { - pkcs7::Content::Data(Some(data)) => data.into_inner(), + pkcs7::Content::Data(Some(data)) => Cow::Borrowed(data.into_inner()), _ => { return Err(CryptographyError::from( pyo3::exceptions::PyValueError::new_err( @@ -801,7 +801,7 @@ fn verify_der<'p>( certificate.call_method0(pyo3::intern!(py, "public_key"))?, &signer_info.digest_encryption_algorithm, signer_info.encrypted_digest, - data, + &data, )?; // Verify the certificate From 62c3c1330d6d07d0ac81bfeff090fa7fdff2041b Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:30 +0200 Subject: [PATCH 09/20] feat: functions now have optional keyword arguments certificate is now optional feat: handling RSA case feat: No signature parameter adapted tests accordingly --- .../hazmat/bindings/_rust/pkcs7.pyi | 18 +- .../hazmat/primitives/serialization/pkcs7.py | 1 + src/rust/src/pkcs7.rs | 168 ++++++++++++------ src/rust/src/types.rs | 4 + tests/hazmat/primitives/test_pkcs7.py | 149 ++++++++++------ .../pkcs7/signed-opaque.msg | 51 ++++-- .../pkcs7/verify_cert.pem | 24 +++ .../cryptography_vectors/pkcs7/verify_key.pem | 27 +++ 8 files changed, 313 insertions(+), 129 deletions(-) create mode 100644 vectors/cryptography_vectors/pkcs7/verify_cert.pem create mode 100644 vectors/cryptography_vectors/pkcs7/verify_key.pem diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi index ac063d0dcfca..5391b4c9d4f4 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi @@ -44,21 +44,21 @@ def sign_and_serialize( ) -> bytes: ... def verify_der( signature: bytes, - content: bytes | None, - certificate: x509.Certificate, - options: Iterable[pkcs7.PKCS7Options], + content: bytes | None = None, + certificate: x509.Certificate | None = None, + options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def verify_pem( signature: bytes, - content: bytes | None, - certificate: x509.Certificate, - options: Iterable[pkcs7.PKCS7Options], + content: bytes | None = None, + certificate: x509.Certificate | None = None, + options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def verify_smime( signature: bytes, - content: bytes | None, - certificate: x509.Certificate, - options: Iterable[pkcs7.PKCS7Options], + content: bytes | None = None, + certificate: x509.Certificate | None = None, + options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def load_pem_pkcs7_certificates( data: bytes, diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 7a847e543294..2e9b2912ca46 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -59,6 +59,7 @@ class PKCS7Options(utils.Enum): NoAttributes = "Don't embed authenticatedAttributes" NoCerts = "Don't embed signer certificate" NoVerify = "Don't verify signers certificate" + NoSigs = "Don't verify signature" def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]: diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 45a89ce6c78a..315ca934f754 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -674,13 +674,13 @@ fn compute_pkcs7_signature_algorithm<'p>( } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content, certificate, options))] +#[pyo3(signature = (signature, content = None, certificate = None, options = None))] fn verify_smime<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, - certificate: pyo3::Bound<'p, x509::certificate::Certificate>, - options: &pyo3::Bound<'p, pyo3::types::PyList>, + certificate: Option>, + options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { // Parse the email let py_content_and_signature = types::SMIME_SIGNED_DECODE.get(py)?.call1((signature,))?; @@ -708,13 +708,13 @@ fn verify_smime<'p>( } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content, certificate, options))] +#[pyo3(signature = (signature, content = None, certificate = None, options = None))] fn verify_pem<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, - certificate: pyo3::Bound<'p, x509::certificate::Certificate>, - options: &pyo3::Bound<'p, pyo3::types::PyList>, + certificate: Option>, + options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { let pem_str = std::str::from_utf8(signature) .map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?; @@ -734,15 +734,19 @@ fn verify_pem<'p>( } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content, certificate, options))] +#[pyo3(signature = (signature, content = None, certificate = None, options = None))] fn verify_der<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, - certificate: pyo3::Bound<'p, x509::certificate::Certificate>, - options: &pyo3::Bound<'p, pyo3::types::PyList>, + certificate: Option>, + options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { // Check the verify options + let options = match options { + Some(options) => options, + None => &pyo3::types::PyList::empty(py), + }; check_verify_options(py, options)?; // Verify the data @@ -752,57 +756,116 @@ fn verify_der<'p>( // Extract signed data let signed_data = signed_data.into_inner(); - // 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, + // Extract the signer certificate: either from given value, or from signed data + let certificate = match certificate { + Some(cert) => cert, None => { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "No signer found that matches the given certificate.", - ), - )); + 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 does not contain any certificate.", + ), + )); + } + } + } + None => { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err( + "The PKCS7 data does not contain any 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()), + // Get recipients, and find the one matching with the signer infos (if any) + // TODO: what to do for multiple certificates? + if !options.contains(types::PKCS7_NO_SIGS.get(py)?)? { + 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( pyo3::exceptions::PyValueError::new_err( - "No content stored in the signature or provided.", + "Unsupported hash algorithm with RSA.", ), - )); + )) } }, - }, - }; - - // Verify the signature - x509::sign::verify_signature_with_signature_algorithm( - py, - certificate.call_method0(pyo3::intern!(py, "public_key"))?, - &signer_info.digest_encryption_algorithm, - signer_info.encrypted_digest, - &data, - )?; + _ => 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 if !options.contains(types::PKCS7_NO_VERIFY.get(py)?)? { @@ -843,11 +906,12 @@ fn check_verify_options<'p>( // Check if any option is not PKCS7Options::NoVerify let no_verify_option = types::PKCS7_NO_VERIFY.get(py)?; + let no_sigs_option = types::PKCS7_NO_SIGS.get(py)?; for opt in options.iter() { - if !opt.eq(no_verify_option.clone())? { + if opt.ne(no_verify_option.clone())? & opt.ne(no_sigs_option.clone())? { return Err(CryptographyError::from( pyo3::exceptions::PyValueError::new_err( - "Only the following options are supported for verification: NoVerify", + "Only the following options are supported for verification: NoVerify, NoSigs", ), )); } diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index cdc3ac9a89c5..53e268147a33 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -357,6 +357,10 @@ pub static PKCS7_NO_VERIFY: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", &["PKCS7Options", "NoVerify"], ); +pub static PKCS7_NO_SIGS: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.serialization.pkcs7", + &["PKCS7Options", "NoSigs"], +); pub static SMIME_ENVELOPED_ENCODE: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index e74da6fa67f1..bb30153297c6 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 ( @@ -1014,17 +1019,27 @@ def fixture_data(self, backend) -> bytes: @pytest.fixture(name="certificate") def fixture_certificate(self, backend) -> x509.Certificate: - certificate, _ = _load_cert_key() - return certificate + return load_vectors_from_file( + os.path.join("pkcs7", "verify_cert.pem"), + loader=lambda pemfile: x509.load_pem_x509_certificate( + pemfile.read() + ), + mode="rb", + ) @pytest.fixture(name="private_key") - def fixture_private_key(self, backend) -> rsa.RSAPrivateKey: - _, private_key = _load_cert_key() - return private_key + def fixture_private_key(self, backend) -> types.PrivateKeyTypes: + return load_vectors_from_file( + os.path.join("pkcs7", "verify_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"", b"", b"wrong_type", []) # type: ignore[arg-type] + pkcs7.pkcs7_verify_der(b"", certificate=b"wrong_type") # type: ignore[arg-type] @pytest.mark.parametrize( "invalid_options", @@ -1032,21 +1047,15 @@ def test_not_a_cert(self, backend): [b"invalid"], ], ) - def test_pkcs7_verify_invalid_options( - self, backend, invalid_options, certificate - ): + def test_pkcs7_verify_invalid_options(self, backend, invalid_options): with pytest.raises(ValueError): - pkcs7.pkcs7_verify_der(b"", b"", certificate, invalid_options) + pkcs7.pkcs7_verify_der(b"", options=invalid_options) @pytest.mark.parametrize( "signing_options", [ [], [pkcs7.PKCS7Options.NoAttributes], - [ - pkcs7.PKCS7Options.NoAttributes, - pkcs7.PKCS7Options.DetachedSignature, - ], ], ) def test_pkcs7_verify_der( @@ -1061,28 +1070,45 @@ def test_pkcs7_verify_der( signature = builder.sign(serialization.Encoding.DER, signing_options) # Verification - pkcs7.pkcs7_verify_der(signature, data, certificate, []) + pkcs7.pkcs7_verify_der(signature) - def test_pkcs7_verify_der_no_content( + @pytest.mark.parametrize( + "options", + [ + [pkcs7.PKCS7Options.NoVerify], + [pkcs7.PKCS7Options.NoSigs], + ], + ) + def test_pkcs7_verify_der_with_options( + self, backend, data, certificate, private_key, options + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA256()) + ) + signature = builder.sign(serialization.Encoding.DER, []) + + # Verification + pkcs7.pkcs7_verify_der(signature, options=options) + + def test_pkcs7_verify_der_with_certificate( self, backend, data, certificate, private_key ): - """ - Tests verification when needing the content stored in the PKCS7 signed - data structure. - """ # Signature builder = ( pkcs7.PKCS7SignatureBuilder() .set_data(data) .add_signer(certificate, private_key, hashes.SHA256()) ) - options = [pkcs7.PKCS7Options.NoAttributes] + options = [pkcs7.PKCS7Options.NoCerts] signature = builder.sign(serialization.Encoding.DER, options) # Verification - pkcs7.pkcs7_verify_der(signature, None, certificate, []) + pkcs7.pkcs7_verify_der(signature, certificate=certificate) - def test_pkcs7_verify_der_no_verify( + def test_pkcs7_verify_der_no_certificates( self, backend, data, certificate, private_key ): # Signature @@ -1091,13 +1117,14 @@ def test_pkcs7_verify_der_no_verify( .set_data(data) .add_signer(certificate, private_key, hashes.SHA256()) ) - signature = builder.sign(serialization.Encoding.DER, []) + options = [pkcs7.PKCS7Options.NoCerts] + signature = builder.sign(serialization.Encoding.DER, options) # Verification - options = [pkcs7.PKCS7Options.NoVerify] - pkcs7.pkcs7_verify_der(signature, data, certificate, options) + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature) - def test_pkcs7_verify_der_no_data( + def test_pkcs7_verify_der_with_content( self, backend, data, certificate, private_key ): # Signature @@ -1113,10 +1140,9 @@ def test_pkcs7_verify_der_no_data( signature = builder.sign(serialization.Encoding.DER, options) # Verification - with pytest.raises(ValueError): - pkcs7.pkcs7_verify_der(signature, None, certificate, []) + pkcs7.pkcs7_verify_der(signature, content=data) - def test_pkcs7_verify_invalid_signature( + def test_pkcs7_verify_der_no_content( self, backend, data, certificate, private_key ): # Signature @@ -1125,24 +1151,34 @@ def test_pkcs7_verify_invalid_signature( .set_data(data) .add_signer(certificate, private_key, hashes.SHA256()) ) - options = [pkcs7.PKCS7Options.NoAttributes] + 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, b"Different", certificate, []) + with pytest.raises(ValueError): + pkcs7.pkcs7_verify_der(signature) - def test_pkcs7_verify_der_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", + 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(ValueError): - pkcs7.pkcs7_verify_pem(enveloped, None, certificate, []) + 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 @@ -1158,7 +1194,7 @@ def test_pkcs7_verify_der_wrong_certificate( # Verification with another certificate rsa_certificate, _ = _load_rsa_cert_key() with pytest.raises(ValueError): - pkcs7.pkcs7_verify_der(signature, None, rsa_certificate, []) + pkcs7.pkcs7_verify_der(signature, certificate=rsa_certificate) def test_pkcs7_verify_pem(self, backend, data, certificate, private_key): # Signature @@ -1175,12 +1211,21 @@ def test_pkcs7_verify_pem(self, backend, data, certificate, private_key): 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), - data, - certificate, - [], + 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 = ( @@ -1191,9 +1236,9 @@ def test_pkcs7_verify_smime(self, backend, data, certificate, private_key): signed = builder.sign(serialization.Encoding.SMIME, []) # Verification - pkcs7.pkcs7_verify_smime(signed, data, certificate, []) + pkcs7.pkcs7_verify_smime(signed) - def test_pkcs7_verify_smime_without_content_argument( + def test_pkcs7_verify_smime_with_content( self, backend, data, certificate, private_key ): # Signature @@ -1205,7 +1250,7 @@ def test_pkcs7_verify_smime_without_content_argument( signed = builder.sign(serialization.Encoding.SMIME, []) # Verification - pkcs7.pkcs7_verify_smime(signed, None, certificate, []) + pkcs7.pkcs7_verify_smime(signed, content=data) def test_pkcs7_verify_smime_opaque_signing( self, backend, data, certificate, private_key @@ -1218,7 +1263,7 @@ def test_pkcs7_verify_smime_opaque_signing( ) # Verification - pkcs7.pkcs7_verify_smime(signed, None, certificate, []) + pkcs7.pkcs7_verify_smime(signed) @pytest.mark.parametrize( "signature", @@ -1231,7 +1276,7 @@ def test_pkcs7_verify_smime_wrong_format( self, backend, data, certificate, signature ): with pytest.raises(ValueError): - pkcs7.pkcs7_verify_smime(signature, data, certificate, []) + pkcs7.pkcs7_verify_smime(signature) def _load_rsa_cert_key(): diff --git a/vectors/cryptography_vectors/pkcs7/signed-opaque.msg b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg index e998dfbbf99e..29a805ca186f 100644 --- a/vectors/cryptography_vectors/pkcs7/signed-opaque.msg +++ b/vectors/cryptography_vectors/pkcs7/signed-opaque.msg @@ -3,19 +3,38 @@ Content-Type: application/pkcs7-mime; smime-type=signed-data; name=smime.p7m Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename=smime.p7m -MIIC4AYJKoZIhvcNAQcCoIIC0TCCAs0CAQExDzANBglghkgBZQMEAgEFADAbBgkq -hkiG9w0BBwGgDgQMSGVsbG8gd29ybGQhoIIBVzCCAVMwgfmgAwIBAgICAwkwCgYI -KoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD -QTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEwMTAwMDAwMFowJzELMAkGA1UEBhMC -VVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTBZMBMGByqGSM49AgEGCCqGSM49 -AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N2CxSJE4f4BBGZ7VfFblivTvPDG++ -Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYI -KoZIzj0EAwIDSQAwRgIhAL22GUvxR8zbSxhsrR0TVQ3XMC1iEljgc+qfWb0uguNr -AiEA04Udtg7mGT1DEN/wBMpcWJzDfsgR4lFH1O4Q7/iCr0wxggE9MIIBOQIBATAt -MCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQDDA9jcnlwdG9ncmFwaHkgQ0ECAgMJMA0G -CWCGSAFlAwQCAQUAoIGhMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI -hvcNAQkFMQ8XDTI1MDExMDEwMjQzMFowLwYJKoZIhvcNAQkEMSIEIMBTXkvit5/9 -kykTBUNr+IkxTko/rsBez/y7ffMa2eUaMDYGCSqGSIb3DQEJDzEpMCcwCwYJYIZI -AWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIzj0EAwIERjBE -AiAfusGb8f8j3wWBbvqTqS2WFLZYPDq8lOTQuWvzOsJjKwIgLkYBzQna4RPaFOP4 -ilRirp50A+Qss54wVH9J1InpZjA= \ No newline at end of file +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 diff --git a/vectors/cryptography_vectors/pkcs7/verify_cert.pem b/vectors/cryptography_vectors/pkcs7/verify_cert.pem new file mode 100644 index 000000000000..0c399c067567 --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/verify_cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkqhkiG9w0BAQsFADAN +MQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAxMDUxMDQ4MjhaMCUx +IzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBsMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre +3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCs +BTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6d +Hd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNY +nYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1 +MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABo4IBOTCC +ATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6z3uk4fJeifohSntD +2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29mdC5jb20ucGwvY3Js +MGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDovL2NhLnRyaXNvZnQu +Y29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2NhLnRyaXNvZnQuY29t +LnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5jb20ucGwwHQYDVR0l +BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0/QFDFX/CCMsX356G +ImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQELBQADggEBAL3Iisca +IqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdRtqCCiaCKtJED/f/9 +NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1NC4FfDfnzyG+DZHus +nz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwtuGaWfNz/w324ukcV +pMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+bLnHaoozZwJ/UqBl +9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxaIRp5uc8HnUd5e4lX +br+Ixxcl3WHckkk= +-----END CERTIFICATE----- diff --git a/vectors/cryptography_vectors/pkcs7/verify_key.pem b/vectors/cryptography_vectors/pkcs7/verify_key.pem new file mode 100644 index 000000000000..a58d5ed34fc5 --- /dev/null +++ b/vectors/cryptography_vectors/pkcs7/verify_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre3L8zkuGYVLCK +BEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCsBTL/EmnD3ZMA +JVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6dHd3uVR7NSs98 +uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNYnYhaOBLpUhZc +kyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1MRYtgcsOTxpG +f+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABAoIBAEiVCdiq4HfWmAwA +7rBTZL2k9gfyGhOGmDVSJI8iPiemprCrtg1bjeXCRqNsYoHuYPjI315MpH/CILN5 +WgoB72BfhN+utX+bmf/oHBh3COPe9U40YLNovdBJskgEsDU2fgZ1ykL8dbZ5HJYU +/5lICntHNJ+Pe5CCyDpGVk00zqXwwBDV7hBhbPZxXqdRwdA49yyLIdw/IlMQph9A +zuJ0cyicQ0eFSFb1nCv/11hx3RyhfZvn/V3/F3BIP1gBipc3npldvCXhM4CjNYSe +tilKiqlYt2exD95RR7NdtL16UcRRCOblgGh23qjJOIb8N4dsr8xbeeCN3A69lILo +fgVs2J0CgYEA5noMFh9GFkZFhMIBFPhTlEn+VgWfwK9gWfcyy5GlVsMfp4UA+Alc +JSqz+0y1es2yoF0N4ckFsuZuh0GFZxFg46cE6WL1mO6NyzbND8VItQ3Mb2nsJiDC +xtJCiLqekfXudbmkNkmXleOIW16ZHorkgJADs0LDehGEGJh6lTxOc7MCgYEAy5FG +FGRHGncMyhkoyw6iZC+vmcpvoiu4HfKmTIPQDm6MGS6CxGU6BcX7IgPjdQkogY7s +UUP7lYnlvR2G8u4rOqrEMhjAsbudYSry24iAvcalT5lRYud2dh/8cpamfC9TrrUt +Zd/p8/lvkLTiF7j88QB6onFtm3seagma4hUJl2sCgYAzo8zpeABgJUaWRFGxvSIc +66dM5t2wcpsIDVcYPX3qPrXs9uQMrywyN6sz9zACX+xR+geOO1hHiVHihE+7lC09 +VMLI+B9HMMwcaB7yFaYAyyKvI/CBan25xoqZ0BaPZacUQZAFid+o+d4ner6cFUq1 +c48gryjVRO9wA1oT7fs1+QKBgBBzPOaI8/X/iNkMD2/ZTuYptFcJNNw2DDrfUPD9 +9eI0rL2cNJUKWRX+Wbz183uRseRGWHJ4u+vpqNcPe8hF1th21EP4HBpAvwcLIXT8 +IuszEkjMavdDHR+OlifsZKfEa07C9Vg2MAG3NnzLITopiMcw8rgN0n2uBVcsT4fV +i2DhAoGBAIJtHUe9e8oPrasRlZ3bTFmDT+jNg+7RB8ebG8ZDqAUI3/gnklUd0+rF +nPGI8GEpjwgBxB/zg4/rYz/TEP0E2pd0beWH2vKD31kQVngbz/zhzLHCNLyKDlB4 +vFHpXRHb7ddgTLjHbg6GvY/pRRCqSxWnLgNRW4m+pyLzAx/Hpk1D +-----END RSA PRIVATE KEY----- From 130d6f685afd9bca96d50f77e08401d28d7be682 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:31 +0200 Subject: [PATCH 10/20] fix: adapted docmentation fix: passed into Cow again coverage: added one test case --- .../primitives/asymmetric/serialization.rst | 135 ++++++++++++++---- src/rust/src/pkcs7.rs | 6 +- tests/hazmat/primitives/test_pkcs7.py | 1 + 3 files changed, 109 insertions(+), 33 deletions(-) diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 15aa99bf35d5..ff6c8c31daeb 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1262,6 +1262,63 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, -----END PRIVATE KEY----- """.strip() + verify_cert = b""" + -----BEGIN CERTIFICATE----- + MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkqhkiG9w0BAQsFADAN + MQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAxMDUxMDQ4MjhaMCUx + IzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBsMIIBIjANBgkqhkiG + 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre + 3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCs + BTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6d + Hd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNY + nYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1 + MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABo4IBOTCC + ATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6z3uk4fJeifohSntD + 2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29mdC5jb20ucGwvY3Js + MGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDovL2NhLnRyaXNvZnQu + Y29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2NhLnRyaXNvZnQuY29t + LnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5jb20ucGwwHQYDVR0l + BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0/QFDFX/CCMsX356G + ImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQELBQADggEBAL3Iisca + IqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdRtqCCiaCKtJED/f/9 + NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1NC4FfDfnzyG+DZHus + nz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwtuGaWfNz/w324ukcV + pMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+bLnHaoozZwJ/UqBl + 9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxaIRp5uc8HnUd5e4lX + br+Ixxcl3WHckkk= + -----END CERTIFICATE----- + """.strip() + + verify_key = b""" + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre3L8zkuGYVLCK + BEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCsBTL/EmnD3ZMA + JVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6dHd3uVR7NSs98 + uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNYnYhaOBLpUhZc + kyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1MRYtgcsOTxpG + f+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABAoIBAEiVCdiq4HfWmAwA + 7rBTZL2k9gfyGhOGmDVSJI8iPiemprCrtg1bjeXCRqNsYoHuYPjI315MpH/CILN5 + WgoB72BfhN+utX+bmf/oHBh3COPe9U40YLNovdBJskgEsDU2fgZ1ykL8dbZ5HJYU + /5lICntHNJ+Pe5CCyDpGVk00zqXwwBDV7hBhbPZxXqdRwdA49yyLIdw/IlMQph9A + zuJ0cyicQ0eFSFb1nCv/11hx3RyhfZvn/V3/F3BIP1gBipc3npldvCXhM4CjNYSe + tilKiqlYt2exD95RR7NdtL16UcRRCOblgGh23qjJOIb8N4dsr8xbeeCN3A69lILo + fgVs2J0CgYEA5noMFh9GFkZFhMIBFPhTlEn+VgWfwK9gWfcyy5GlVsMfp4UA+Alc + JSqz+0y1es2yoF0N4ckFsuZuh0GFZxFg46cE6WL1mO6NyzbND8VItQ3Mb2nsJiDC + xtJCiLqekfXudbmkNkmXleOIW16ZHorkgJADs0LDehGEGJh6lTxOc7MCgYEAy5FG + FGRHGncMyhkoyw6iZC+vmcpvoiu4HfKmTIPQDm6MGS6CxGU6BcX7IgPjdQkogY7s + UUP7lYnlvR2G8u4rOqrEMhjAsbudYSry24iAvcalT5lRYud2dh/8cpamfC9TrrUt + Zd/p8/lvkLTiF7j88QB6onFtm3seagma4hUJl2sCgYAzo8zpeABgJUaWRFGxvSIc + 66dM5t2wcpsIDVcYPX3qPrXs9uQMrywyN6sz9zACX+xR+geOO1hHiVHihE+7lC09 + VMLI+B9HMMwcaB7yFaYAyyKvI/CBan25xoqZ0BaPZacUQZAFid+o+d4ner6cFUq1 + c48gryjVRO9wA1oT7fs1+QKBgBBzPOaI8/X/iNkMD2/ZTuYptFcJNNw2DDrfUPD9 + 9eI0rL2cNJUKWRX+Wbz183uRseRGWHJ4u+vpqNcPe8hF1th21EP4HBpAvwcLIXT8 + IuszEkjMavdDHR+OlifsZKfEa07C9Vg2MAG3NnzLITopiMcw8rgN0n2uBVcsT4fV + i2DhAoGBAIJtHUe9e8oPrasRlZ3bTFmDT+jNg+7RB8ebG8ZDqAUI3/gnklUd0+rF + nPGI8GEpjwgBxB/zg4/rYz/TEP0E2pd0beWH2vKD31kQVngbz/zhzLHCNLyKDlB4 + vFHpXRHb7ddgTLjHbg6GvY/pRRCqSxWnLgNRW4m+pyLzAx/Hpk1D + -----END RSA PRIVATE KEY----- + """.strip() + .. class:: PKCS7SignatureBuilder The PKCS7 signature builder can create both basic PKCS7 signed messages as @@ -1340,7 +1397,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :returns bytes: The signed PKCS7 message. -.. function:: pkcs7_verify_der(data, content, certificate, options) +.. function:: pkcs7_verify_der(data, content=None, certificate=None, options=None) .. versionadded:: 45.0.0 @@ -1349,8 +1406,8 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, >>> from cryptography import x509 >>> from cryptography.hazmat.primitives import hashes, serialization >>> from cryptography.hazmat.primitives.serialization import pkcs7 - >>> cert = x509.load_pem_x509_certificate(ca_cert) - >>> key = serialization.load_pem_private_key(ca_key, None) + >>> 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( @@ -1358,7 +1415,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, ... ).sign( ... serialization.Encoding.DER, [] ... ) - >>> pkcs7.pkcs7_verify_der(signed, None, cert, []) + >>> 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 @@ -1368,15 +1425,21 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :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. - :type data: bytes or None + is not specified, the function will look for the content in the signed message. Defaults to + None. + :type content: bytes or None - :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed - message. + :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 :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For - this operation, no options are supported as of now. + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this + operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the + function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, + the function will not verify the certificates in the PKCS#7 message. Defaults to None. + :type options: list[`~cryptography.x509.Certificate`] or None :raises ValueError: If the recipient certificate does not match any of the signers in the PKCS7 data. @@ -1386,7 +1449,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :raises ValueError: If the PKCS7 data is not of the signed data type. -.. function:: pkcs7_verify_pem(data, content, certificate, options) +.. function:: pkcs7_verify_pem(data, content=None, certificate=None, options=None) .. versionadded:: 45.0.0 @@ -1395,8 +1458,8 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, >>> from cryptography import x509 >>> from cryptography.hazmat.primitives import hashes, serialization >>> from cryptography.hazmat.primitives.serialization import pkcs7 - >>> cert = x509.load_pem_x509_certificate(ca_cert) - >>> key = serialization.load_pem_private_key(ca_key, None) + >>> 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( @@ -1404,7 +1467,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, ... ).sign( ... serialization.Encoding.PEM, [] ... ) - >>> pkcs7.pkcs7_verify_pem(signed, None, cert, []) + >>> 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 @@ -1414,15 +1477,21 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :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. - :type data: bytes or None + is not specified, the function will look for the content in the signed message. Defaults to + None. + :type content: bytes or None - :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed - message. + :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 :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For - this operation, no options are supported as of now. + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this + operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the + function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, + the function will not verify the certificates in the PKCS#7 message. Defaults to None. + :type options: list[`~cryptography.x509.Certificate`] or None :raises ValueError: If the PEM data does not have the PKCS7 tag. @@ -1434,7 +1503,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :raises ValueError: If the PKCS7 data is not of the signed data type. -.. function:: pkcs7_verify_smime(data, content, certificate, options) +.. function:: pkcs7_verify_smime(data, content=None, certificate=None, options=None) .. versionadded:: 45.0.0 @@ -1443,8 +1512,8 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, >>> from cryptography import x509 >>> from cryptography.hazmat.primitives import hashes, serialization >>> from cryptography.hazmat.primitives.serialization import pkcs7 - >>> cert = x509.load_pem_x509_certificate(ca_cert) - >>> key = serialization.load_pem_private_key(ca_key, None) + >>> 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( @@ -1452,7 +1521,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, ... ).sign( ... serialization.Encoding.SMIME, [] ... ) - >>> pkcs7.pkcs7_verify_smime(signed, None, cert, []) + >>> 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 @@ -1465,15 +1534,21 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :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. - :type data: bytes or None + signature. Defaults to None. + :type content: bytes or None - :param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed - message. + :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 :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For - this operation, no options are supported as of now. + :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this + operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the + function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, + the function will not verify the certificates in the PKCS#7 message. Defaults to None. + :type options: list[`~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``. diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 315ca934f754..54bcc13a1fa9 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -744,10 +744,10 @@ fn verify_der<'p>( ) -> CryptographyResult<()> { // Check the verify options let options = match options { - Some(options) => options, - None => &pyo3::types::PyList::empty(py), + Some(options) => Cow::Borrowed(options), + None => Cow::Owned(pyo3::types::PyList::empty(py)), }; - check_verify_options(py, options)?; + check_verify_options(py, &options)?; // Verify the data let content_info = asn1::parse_single::>(signature)?; diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index bb30153297c6..acd3c7362ff7 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1045,6 +1045,7 @@ def test_not_a_cert(self, backend): "invalid_options", [ [b"invalid"], + [pkcs7.PKCS7Options.Binary], ], ) def test_pkcs7_verify_invalid_options(self, backend, invalid_options): From e1d5f17ded8f7ae41c2793266190386ce43a14d3 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:31 +0200 Subject: [PATCH 11/20] one more test case --- tests/hazmat/primitives/test_pkcs7.py | 12 ++++++++++++ .../pkcs7/signature-empty-certs.der | Bin 0 -> 574 bytes 2 files changed, 12 insertions(+) create mode 100644 vectors/cryptography_vectors/pkcs7/signature-empty-certs.der diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index acd3c7362ff7..c33e36784256 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1109,6 +1109,18 @@ def test_pkcs7_verify_der_with_certificate( # 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 ): 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 From bda9e464cec43524985af2823d312b44ac227722 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:31 +0200 Subject: [PATCH 12/20] changing the error message for clarity --- src/rust/src/pkcs7.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 54bcc13a1fa9..521ec032bbf0 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -775,7 +775,7 @@ fn verify_der<'p>( None => { return Err(CryptographyError::from( pyo3::exceptions::PyValueError::new_err( - "The PKCS7 data does not contain any certificate.", + "The PKCS7 data has an empty certificates attributes.", ), )); } From fe3f7bd1a5948775ca1099fc2cbbe95e94221424 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:03:31 +0200 Subject: [PATCH 13/20] two more test cases handled --- src/rust/src/pkcs7.rs | 7 +++--- tests/hazmat/primitives/test_pkcs7.py | 31 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 521ec032bbf0..7156c592c5ef 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -848,9 +848,10 @@ fn verify_der<'p>( }, _ => { return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "Unsupported hash algorithm with RSA.", - ), + exceptions::UnsupportedAlgorithm::new_err(( + "Only SHA-256 is currently supported for content verification with RSA.", + exceptions::Reasons::UNSUPPORTED_SERIALIZATION, + )), )) } }, diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index c33e36784256..668aa843ccb8 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1174,6 +1174,22 @@ def test_pkcs7_verify_der_no_content( 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 + options = [pkcs7.PKCS7Options.NoVerify] + pkcs7.pkcs7_verify_der(signature, options=options) + def test_pkcs7_verify_invalid_signature( self, backend, data, certificate, private_key ): @@ -1209,6 +1225,21 @@ def test_pkcs7_verify_der_wrong_certificate( with pytest.raises(ValueError): pkcs7.pkcs7_verify_der(signature, certificate=rsa_certificate) + def test_pkcs7_verify_der_unsupported_digest_algorithm( + self, backend, data, certificate, private_key + ): + # Signature + builder = ( + pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer(certificate, private_key, hashes.SHA384()) + ) + signature = builder.sign(serialization.Encoding.DER, []) + + # Verification with another certificate + with pytest.raises(exceptions.UnsupportedAlgorithm): + pkcs7.pkcs7_verify_der(signature) + def test_pkcs7_verify_pem(self, backend, data, certificate, private_key): # Signature builder = ( From 2611d24076ae63cc63784e0b385a63471ae2fee1 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:04:06 +0200 Subject: [PATCH 14/20] loading the load_der func for all backends --- src/rust/src/pkcs7.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 7156c592c5ef..f709df615d45 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}; From 003f070a5e98ea085f34624efd2a2ab0c7e2279f Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:04:06 +0200 Subject: [PATCH 15/20] removed options for now --- .../primitives/asymmetric/serialization.rst | 27 --- .../hazmat/bindings/_rust/pkcs7.pyi | 3 - .../hazmat/primitives/serialization/pkcs7.py | 2 - src/rust/src/pkcs7.rs | 198 +++++++----------- src/rust/src/types.rs | 8 - tests/hazmat/primitives/test_pkcs7.py | 37 +--- 6 files changed, 78 insertions(+), 197 deletions(-) diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index ff6c8c31daeb..b44c9024163e 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1433,13 +1433,6 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, 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 - - :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this - operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the - function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, - the function will not verify the certificates in the PKCS#7 message. Defaults to None. - :type options: list[`~cryptography.x509.Certificate`] or None :raises ValueError: If the recipient certificate does not match any of the signers in the PKCS7 data. @@ -1486,13 +1479,6 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, message. Defaults to None. :type certificate: :class:`~cryptography.x509.Certificate` or None - :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this - operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the - function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, - the function will not verify the certificates in the PKCS#7 message. Defaults to None. - :type options: list[`~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 @@ -1542,14 +1528,6 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, message. Defaults to None. :type certificate: :class:`~cryptography.x509.Certificate` or None - :param options: A list of - :class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For this - operation, the `NoSigs` and `NoVerify` options are supported. If `NoSigs` is specified, the - function will not verify the signature in the PKCS#7 message. If `NoVerify` is specified, - the function will not verify the certificates in the PKCS#7 message. Defaults to None. - :type options: list[`~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``. @@ -1856,11 +1834,6 @@ 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). - .. attribute:: NoVerify - - For S/MIME verification only. Don't verify signers certificate. This is - useful when the signer's certificate is not available or when the signer's - certificate is not trusted. Serialization Formats ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi index 5391b4c9d4f4..48169dd094d1 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi @@ -46,19 +46,16 @@ def verify_der( signature: bytes, content: bytes | None = None, certificate: x509.Certificate | None = None, - options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def verify_pem( signature: bytes, content: bytes | None = None, certificate: x509.Certificate | None = None, - options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def verify_smime( signature: bytes, content: bytes | None = None, certificate: x509.Certificate | None = None, - options: Iterable[pkcs7.PKCS7Options] | None = None, ) -> None: ... def load_pem_pkcs7_certificates( data: bytes, diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 2e9b2912ca46..db679178b9c4 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -58,8 +58,6 @@ class PKCS7Options(utils.Enum): NoCapabilities = "Don't embed SMIME capabilities" NoAttributes = "Don't embed authenticatedAttributes" NoCerts = "Don't embed signer certificate" - NoVerify = "Don't verify signers certificate" - NoSigs = "Don't verify signature" def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]: diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index f709df615d45..2fdd434a4dc4 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -673,13 +673,12 @@ fn compute_pkcs7_signature_algorithm<'p>( } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content = None, certificate = None, options = None))] +#[pyo3(signature = (signature, content = None, certificate = None))] fn verify_smime<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, certificate: Option>, - options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { // Parse the email let py_content_and_signature = types::SMIME_SIGNED_DECODE.get(py)?.call1((signature,))?; @@ -703,17 +702,16 @@ fn verify_smime<'p>( } }; - verify_der(py, signature, content, certificate, options) + verify_der(py, signature, content, certificate) } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content = None, certificate = None, options = None))] +#[pyo3(signature = (signature, content = None, certificate = None))] fn verify_pem<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, certificate: Option>, - options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { let pem_str = std::str::from_utf8(signature) .map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid PEM data"))?; @@ -729,25 +727,17 @@ fn verify_pem<'p>( )); } - verify_der(py, &pem.into_contents(), content, certificate, options) + verify_der(py, &pem.into_contents(), content, certificate) } #[pyo3::pyfunction] -#[pyo3(signature = (signature, content = None, certificate = None, options = None))] +#[pyo3(signature = (signature, content = None, certificate = None))] fn verify_der<'p>( py: pyo3::Python<'p>, signature: &[u8], content: Option<&[u8]>, certificate: Option>, - options: Option<&pyo3::Bound<'p, pyo3::types::PyList>>, ) -> CryptographyResult<()> { - // Check the verify options - let options = match options { - Some(options) => Cow::Borrowed(options), - None => Cow::Owned(pyo3::types::PyList::empty(py)), - }; - check_verify_options(py, &options)?; - // Verify the data let content_info = asn1::parse_single::>(signature)?; match content_info.content { @@ -793,88 +783,84 @@ fn verify_der<'p>( // Get recipients, and find the one matching with the signer infos (if any) // TODO: what to do for multiple certificates? - if !options.contains(types::PKCS7_NO_SIGS.get(py)?)? { - 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 - }); + 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(())), - }, + // 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( - exceptions::UnsupportedAlgorithm::new_err(( - "Only SHA-256 is currently supported for content verification with RSA.", - exceptions::Reasons::UNSUPPORTED_SERIALIZATION, - )), - )) + pyo3::exceptions::PyValueError::new_err( + "No content stored in the signature or provided.", + ), + )); } }, - _ => 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, - )?; - } + }, + }; + + // 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 - if !options.contains(types::PKCS7_NO_VERIFY.get(py)?)? { - let certificates = pyo3::types::PyList::empty(py); - certificates.append(certificate)?; - types::VERIFY_PKCS7_CERTIFICATES - .get(py)? - .call1((certificates,))?; - } + let certificates = pyo3::types::PyList::empty(py); + certificates.append(certificate)?; + types::VERIFY_PKCS7_CERTIFICATES + .get(py)? + .call1((certificates,))?; } _ => { return Err(CryptographyError::from( @@ -888,38 +874,6 @@ fn verify_der<'p>( Ok(()) } -fn check_verify_options<'p>( - py: pyo3::Python<'p>, - options: &pyo3::Bound<'p, pyo3::types::PyList>, -) -> Result<(), CryptographyError> { - // Check if all options are from the PKCS7Options enum - let pkcs7_options = types::PKCS7_OPTIONS.get(py)?; - for opt in options.iter() { - if !opt.is_instance(&pkcs7_options)? { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "options must be from the PKCS7Options enum", - ), - )); - } - } - - // Check if any option is not PKCS7Options::NoVerify - let no_verify_option = types::PKCS7_NO_VERIFY.get(py)?; - let no_sigs_option = types::PKCS7_NO_SIGS.get(py)?; - for opt in options.iter() { - if opt.ne(no_verify_option.clone())? & opt.ne(no_sigs_option.clone())? { - return Err(CryptographyError::from( - pyo3::exceptions::PyValueError::new_err( - "Only the following options are supported for verification: NoVerify, NoSigs", - ), - )); - } - } - - 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![]; diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index 53e268147a33..c06410d5deb4 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -353,14 +353,6 @@ pub static PKCS7_DETACHED_SIGNATURE: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", &["PKCS7Options", "DetachedSignature"], ); -pub static PKCS7_NO_VERIFY: LazyPyImport = LazyPyImport::new( - "cryptography.hazmat.primitives.serialization.pkcs7", - &["PKCS7Options", "NoVerify"], -); -pub static PKCS7_NO_SIGS: LazyPyImport = LazyPyImport::new( - "cryptography.hazmat.primitives.serialization.pkcs7", - &["PKCS7Options", "NoSigs"], -); pub static SMIME_ENVELOPED_ENCODE: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.serialization.pkcs7", diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index 668aa843ccb8..acb09520b21b 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1041,17 +1041,6 @@ 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( - "invalid_options", - [ - [b"invalid"], - [pkcs7.PKCS7Options.Binary], - ], - ) - def test_pkcs7_verify_invalid_options(self, backend, invalid_options): - with pytest.raises(ValueError): - pkcs7.pkcs7_verify_der(b"", options=invalid_options) - @pytest.mark.parametrize( "signing_options", [ @@ -1073,27 +1062,6 @@ def test_pkcs7_verify_der( # Verification pkcs7.pkcs7_verify_der(signature) - @pytest.mark.parametrize( - "options", - [ - [pkcs7.PKCS7Options.NoVerify], - [pkcs7.PKCS7Options.NoSigs], - ], - ) - def test_pkcs7_verify_der_with_options( - self, backend, data, certificate, private_key, options - ): - # Signature - builder = ( - pkcs7.PKCS7SignatureBuilder() - .set_data(data) - .add_signer(certificate, private_key, hashes.SHA256()) - ) - signature = builder.sign(serialization.Encoding.DER, []) - - # Verification - pkcs7.pkcs7_verify_der(signature, options=options) - def test_pkcs7_verify_der_with_certificate( self, backend, data, certificate, private_key ): @@ -1187,8 +1155,7 @@ def test_pkcs7_verify_der_ecdsa_certificate(self, backend, data): signature = builder.sign(serialization.Encoding.DER, []) # Verification with another certificate - options = [pkcs7.PKCS7Options.NoVerify] - pkcs7.pkcs7_verify_der(signature, options=options) + pkcs7.pkcs7_verify_der(signature) def test_pkcs7_verify_invalid_signature( self, backend, data, certificate, private_key @@ -1250,7 +1217,7 @@ def test_pkcs7_verify_pem(self, backend, data, certificate, private_key): signature = builder.sign(serialization.Encoding.PEM, []) # Verification - pkcs7.pkcs7_verify_pem(signature, data, certificate, []) + pkcs7.pkcs7_verify_pem(signature, data, certificate) def test_pkcs7_verify_pem_with_wrong_tag(self, backend, data, certificate): with pytest.raises(ValueError): From 3d302fda254b08d23d95e2445547cfef5eefa316 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:04:32 +0200 Subject: [PATCH 16/20] first draft of smime extension policy --- .../hazmat/primitives/serialization/pkcs7.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index db679178b9c4..db29f0ccf7f7 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -511,8 +511,54 @@ def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]: raise ValueError("Not an S/MIME signed message") +def get_smime_x509_extension_policies() -> tuple[ + ExtensionPolicy, ExtensionPolicy +]: + """ + Gets the default X.509 extension policy for S/MIME. Some specifications + that differ from the standard ones: + - Certificates used as end entities (i.e., the cert used to sign + a PKCS#7/SMIME message) should not have ca=true in their basic + constraints extension. + - EKU_CLIENT_AUTH_OID is not required + - EKU_EMAIL_PROTECTION_OID is required + """ + + # CA policy + def _validate_ca( + policy: Policy, cert: Certificate, bc: x509.BasicConstraints + ): + assert not bc.ca + + ca_policy = ExtensionPolicy.permit_all().require_present( + x509.BasicConstraints, + Criticality.AGNOSTIC, + _validate_ca, + ) + + # EE policy + def _validate_eku( + policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage + ): + # Checking for EKU_EMAIL_PROTECTION_OID + assert x509.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku # type: ignore[attr-defined] + + ee_policy = ExtensionPolicy.permit_all().require_present( + x509.ExtendedKeyUsage, + Criticality.AGNOSTIC, + _validate_eku, + ) + + return ca_policy, ee_policy + + def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None: - builder = PolicyBuilder().store(Store(certificates)) + builder = ( + PolicyBuilder() + .store(Store(certificates)) + .extension_policies(*get_smime_x509_extension_policies()) + ) + verifier = builder.build_client_verifier() verifier.verify(certificates[0], certificates[1:]) From 412c614fb42c220ab087e8579f290d053dfb8256 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:05:41 +0200 Subject: [PATCH 17/20] added back changelog --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e1118db8dc3..3658ac5855d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,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-4: From 8a86f96fbf085e066d1e7bfc6f2cf2dc094068aa Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:16:31 +0200 Subject: [PATCH 18/20] integrated built-in verifier --- .../hazmat/primitives/serialization/pkcs7.py | 50 +++---------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index db29f0ccf7f7..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 @@ -511,55 +513,15 @@ def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]: raise ValueError("Not an S/MIME signed message") -def get_smime_x509_extension_policies() -> tuple[ - ExtensionPolicy, ExtensionPolicy -]: - """ - Gets the default X.509 extension policy for S/MIME. Some specifications - that differ from the standard ones: - - Certificates used as end entities (i.e., the cert used to sign - a PKCS#7/SMIME message) should not have ca=true in their basic - constraints extension. - - EKU_CLIENT_AUTH_OID is not required - - EKU_EMAIL_PROTECTION_OID is required - """ - - # CA policy - def _validate_ca( - policy: Policy, cert: Certificate, bc: x509.BasicConstraints - ): - assert not bc.ca - - ca_policy = ExtensionPolicy.permit_all().require_present( - x509.BasicConstraints, - Criticality.AGNOSTIC, - _validate_ca, - ) - - # EE policy - def _validate_eku( - policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage - ): - # Checking for EKU_EMAIL_PROTECTION_OID - assert x509.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku # type: ignore[attr-defined] - - ee_policy = ExtensionPolicy.permit_all().require_present( - x509.ExtendedKeyUsage, - Criticality.AGNOSTIC, - _validate_eku, - ) - - return ca_policy, ee_policy - - def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None: - builder = ( + ca_policy, ee_policy = pkcs7_x509_extension_policies() + verifier = ( PolicyBuilder() .store(Store(certificates)) - .extension_policies(*get_smime_x509_extension_policies()) + .extension_policies(ca_policy=ca_policy, ee_policy=ee_policy) + .build_client_verifier() ) - verifier = builder.build_client_verifier() verifier.verify(certificates[0], certificates[1:]) From d837fc51139196989f322829c9e21cf97c6249c8 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 18:55:20 +0200 Subject: [PATCH 19/20] using existing vectors :) --- tests/hazmat/primitives/test_pkcs7.py | 22 +++++++-------- .../pkcs7/verify_cert.pem | 24 ----------------- .../cryptography_vectors/pkcs7/verify_key.pem | 27 ------------------- 3 files changed, 10 insertions(+), 63 deletions(-) delete mode 100644 vectors/cryptography_vectors/pkcs7/verify_cert.pem delete mode 100644 vectors/cryptography_vectors/pkcs7/verify_key.pem diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index acb09520b21b..436c1c34d513 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -1020,7 +1020,7 @@ def fixture_data(self, backend) -> bytes: @pytest.fixture(name="certificate") def fixture_certificate(self, backend) -> x509.Certificate: return load_vectors_from_file( - os.path.join("pkcs7", "verify_cert.pem"), + os.path.join("pkcs7", "ca.pem"), loader=lambda pemfile: x509.load_pem_x509_certificate( pemfile.read() ), @@ -1030,7 +1030,7 @@ def fixture_certificate(self, backend) -> x509.Certificate: @pytest.fixture(name="private_key") def fixture_private_key(self, backend) -> types.PrivateKeyTypes: return load_vectors_from_file( - os.path.join("pkcs7", "verify_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 ), @@ -1192,10 +1192,12 @@ def test_pkcs7_verify_der_wrong_certificate( with pytest.raises(ValueError): pkcs7.pkcs7_verify_der(signature, certificate=rsa_certificate) - def test_pkcs7_verify_der_unsupported_digest_algorithm( - self, backend, data, certificate, private_key + def test_pkcs7_verify_der_unsupported_rsa_digest_algorithm( + self, backend, data ): - # Signature + certificate, private_key = _load_rsa_cert_key() + + # Signature with an unsupported digest algorithm builder = ( pkcs7.PKCS7SignatureBuilder() .set_data(data) @@ -1203,7 +1205,7 @@ def test_pkcs7_verify_der_unsupported_digest_algorithm( ) signature = builder.sign(serialization.Encoding.DER, []) - # Verification with another certificate + # Verification with pytest.raises(exceptions.UnsupportedAlgorithm): pkcs7.pkcs7_verify_der(signature) @@ -1263,9 +1265,7 @@ def test_pkcs7_verify_smime_with_content( # Verification pkcs7.pkcs7_verify_smime(signed, content=data) - def test_pkcs7_verify_smime_opaque_signing( - self, backend, data, certificate, private_key - ): + def test_pkcs7_verify_smime_opaque_signing(self, backend): # Signature signed = load_vectors_from_file( os.path.join("pkcs7", "signed-opaque.msg"), @@ -1283,9 +1283,7 @@ def test_pkcs7_verify_smime_opaque_signing( b"Content-Type: multipart/signed;\nHello world!", ], ) - def test_pkcs7_verify_smime_wrong_format( - self, backend, data, certificate, signature - ): + def test_pkcs7_verify_smime_wrong_format(self, backend, signature): with pytest.raises(ValueError): pkcs7.pkcs7_verify_smime(signature) diff --git a/vectors/cryptography_vectors/pkcs7/verify_cert.pem b/vectors/cryptography_vectors/pkcs7/verify_cert.pem deleted file mode 100644 index 0c399c067567..000000000000 --- a/vectors/cryptography_vectors/pkcs7/verify_cert.pem +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkqhkiG9w0BAQsFADAN -MQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAxMDUxMDQ4MjhaMCUx -IzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBsMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre -3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCs -BTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6d -Hd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNY -nYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1 -MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABo4IBOTCC -ATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6z3uk4fJeifohSntD -2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29mdC5jb20ucGwvY3Js -MGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDovL2NhLnRyaXNvZnQu -Y29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2NhLnRyaXNvZnQuY29t -LnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5jb20ucGwwHQYDVR0l -BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0/QFDFX/CCMsX356G -ImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQELBQADggEBAL3Iisca -IqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdRtqCCiaCKtJED/f/9 -NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1NC4FfDfnzyG+DZHus -nz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwtuGaWfNz/w324ukcV -pMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+bLnHaoozZwJ/UqBl -9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxaIRp5uc8HnUd5e4lX -br+Ixxcl3WHckkk= ------END CERTIFICATE----- diff --git a/vectors/cryptography_vectors/pkcs7/verify_key.pem b/vectors/cryptography_vectors/pkcs7/verify_key.pem deleted file mode 100644 index a58d5ed34fc5..000000000000 --- a/vectors/cryptography_vectors/pkcs7/verify_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre3L8zkuGYVLCK -BEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCsBTL/EmnD3ZMA -JVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6dHd3uVR7NSs98 -uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNYnYhaOBLpUhZc -kyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1MRYtgcsOTxpG -f+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABAoIBAEiVCdiq4HfWmAwA -7rBTZL2k9gfyGhOGmDVSJI8iPiemprCrtg1bjeXCRqNsYoHuYPjI315MpH/CILN5 -WgoB72BfhN+utX+bmf/oHBh3COPe9U40YLNovdBJskgEsDU2fgZ1ykL8dbZ5HJYU -/5lICntHNJ+Pe5CCyDpGVk00zqXwwBDV7hBhbPZxXqdRwdA49yyLIdw/IlMQph9A -zuJ0cyicQ0eFSFb1nCv/11hx3RyhfZvn/V3/F3BIP1gBipc3npldvCXhM4CjNYSe -tilKiqlYt2exD95RR7NdtL16UcRRCOblgGh23qjJOIb8N4dsr8xbeeCN3A69lILo -fgVs2J0CgYEA5noMFh9GFkZFhMIBFPhTlEn+VgWfwK9gWfcyy5GlVsMfp4UA+Alc -JSqz+0y1es2yoF0N4ckFsuZuh0GFZxFg46cE6WL1mO6NyzbND8VItQ3Mb2nsJiDC -xtJCiLqekfXudbmkNkmXleOIW16ZHorkgJADs0LDehGEGJh6lTxOc7MCgYEAy5FG -FGRHGncMyhkoyw6iZC+vmcpvoiu4HfKmTIPQDm6MGS6CxGU6BcX7IgPjdQkogY7s -UUP7lYnlvR2G8u4rOqrEMhjAsbudYSry24iAvcalT5lRYud2dh/8cpamfC9TrrUt -Zd/p8/lvkLTiF7j88QB6onFtm3seagma4hUJl2sCgYAzo8zpeABgJUaWRFGxvSIc -66dM5t2wcpsIDVcYPX3qPrXs9uQMrywyN6sz9zACX+xR+geOO1hHiVHihE+7lC09 -VMLI+B9HMMwcaB7yFaYAyyKvI/CBan25xoqZ0BaPZacUQZAFid+o+d4ner6cFUq1 -c48gryjVRO9wA1oT7fs1+QKBgBBzPOaI8/X/iNkMD2/ZTuYptFcJNNw2DDrfUPD9 -9eI0rL2cNJUKWRX+Wbz183uRseRGWHJ4u+vpqNcPe8hF1th21EP4HBpAvwcLIXT8 -IuszEkjMavdDHR+OlifsZKfEa07C9Vg2MAG3NnzLITopiMcw8rgN0n2uBVcsT4fV -i2DhAoGBAIJtHUe9e8oPrasRlZ3bTFmDT+jNg+7RB8ebG8ZDqAUI3/gnklUd0+rF -nPGI8GEpjwgBxB/zg4/rYz/TEP0E2pd0beWH2vKD31kQVngbz/zhzLHCNLyKDlB4 -vFHpXRHb7ddgTLjHbg6GvY/pRRCqSxWnLgNRW4m+pyLzAx/Hpk1D ------END RSA PRIVATE KEY----- From 8975aa980606e773b87dc1363406cdd128d19fd6 Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:09:22 +0200 Subject: [PATCH 20/20] minor doc modification --- .../primitives/asymmetric/serialization.rst | 63 +++++-------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index b44c9024163e..1c879069124f 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1264,59 +1264,24 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, verify_cert = b""" -----BEGIN CERTIFICATE----- - MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkqhkiG9w0BAQsFADAN - MQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAxMDUxMDQ4MjhaMCUx - IzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBsMIIBIjANBgkqhkiG - 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre - 3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCs - BTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6d - Hd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNY - nYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1 - MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABo4IBOTCC - ATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6z3uk4fJeifohSntD - 2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29mdC5jb20ucGwvY3Js - MGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDovL2NhLnRyaXNvZnQu - Y29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2NhLnRyaXNvZnQuY29t - LnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5jb20ucGwwHQYDVR0l - BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0/QFDFX/CCMsX356G - ImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQELBQADggEBAL3Iisca - IqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdRtqCCiaCKtJED/f/9 - NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1NC4FfDfnzyG+DZHus - nz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwtuGaWfNz/w324ukcV - pMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+bLnHaoozZwJ/UqBl - 9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxaIRp5uc8HnUd5e4lX - br+Ixxcl3WHckkk= + 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 RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre3L8zkuGYVLCK - BEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCsBTL/EmnD3ZMA - JVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6dHd3uVR7NSs98 - uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNYnYhaOBLpUhZc - kyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1MRYtgcsOTxpG - f+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABAoIBAEiVCdiq4HfWmAwA - 7rBTZL2k9gfyGhOGmDVSJI8iPiemprCrtg1bjeXCRqNsYoHuYPjI315MpH/CILN5 - WgoB72BfhN+utX+bmf/oHBh3COPe9U40YLNovdBJskgEsDU2fgZ1ykL8dbZ5HJYU - /5lICntHNJ+Pe5CCyDpGVk00zqXwwBDV7hBhbPZxXqdRwdA49yyLIdw/IlMQph9A - zuJ0cyicQ0eFSFb1nCv/11hx3RyhfZvn/V3/F3BIP1gBipc3npldvCXhM4CjNYSe - tilKiqlYt2exD95RR7NdtL16UcRRCOblgGh23qjJOIb8N4dsr8xbeeCN3A69lILo - fgVs2J0CgYEA5noMFh9GFkZFhMIBFPhTlEn+VgWfwK9gWfcyy5GlVsMfp4UA+Alc - JSqz+0y1es2yoF0N4ckFsuZuh0GFZxFg46cE6WL1mO6NyzbND8VItQ3Mb2nsJiDC - xtJCiLqekfXudbmkNkmXleOIW16ZHorkgJADs0LDehGEGJh6lTxOc7MCgYEAy5FG - FGRHGncMyhkoyw6iZC+vmcpvoiu4HfKmTIPQDm6MGS6CxGU6BcX7IgPjdQkogY7s - UUP7lYnlvR2G8u4rOqrEMhjAsbudYSry24iAvcalT5lRYud2dh/8cpamfC9TrrUt - Zd/p8/lvkLTiF7j88QB6onFtm3seagma4hUJl2sCgYAzo8zpeABgJUaWRFGxvSIc - 66dM5t2wcpsIDVcYPX3qPrXs9uQMrywyN6sz9zACX+xR+geOO1hHiVHihE+7lC09 - VMLI+B9HMMwcaB7yFaYAyyKvI/CBan25xoqZ0BaPZacUQZAFid+o+d4ner6cFUq1 - c48gryjVRO9wA1oT7fs1+QKBgBBzPOaI8/X/iNkMD2/ZTuYptFcJNNw2DDrfUPD9 - 9eI0rL2cNJUKWRX+Wbz183uRseRGWHJ4u+vpqNcPe8hF1th21EP4HBpAvwcLIXT8 - IuszEkjMavdDHR+OlifsZKfEa07C9Vg2MAG3NnzLITopiMcw8rgN0n2uBVcsT4fV - i2DhAoGBAIJtHUe9e8oPrasRlZ3bTFmDT+jNg+7RB8ebG8ZDqAUI3/gnklUd0+rF - nPGI8GEpjwgBxB/zg4/rYz/TEP0E2pd0beWH2vKD31kQVngbz/zhzLHCNLyKDlB4 - vFHpXRHb7ddgTLjHbg6GvY/pRRCqSxWnLgNRW4m+pyLzAx/Hpk1D - -----END RSA PRIVATE KEY----- + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe + jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs + UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF + -----END PRIVATE KEY----- """.strip() .. class:: PKCS7SignatureBuilder