From 6143683d8720ded085fad699e2f075edb91177fb Mon Sep 17 00:00:00 2001 From: Quentin Retourne <32574188+nitneuqr@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:02:20 +0100 Subject: [PATCH] PKCS7: added encryption with AES-256-CBC (#12172) * feat: added encryption with AES-256-CBC added & updated tests accordingly updated documentation removed useless test vector * fixing coverage * last python coverage fix * restraining the number of classes changed name to content_encryption_algorithm simplified the rust code accordingly tried to simplify the documentation * python 3.8 artefacts * passed content encryption algo locally adapted rust code accordingly --- CHANGELOG.rst | 3 +- docs/development/test-vectors.rst | 3 -- .../primitives/asymmetric/serialization.rst | 11 +++++ .../hazmat/bindings/_rust/pkcs7.pyi | 1 + .../hazmat/primitives/serialization/pkcs7.py | 45 ++++++++++++++++++- src/rust/src/pkcs7.rs | 30 ++++++++++--- tests/hazmat/primitives/test_pkcs7.py | 28 +++++++++--- .../pkcs7/enveloped-aes-256-cbc.pem | 16 ------- 8 files changed, 104 insertions(+), 33 deletions(-) delete mode 100644 vectors/cryptography_vectors/pkcs7/enveloped-aes-256-cbc.pem diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 61ba3008d..fdb2d5ffb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,7 +10,8 @@ Changelog * Support for Python 3.7 is deprecated and will be removed in the next ``cryptography`` release. - +* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm, + in addition to AES-128. .. _v44-0-0: diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index a35f70c47..cb72181da 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -878,9 +878,6 @@ Custom PKCS7 Test Vectors * ``pkcs7/amazon-roots.der`` - A DER encoded PCKS7 file containing Amazon Root CA 2 and 3 generated by OpenSSL. * ``pkcs7/enveloped.pem`` - A PEM encoded PKCS7 file with enveloped data. -* ``pkcs7/enveloped-aes-256-cbc.pem`` - A PEM encoded PKCS7 file with - enveloped data, with content encrypted using AES-256-CBC, under the public - key of ``x509/custom/ca/rsa_ca.pem``. * ``pkcs7/enveloped-triple-des.pem`` - A PEM encoded PKCS7 file with enveloped data, with content encrypted using DES EDE3 CBC (also called Triple DES), under the public key of ``x509/custom/ca/rsa_ca.pem``. diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index f2ce1be06..4e86bd608 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -1268,10 +1268,13 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, >>> from cryptography import x509 >>> from cryptography.hazmat.primitives import serialization >>> from cryptography.hazmat.primitives.serialization import pkcs7 + >>> from cryptography.hazmat.primitives.ciphers import algorithms >>> cert = x509.load_pem_x509_certificate(ca_cert_rsa) >>> options = [pkcs7.PKCS7Options.Text] >>> pkcs7.PKCS7EnvelopeBuilder().set_data( ... b"data to encrypt" + ... ).set_content_encryption_algorithm( + ... algorithms.AES128 ... ).add_recipient( ... cert ... ).encrypt( @@ -1284,6 +1287,14 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``, :param data: The data to be encrypted. :type data: :term:`bytes-like` + .. method:: set_content_encryption_algorithm(content_encryption_algorithm) + + :param content_encryption_algorithm: the content encryption algorithm to use. + Only AES is supported, with a key size of 128 or 256 bits. + :type content_encryption_algorithm: + :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES128` + or :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES256` + .. method:: add_recipient(certificate) Add a recipient for the message. Recipients will be able to use their private keys diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi index f9aa81ea0..051e90bad 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs7.pyi @@ -15,6 +15,7 @@ def serialize_certificates( ) -> bytes: ... def encrypt_and_serialize( builder: pkcs7.PKCS7EnvelopeBuilder, + content_encryption_algorithm: pkcs7.ContentEncryptionAlgorithm, encoding: serialization.Encoding, options: typing.Iterable[pkcs7.PKCS7Options], ) -> bytes: ... diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs7.py b/src/cryptography/hazmat/primitives/serialization/pkcs7.py index 882e345f2..c3818724c 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs7.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs7.py @@ -16,6 +16,9 @@ from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.bindings._rust import pkcs7 as rust_pkcs7 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa +from cryptography.hazmat.primitives.ciphers import ( + algorithms, +) from cryptography.utils import _check_byteslike load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates @@ -35,6 +38,10 @@ PKCS7PrivateKeyTypes = typing.Union[ rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey ] +ContentEncryptionAlgorithm = typing.Union[ + typing.Type[algorithms.AES128], typing.Type[algorithms.AES256] +] + class PKCS7Options(utils.Enum): Text = "Add text/plain MIME type" @@ -184,6 +191,8 @@ class PKCS7EnvelopeBuilder: *, _data: bytes | None = None, _recipients: list[x509.Certificate] | None = None, + _content_encryption_algorithm: ContentEncryptionAlgorithm + | None = None, ): from cryptography.hazmat.backends.openssl.backend import ( backend as ossl, @@ -197,13 +206,18 @@ class PKCS7EnvelopeBuilder: ) self._data = _data self._recipients = _recipients if _recipients is not None else [] + self._content_encryption_algorithm = _content_encryption_algorithm def set_data(self, data: bytes) -> PKCS7EnvelopeBuilder: _check_byteslike("data", data) if self._data is not None: raise ValueError("data may only be set once") - return PKCS7EnvelopeBuilder(_data=data, _recipients=self._recipients) + return PKCS7EnvelopeBuilder( + _data=data, + _recipients=self._recipients, + _content_encryption_algorithm=self._content_encryption_algorithm, + ) def add_recipient( self, @@ -221,6 +235,24 @@ class PKCS7EnvelopeBuilder: *self._recipients, certificate, ], + _content_encryption_algorithm=self._content_encryption_algorithm, + ) + + def set_content_encryption_algorithm( + self, content_encryption_algorithm: ContentEncryptionAlgorithm + ) -> PKCS7EnvelopeBuilder: + if self._content_encryption_algorithm is not None: + raise ValueError("Content encryption algo may only be set once") + if content_encryption_algorithm not in { + algorithms.AES128, + algorithms.AES256, + }: + raise TypeError("Only AES128 and AES256 are supported") + + return PKCS7EnvelopeBuilder( + _data=self._data, + _recipients=self._recipients, + _content_encryption_algorithm=content_encryption_algorithm, ) def encrypt( @@ -232,6 +264,13 @@ class PKCS7EnvelopeBuilder: raise ValueError("Must have at least one recipient") if self._data is None: raise ValueError("You must add data to encrypt") + + # The default content encryption algorithm is AES-128, which the S/MIME + # v3.2 RFC specifies as MUST support (https://datatracker.ietf.org/doc/html/rfc5751#section-2.7) + content_encryption_algorithm = ( + self._content_encryption_algorithm or algorithms.AES128 + ) + options = list(options) if not all(isinstance(x, PKCS7Options) for x in options): raise ValueError("options must be from the PKCS7Options enum") @@ -260,7 +299,9 @@ class PKCS7EnvelopeBuilder: "Cannot use Binary and Text options at the same time" ) - return rust_pkcs7.encrypt_and_serialize(self, encoding, options) + return rust_pkcs7.encrypt_and_serialize( + self, content_encryption_algorithm, encoding, options + ) pkcs7_decrypt_der = rust_pkcs7.decrypt_der diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index c22d7409e..d9f5ba18a 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -84,6 +84,7 @@ fn serialize_certificates<'p>( fn encrypt_and_serialize<'p>( py: pyo3::Python<'p>, builder: &pyo3::Bound<'p, pyo3::PyAny>, + content_encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, encoding: &pyo3::Bound<'p, pyo3::PyAny>, options: &pyo3::Bound<'p, pyo3::types::PyList>, ) -> CryptographyResult> { @@ -95,14 +96,24 @@ fn encrypt_and_serialize<'p>( smime_canonicalize(raw_data.as_bytes(), text_mode).0 }; - // The message is encrypted with AES-128-CBC, which the S/MIME v3.2 RFC - // specifies as MUST support (https://datatracker.ietf.org/doc/html/rfc5751#section-2.7) - let key = types::OS_URANDOM.get(py)?.call1((16,))?; - let aes128_algorithm = types::AES128.get(py)?.call1((&key,))?; + // Get the content encryption algorithm + let content_encryption_algorithm_type = content_encryption_algorithm; + let key_size = content_encryption_algorithm_type.getattr(pyo3::intern!(py, "key_size"))?; + let key = types::OS_URANDOM + .get(py)? + .call1((key_size.floor_div(8)?,))?; + let content_encryption_algorithm = content_encryption_algorithm_type.call1((&key,))?; + + // Get the mode let iv = types::OS_URANDOM.get(py)?.call1((16,))?; let cbc_mode = types::CBC.get(py)?.call1((&iv,))?; - let encrypted_content = symmetric_encrypt(py, aes128_algorithm, cbc_mode, &data_with_header)?; + let encrypted_content = symmetric_encrypt( + py, + content_encryption_algorithm, + cbc_mode, + &data_with_header, + )?; let py_recipients: Vec> = builder .getattr(pyo3::intern!(py, "_recipients"))? @@ -133,6 +144,13 @@ fn encrypt_and_serialize<'p>( }); } + // Prepare the algorithm parameters + let algorithm_parameters = if content_encryption_algorithm_type.eq(types::AES128.get(py)?)? { + AlgorithmParameters::Aes128Cbc(iv.extract()?) + } else { + AlgorithmParameters::Aes256Cbc(iv.extract()?) + }; + let enveloped_data = pkcs7::EnvelopedData { version: 0, recipient_infos: common::Asn1ReadableOrWritable::new_write(asn1::SetOfWriter::new( @@ -143,7 +161,7 @@ fn encrypt_and_serialize<'p>( content_type: PKCS7_DATA_OID, content_encryption_algorithm: AlgorithmIdentifier { oid: asn1::DefinedByMarker::marker(), - params: AlgorithmParameters::Aes128Cbc(iv.extract()?), + params: algorithm_parameters, }, encrypted_content: Some(&encrypted_content), }, diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py index f853f0aba..86026a3c9 100644 --- a/tests/hazmat/primitives/test_pkcs7.py +++ b/tests/hazmat/primitives/test_pkcs7.py @@ -15,6 +15,7 @@ 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.ciphers import algorithms from cryptography.hazmat.primitives.serialization import pkcs7 from tests.x509.test_x509 import _generate_ca_and_leaf @@ -899,6 +900,21 @@ class TestPKCS7EnvelopeBuilder: b"notacert", # type: ignore[arg-type] ) + def test_set_content_encryption_algorithm_twice(self, backend): + builder = pkcs7.PKCS7EnvelopeBuilder() + builder = builder.set_content_encryption_algorithm(algorithms.AES128) + with pytest.raises(ValueError): + builder.set_content_encryption_algorithm(algorithms.AES128) + + def test_invalid_content_encryption_algorithm(self, backend): + class InvalidAlgorithm: + pass + + with pytest.raises(TypeError): + pkcs7.PKCS7EnvelopeBuilder().set_content_encryption_algorithm( + InvalidAlgorithm, # type: ignore[arg-type] + ) + def test_encrypt_invalid_options(self, backend): cert, _ = _load_rsa_cert_key() builder = ( @@ -1151,12 +1167,14 @@ class TestPKCS7Decrypt: def test_pkcs7_decrypt_aes_256_cbc_encrypted_content( self, backend, data, certificate, private_key ): - # Loading encrypted content (for now, not possible natively) - enveloped = load_vectors_from_file( - os.path.join("pkcs7", "enveloped-aes-256-cbc.pem"), - loader=lambda pemfile: pemfile.read(), - mode="rb", + # Encryption + builder = ( + pkcs7.PKCS7EnvelopeBuilder() + .set_data(data) + .set_content_encryption_algorithm(algorithms.AES256) + .add_recipient(certificate) ) + enveloped = builder.encrypt(serialization.Encoding.PEM, []) # Test decryption: new lines are canonicalized to '\r\n' when # encryption has no Binary option diff --git a/vectors/cryptography_vectors/pkcs7/enveloped-aes-256-cbc.pem b/vectors/cryptography_vectors/pkcs7/enveloped-aes-256-cbc.pem deleted file mode 100644 index bddac0b4e..000000000 --- a/vectors/cryptography_vectors/pkcs7/enveloped-aes-256-cbc.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PKCS7----- -MIICmwYJKoZIhvcNAQcDoIICjDCCAogCAQAxggJDMIICPwIBADAnMBoxGDAWBgNV -BAMMD2NyeXB0b2dyYXBoeSBDQQIJAOcS06ClbtbJMA0GCSqGSIb3DQEBAQUABIIC -ACTeTHyg8zwnBdhLFogSBMInoAqc8HHZ+3vRN57MJ9UA4MIkqgrUEMg2sYwNkpuS -pT3B0tw3CbrJwL4SemPul1FuYMluTRdhJuI9wskR9BvE6d+BlmnFSjNGdt1y9RM+ -7ZqViXGA2t2HVRQ42Q43tkDUL7gMzveYZ1LxG1d+GNbfKLHVqJLokIe+IQYtyRay -3Tck7l/cC2VpI9lwmF+DugpZbagmb3pSij/ZSzzub3PwNp4YaL2YSa1Vkswdm3LD -jhOMSKyw7jIn2e9gQ3VI8vzh/38OFFFoKq7sAGvNGSLDbCHm6AKvOylksnTCUBF2 -6mbNWaaNpRjCQU+8N5/1UblJAs/voG+hGuWbGjS6z4v6mYvIr5731rQjxYbIpZRT -B6+lu9sCbwHuYQKe8MBlsn0+Y/o7l25m+xOfeRK1UGViUNV+2G2SQKY2CnfBoPis -lZSwKv1mfYifT1bsVyTsDWi0yr3BdbhVRI4pLziNrMFJ5tJhN2Y8HB2FGLlmzJtM -YRyljlMtj3YrYnhX82dKIwlrLfoWYP90tiiGh3DlqUTVCj4Y/IBmFGF6VpKWYZ0F -1VGwR8dDt0a0IonoBo3T4OtqUStlMkWgwGyNlauZnXt4jHoP5ECZ23TLpAtLCgUE -BuTiSXYFHaz+ToomhzTqrqznhLf9PRV+TM96/66xYdSYMDwGCSqGSIb3DQEHATAd -BglghkgBZQMEASoEEFSk9vw7RRWfjkB3sVedCgqAEPYXgbXvcA4rj2DCHA80Etg= ------END PKCS7-----