Skip to content

Commit

Permalink
feat: added encryption with AES-256-CBC
Browse files Browse the repository at this point in the history
added & updated tests accordingly

updated documentation

removed useless test vector
  • Loading branch information
nitneuqr committed Dec 20, 2024
1 parent 9f6b24d commit 6cde053
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changelog

.. note:: This version is not yet released and is under active development.

* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm,
in addition to AES-128.

.. _v44-0-0:

Expand Down
3 changes: 0 additions & 3 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
10 changes: 10 additions & 0 deletions docs/hazmat/primitives/asymmetric/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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_algorithm(
... algorithms.AES128
... ).add_recipient(
... cert
... ).encrypt(
Expand All @@ -1284,6 +1287,13 @@ 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_algorithm(algorithm)

Sets the content encryption algorithm to use. Only AES-128 and AES-256 are supported.

:type algorithm: A :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`
with a key size of 128 or 256 bits.

.. method:: add_recipient(certificate)

Add a recipient for the message. Recipients will be able to use their private keys
Expand Down
28 changes: 27 additions & 1 deletion src/cryptography/hazmat/primitives/serialization/pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
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 (
CipherAlgorithm,
algorithms,
)
from cryptography.utils import _check_byteslike

load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates
Expand Down Expand Up @@ -184,6 +188,7 @@ def __init__(
*,
_data: bytes | None = None,
_recipients: list[x509.Certificate] | None = None,
_algorithm: type[CipherAlgorithm] | None = None,
):
from cryptography.hazmat.backends.openssl.backend import (
backend as ossl,
Expand All @@ -198,12 +203,20 @@ def __init__(
self._data = _data
self._recipients = _recipients if _recipients is not None else []

# 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)
self._algorithm = _algorithm or algorithms.AES128

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,
_algorithm=self._algorithm,
)

def add_recipient(
self,
Expand All @@ -221,6 +234,19 @@ def add_recipient(
*self._recipients,
certificate,
],
_algorithm=self._algorithm,
)

def set_algorithm(
self, algorithm: type[CipherAlgorithm]
) -> PKCS7EnvelopeBuilder:
if not issubclass(algorithm, CipherAlgorithm):
raise TypeError("Algorithm must be a CipherAlgorithm")

return PKCS7EnvelopeBuilder(
_data=self._data,
_recipients=self._recipients,
_algorithm=algorithm,
)

def encrypt(
Expand Down
31 changes: 25 additions & 6 deletions src/rust/src/pkcs7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,19 @@ 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 algorithm
let algorithm_type = builder.getattr(pyo3::intern!(py, "_algorithm"))?;
let key_size = algorithm_type.getattr(pyo3::intern!(py, "key_size"))?;
let key = types::OS_URANDOM
.get(py)?
.call1((key_size.floor_div(8)?,))?;
let algorithm = 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, algorithm, cbc_mode, &data_with_header)?;

let py_recipients: Vec<pyo3::Bound<'p, x509::certificate::Certificate>> = builder
.getattr(pyo3::intern!(py, "_recipients"))?
Expand Down Expand Up @@ -133,6 +138,20 @@ fn encrypt_and_serialize<'p>(
});
}

// Prepare the algorithm parameters
let algorithm_parameters = if algorithm_type.eq(types::AES128.get(py)?)? {
AlgorithmParameters::Aes128Cbc(iv.extract()?)
} else if algorithm_type.eq(types::AES256.get(py)?)? {
AlgorithmParameters::Aes256Cbc(iv.extract()?)
} else {
return Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err((
"Unsupported content encryption algorithm: only AES128 and AES256 are supported.",
exceptions::Reasons::UNSUPPORTED_SERIALIZATION,
)),
));
};

let enveloped_data = pkcs7::EnvelopedData {
version: 0,
recipient_infos: common::Asn1ReadableOrWritable::new_write(asn1::SetOfWriter::new(
Expand All @@ -143,7 +162,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),
},
Expand Down
30 changes: 25 additions & 5 deletions tests/hazmat/primitives/test_pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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

Expand Down Expand Up @@ -1078,6 +1079,23 @@ def test_smime_encrypt_multiple_recipients(self, backend):
)
assert enveloped.count(common_name_bytes) == 2

def test_smime_encrypt_unsupported_algorithm(self, backend):
cert, _ = _load_rsa_cert_key()

class AES192(algorithms.AES):
key_sizes = frozenset([192])
key_size = 192

builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(b"hello world\n")
.set_algorithm(AES192)
.add_recipient(cert)
)

with pytest.raises(exceptions.UnsupportedAlgorithm):
builder.encrypt(serialization.Encoding.DER, [])


@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported()
Expand Down Expand Up @@ -1151,12 +1169,14 @@ def test_pkcs7_decrypt_der(
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_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
Expand Down
16 changes: 0 additions & 16 deletions vectors/cryptography_vectors/pkcs7/enveloped-aes-256-cbc.pem

This file was deleted.

0 comments on commit 6cde053

Please sign in to comment.