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
This commit is contained in:
Quentin Retourne 2024-12-29 19:02:20 +01:00 committed by GitHub
parent 97b388a0a9
commit 6143683d87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 104 additions and 33 deletions

View file

@ -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:

View file

@ -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``.

View file

@ -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

View file

@ -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: ...

View file

@ -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

View file

@ -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<pyo3::Bound<'p, pyo3::types::PyBytes>> {
@ -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<pyo3::Bound<'p, x509::certificate::Certificate>> = 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),
},

View file

@ -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

View file

@ -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-----