Add support for decrypting S/MIME messages (#11555)

* first python API proposition

first round-trip tests

feat: made asn1 structures readable

refacto: adapted existing functions accordingly

feat/pkcs12: added symmetric_decrypt

feat: deserialize 3 possible encodings

feat: handling AES-128

feat: raise error when no recipient is found

feat/pkcs7: added decanonicalize function

feat/asn1: added decode_der_data

feat/pkcs7: added smime_enveloped_decode

tests are the round-trip (encrypt & decrypt)

more tests for 100% python coverage

test support pkcs7_encrypt with openssl

added algorithm to pkcs7_encrypt signature

refacto: decrypt function is clearer

flow is more natural

refacto: added all rust error tests

refacto: added another CA chain for checking

fix: const handling

Refactor PKCS7Decryptor to pkcs7_decrypt

refacto: removed SMIME_ENVELOPED_DECODE from rust code

refacto: removed decode_der_data

adapted tests accordingly

removed the PEM tag check

added tests for smime_decnonicalize

one more test case

Update src/rust/src/pkcs7.rs

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>

took comments into account

pem to der is now outside of decrypt

fix: removed test_support pkcs7_encrypt

added vector for aes_256_cbc encrypted pkcs7

feat: not using test_support decrypt anymore

added new vectors for PKCS7 tests

feat: using pkcs7 vectors

removed previous ones

fix: changed wrong function

feat: added certificate issuer check

test: generating the RSA chain

removed the vectors accordingly

moved symmetric_decrypt to pkcs7.rs

* Update src/cryptography/hazmat/primitives/serialization/pkcs7.py

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>

* fix: removed use of deprecated new_bound for PyBytes

* corrected some error types

* updated tests accordingly

* fix: handling other key encryption algorithms

added vectors & tests accordingly

* first attempts raising error when no header to remove

* one more test to handle text data without header

* fix: went back to the previous implementation

* refacto: removed the return part

* feat: Binary option does not seem useful for decryption

removed decanonicalization function

adapted tests accordingly

* moved logic into rust

only left some checks (for now?)

* removed pyfunction for the inner decrypt one

* added checks in rust now :)

changed name for clarity

* removed unused function

* some checks not needed anymore

* removed a parameter

* took comments into account

* removed unused import

removed excess get_type

* added first unwrap corrections

cleaned tests, added some others

added more vectors

* no more unwrap for parameter checks

* removing headers is Python now

added tests accordingly

will compare with OpenSSL

* final corrections?

* first version of documentation

some minor refactoring

* corrected doctests

* better indentation

* doctest: added RSA private key

* oops

---------

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
This commit is contained in:
Quentin Retourne 2024-11-26 14:39:53 +01:00 committed by GitHub
parent b8e5bfd4d7
commit d6cac753c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 886 additions and 80 deletions

View file

@ -26,6 +26,10 @@ Changelog
* Added support for :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`
when using OpenSSL 3.2.0+.
* Added support for the :class:`~cryptography.x509.Admissions` certificate extension.
* Added basic support for PKCS7 decryption (including S/MIME 3.2) via
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der`,
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_pem`, and
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_smime`.
.. _v43-0-3:

View file

@ -882,6 +882,9 @@ Custom PKCS7 Test Vectors
* ``pkcs7/enveloped-rsa-oaep.pem``- A PEM encoded PKCS7 file with
enveloped data, with key encrypted using RSA-OAEP, under the public key of
``x509/custom/ca/rsa_ca.pem``.
* ``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``.
Custom OpenSSH Test Vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -1001,11 +1001,6 @@ PKCS7 is a format described in :rfc:`2315`, among other specifications. It can
contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
``p7m``, or ``p7s`` file suffix but other suffixes are also seen in the wild.
.. note::
``cryptography`` only supports parsing certificates from PKCS7 files at
this time.
.. data:: PKCS7HashTypes
.. versionadded:: 40.0.0
@ -1126,6 +1121,60 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
-----END CERTIFICATE-----
""".strip()
ca_key_rsa = b"""
-----BEGIN PRIVATE KEY-----
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
fDWXtBuybAb5u+ZaVwHgqaaPCkKkVQ==
-----END PRIVATE KEY-----
""".strip()
.. class:: PKCS7SignatureBuilder
@ -1261,28 +1310,204 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
this operation only
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Text` and
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Binary`
are supported.
are supported, and cannot be used at the same time.
:returns bytes: The enveloped PKCS7 message.
.. function:: pkcs7_decrypt_der(data, certificate, private_key, options)
.. versionadded:: 44.0.0
.. doctest::
>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(ca_cert_rsa)
>>> key = serialization.load_pem_private_key(ca_key_rsa, None)
>>> options = [pkcs7.PKCS7Options.Text]
>>> enveloped = pkcs7.PKCS7EnvelopeBuilder().set_data(
... b"data to encrypt"
... ).add_recipient(
... cert
... ).encrypt(
... serialization.Encoding.DER, options
... )
>>> pkcs7.pkcs7_decrypt_der(enveloped, cert, key, options)
b'data to encrypt'
Deserialize and decrypt a DER-encoded PKCS7 message. PKCS7 (or S/MIME) has multiple versions,
but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
:param data: The data, encoded in DER format.
:type data: bytes
:param certificate: A :class:`~cryptography.x509.Certificate` for an intended
recipient of the encrypted message. Only certificates with public RSA keys
are currently supported.
:param private_key: The :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
associated with the certificate provided. Only private RSA keys are supported.
:param options: A list of
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
this operation only
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Text` is supported.
:returns bytes: The decrypted message.
:raises ValueError: If the recipient certificate does not match any of the encrypted keys in the
PKCS7 data.
:raises cryptography.exceptions.UnsupportedAlgorithm: If any of the PKCS7 keys are encrypted
with another algorithm than RSA with PKCS1 v1.5 padding.
:raises cryptography.exceptions.UnsupportedAlgorithm: If the content is encrypted with
another algorithm than AES-128-CBC.
:raises ValueError: If the PKCS7 data does not contain encrypted content.
:raises ValueError: If the PKCS7 data is not of the enveloped data type.
.. function:: pkcs7_decrypt_pem(data, certificate, private_key, options)
.. versionadded:: 44.0.0
.. doctest::
>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(ca_cert_rsa)
>>> key = serialization.load_pem_private_key(ca_key_rsa, None)
>>> options = [pkcs7.PKCS7Options.Text]
>>> enveloped = pkcs7.PKCS7EnvelopeBuilder().set_data(
... b"data to encrypt"
... ).add_recipient(
... cert
... ).encrypt(
... serialization.Encoding.PEM, options
... )
>>> pkcs7.pkcs7_decrypt_pem(enveloped, cert, key, options)
b'data to encrypt'
Deserialize and decrypt a PEM-encoded PKCS7E message. PKCS7 (or S/MIME) has multiple versions,
but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
:param data: The data, encoded in PEM format.
:type data: bytes
:param certificate: A :class:`~cryptography.x509.Certificate` for an intended
recipient of the encrypted message. Only certificates with public RSA keys
are currently supported.
:param private_key: The :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
associated with the certificate provided. Only private RSA keys are supported.
:param options: A list of
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
this operation only
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Text` is supported.
:returns bytes: The decrypted message.
:raises ValueError: If the PEM data does not have the PKCS7 tag.
:raises ValueError: If the recipient certificate does not match any of the encrypted keys in the
PKCS7 data.
:raises cryptography.exceptions.UnsupportedAlgorithm: If any of the PKCS7 keys are encrypted
with another algorithm than RSA with PKCS1 v1.5 padding.
:raises cryptography.exceptions.UnsupportedAlgorithm: If the content is encrypted with
another algorithm than AES-128-CBC.
:raises ValueError: If the PKCS7 data does not contain encrypted content.
:raises ValueError: If the PKCS7 data is not of the enveloped data type.
.. function:: pkcs7_decrypt_smime(data, certificate, private_key, options)
.. versionadded:: 44.0.0
.. doctest::
>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(ca_cert_rsa)
>>> key = serialization.load_pem_private_key(ca_key_rsa, None)
>>> options = [pkcs7.PKCS7Options.Text]
>>> enveloped = pkcs7.PKCS7EnvelopeBuilder().set_data(
... b"data to encrypt"
... ).add_recipient(
... cert
... ).encrypt(
... serialization.Encoding.SMIME, options
... )
>>> pkcs7.pkcs7_decrypt_smime(enveloped, cert, key, options)
b'data to encrypt'
Deserialize and decrypt a S/MIME-encoded PKCS7 message. PKCS7 (or S/MIME) has multiple versions,
but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
:param data: The data. It should be in S/MIME format, meaning MIME with content type
``application/pkcs7-mime`` or ``application/x-pkcs7-mime``.
:type data: bytes
:param certificate: A :class:`~cryptography.x509.Certificate` for an intended
recipient of the encrypted message. Only certificates with public RSA keys
are currently supported.
:param private_key: The :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
associated with the certificate provided. Only private RSA keys are supported.
:param options: A list of
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
this operation only
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Text` is supported.
:returns bytes: The decrypted message.
:raises ValueError: If the S/MIME data is not one of the correct content types.
:raises ValueError: If the recipient certificate does not match any of the encrypted keys in the
PKCS7 data.
:raises cryptography.exceptions.UnsupportedAlgorithm: If any of the PKCS7 keys are encrypted
with another algorithm than RSA with PKCS1 v1.5 padding.
:raises cryptography.exceptions.UnsupportedAlgorithm: If the content is encrypted with
another algorithm than AES-128-CBC.
:raises ValueError: If the PKCS7 data does not contain encrypted content.
:raises ValueError: If the PKCS7 data is not of the enveloped data type.
.. class:: PKCS7Options
.. versionadded:: 3.2
An enumeration of options for PKCS7 signature and envelope creation.
An enumeration of options for PKCS7 signature, envelope creation, and decryption.
.. attribute:: Text
The text option adds ``text/plain`` headers to an S/MIME message when
serializing to
For signing, the text option adds ``text/plain`` headers to an S/MIME message when
serializing to
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.SMIME`.
This option is disallowed with ``DER`` serialization.
For envelope creation, it adds ``text/plain`` headers to the encrypted content, regardless
of the specified encoding.
For envelope decryption, it parses the decrypted content headers (if any), checks if the
content type is 'text/plain', then removes all headers (keeping only the payload) of this
decrypted content. If there is no header, or the content type is not "text/plain", it
raises an error.
.. attribute:: Binary
Signing normally converts line endings (LF to CRLF). When
passing this option the data will not be converted.
Signature and envelope creation normally converts line endings (LF to CRLF). When
passing this option, the data will not be converted.
.. attribute:: DetachedSignature

View file

@ -6,6 +6,7 @@ import typing
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import pkcs7
def serialize_certificates(
@ -22,6 +23,24 @@ def sign_and_serialize(
encoding: serialization.Encoding,
options: typing.Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def decrypt_der(
data: bytes,
certificate: x509.Certificate,
private_key: rsa.RSAPrivateKey,
options: typing.Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def decrypt_pem(
data: bytes,
certificate: x509.Certificate,
private_key: rsa.RSAPrivateKey,
options: typing.Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def decrypt_smime(
data: bytes,
certificate: x509.Certificate,
private_key: rsa.RSAPrivateKey,
options: typing.Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def load_pem_pkcs7_certificates(
data: bytes,
) -> list[x509.Certificate]: ...

View file

@ -13,13 +13,6 @@ class TestCertificate:
subject_value_tags: list[int]
def test_parse_certificate(data: bytes) -> TestCertificate: ...
def pkcs7_decrypt(
encoding: serialization.Encoding,
msg: bytes,
pkey: serialization.pkcs7.PKCS7PrivateKeyTypes,
cert_recipient: x509.Certificate,
options: list[pkcs7.PKCS7Options],
) -> bytes: ...
def pkcs7_verify(
encoding: serialization.Encoding,
sig: bytes,

View file

@ -263,6 +263,11 @@ class PKCS7EnvelopeBuilder:
return rust_pkcs7.encrypt_and_serialize(self, encoding, options)
pkcs7_decrypt_der = rust_pkcs7.decrypt_der
pkcs7_decrypt_pem = rust_pkcs7.decrypt_pem
pkcs7_decrypt_smime = rust_pkcs7.decrypt_smime
def _smime_signed_encode(
data: bytes, signature: bytes, micalg: str, text_mode: bool
) -> bytes:
@ -328,6 +333,34 @@ def _smime_enveloped_encode(data: bytes) -> bytes:
return m.as_bytes(policy=m.policy.clone(linesep="\n", max_line_length=0))
def _smime_enveloped_decode(data: bytes) -> bytes:
m = email.message_from_bytes(data)
if m.get_content_type() not in {
"application/x-pkcs7-mime",
"application/pkcs7-mime",
}:
raise ValueError("Not an S/MIME enveloped message")
return bytes(m.get_payload(decode=True))
def _smime_remove_text_headers(data: bytes) -> bytes:
m = email.message_from_bytes(data)
# Using get() instead of get_content_type() since it has None as default,
# where the latter has "text/plain". Both methods are case-insensitive.
content_type = m.get("content-type")
if content_type is None:
raise ValueError(
"Decrypted MIME data has no 'Content-Type' header. "
"Please remove the 'Text' option to parse it manually."
)
if "text/plain" not in content_type:
raise ValueError(
f"Decrypted MIME data content type is '{content_type}', not "
"'text/plain'. Remove the 'Text' option to parse it manually."
)
return bytes(m.get_payload(decode=True))
class OpenSSLMimePart(email.message.MIMEPart):
# A MIMEPart subclass that replicates OpenSSL's behavior of not including
# a newline if there are no headers.

View file

@ -16,8 +16,10 @@ use openssl::pkcs7::Pkcs7;
use pyo3::types::{PyAnyMethods, PyBytesMethods, PyListMethods};
use crate::asn1::encode_der_data;
use crate::backend::ciphers;
use crate::buf::CffiBuf;
use crate::error::{CryptographyError, CryptographyResult};
use crate::padding::PKCS7UnpaddingContext;
use crate::pkcs12::symmetric_encrypt;
#[cfg(not(CRYPTOGRAPHY_IS_BORINGSSL))]
use crate::x509::certificate::load_der_x509_certificate;
@ -164,6 +166,265 @@ fn encrypt_and_serialize<'p>(
}
}
#[pyo3::pyfunction]
fn decrypt_smime<'p>(
py: pyo3::Python<'p>,
data: CffiBuf<'p>,
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
private_key: pyo3::Bound<'p, pyo3::types::PyAny>,
options: &pyo3::Bound<'p, pyo3::types::PyList>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let decoded_smime_data = types::SMIME_ENVELOPED_DECODE
.get(py)?
.call1((data.as_bytes(),))?;
let data = decoded_smime_data.extract()?;
decrypt_der(py, data, certificate, private_key, options)
}
#[pyo3::pyfunction]
fn decrypt_pem<'p>(
py: pyo3::Python<'p>,
data: &[u8],
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
private_key: pyo3::Bound<'p, pyo3::types::PyAny>,
options: &pyo3::Bound<'p, pyo3::types::PyList>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let pem_str = std::str::from_utf8(data)
.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.",
),
));
}
decrypt_der(py, &pem.into_contents(), certificate, private_key, options)
}
#[pyo3::pyfunction]
fn decrypt_der<'p>(
py: pyo3::Python<'p>,
data: &[u8],
certificate: pyo3::Bound<'p, x509::certificate::Certificate>,
private_key: pyo3::Bound<'p, pyo3::types::PyAny>,
options: &pyo3::Bound<'p, pyo3::types::PyList>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
// Check the decrypt parameters
check_decrypt_parameters(py, &certificate, &private_key, options)?;
// Decrypt the data
let content_info = asn1::parse_single::<pkcs7::ContentInfo<'_>>(data)?;
let plain_content = match content_info.content {
pkcs7::Content::EnvelopedData(data) => {
// Extract enveloped data
let enveloped_data = data.into_inner();
// Get recipients, and the one matching with the given certificate (if any)
let mut recipient_infos = enveloped_data.recipient_infos.unwrap_read().clone();
let recipient_certificate = certificate.get().raw.borrow_dependent();
let recipient_serial_number = recipient_certificate.tbs_cert.serial;
let recipient_issuer = recipient_certificate.tbs_cert.issuer.clone();
let found_recipient_info = recipient_infos.find(|info| {
info.issuer_and_serial_number.serial_number == recipient_serial_number
&& info.issuer_and_serial_number.issuer == recipient_issuer
});
// Raise error when no recipient is found
let recipient_info = match found_recipient_info {
Some(info) => info,
None => {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"No recipient found that matches the given certificate.",
),
));
}
};
// Raise error when the key encryption algorithm is not RSA
let key = match recipient_info.key_encryption_algorithm.oid() {
&oid::RSA_OID => {
let padding = types::PKCS1V15.get(py)?.call0()?;
private_key
.call_method1(
pyo3::intern!(py, "decrypt"),
(recipient_info.encrypted_key, &padding),
)?
.extract::<pyo3::pybacked::PyBackedBytes>()?
}
_ => {
return Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err((
"Only RSA with PKCS #1 v1.5 padding is currently supported for key decryption.",
exceptions::Reasons::UNSUPPORTED_SERIALIZATION,
)),
));
}
};
// Get algorithm
// TODO: implement all the possible algorithms
let algorithm_identifier = enveloped_data
.encrypted_content_info
.content_encryption_algorithm;
let (algorithm, mode) = match algorithm_identifier.params {
AlgorithmParameters::Aes128Cbc(iv) => (
types::AES128.get(py)?.call1((key,))?,
types::CBC
.get(py)?
.call1((pyo3::types::PyBytes::new(py, &iv),))?,
),
_ => {
return Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err((
"Only AES-128-CBC is currently supported for content decryption.",
exceptions::Reasons::UNSUPPORTED_SERIALIZATION,
)),
));
}
};
// Decrypt the content using the key and proper algorithm
let encrypted_content = match enveloped_data.encrypted_content_info.encrypted_content {
Some(content) => content,
None => {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"The EnvelopedData structure does not contain encrypted content.",
),
));
}
};
let decrypted_content = symmetric_decrypt(py, algorithm, mode, encrypted_content)?;
pyo3::types::PyBytes::new(py, decrypted_content.as_slice())
}
_ => {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"The PKCS7 data is not an EnvelopedData structure.",
),
));
}
};
// If text_mode, remove the headers after checking the content type
let plain_data = if options.contains(types::PKCS7_TEXT.get(py)?)? {
let stripped_data = types::SMIME_REMOVE_TEXT_HEADERS
.get(py)?
.call1((plain_content.as_bytes(),))?;
pyo3::types::PyBytes::new(py, stripped_data.extract()?)
} else {
pyo3::types::PyBytes::new(py, plain_content.as_bytes())
};
Ok(plain_data)
}
fn check_decrypt_parameters<'p>(
py: pyo3::Python<'p>,
certificate: &pyo3::Bound<'p, x509::certificate::Certificate>,
private_key: &pyo3::Bound<'p, pyo3::PyAny>,
options: &pyo3::Bound<'p, pyo3::types::PyList>,
) -> Result<(), CryptographyError> {
// Check if RSA encryption with PKCS1 v1.5 padding is supported (dependent of FIPS mode)
if cryptography_openssl::fips::is_enabled() {
return Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err((
"RSA with PKCS1 v1.5 padding is not supported by this version of OpenSSL.",
exceptions::Reasons::UNSUPPORTED_PADDING,
)),
));
}
// 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::Text
let text_option = types::PKCS7_TEXT.get(py)?;
for opt in options.iter() {
if !opt.eq(text_option.clone())? {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"Only the following options are supported for decryption: Text",
),
));
}
}
// Check if certificate's public key is an RSA public key
let public_key_type = types::RSA_PUBLIC_KEY.get(py)?;
if !certificate
.call_method0(pyo3::intern!(py, "public_key"))?
.is_instance(&public_key_type)?
{
return Err(CryptographyError::from(
pyo3::exceptions::PyTypeError::new_err(
"Only certificate with RSA public keys are supported at this time.",
),
));
}
// Check if private_key is an instance of RSA private key
let private_key_type = types::RSA_PRIVATE_KEY.get(py)?;
if !private_key.is_instance(&private_key_type)? {
return Err(CryptographyError::from(
pyo3::exceptions::PyTypeError::new_err(
"Only RSA private keys are supported at this time.",
),
));
}
Ok(())
}
pub(crate) fn symmetric_decrypt(
py: pyo3::Python<'_>,
algorithm: pyo3::Bound<'_, pyo3::PyAny>,
mode: pyo3::Bound<'_, pyo3::PyAny>,
data: &[u8],
) -> CryptographyResult<Vec<u8>> {
let block_size = algorithm
.getattr(pyo3::intern!(py, "block_size"))?
.extract()?;
let mut cipher =
ciphers::CipherContext::new(py, algorithm, mode, openssl::symm::Mode::Decrypt)?;
// Decrypt the data
let mut decrypted_data = vec![0; data.len() + (block_size / 8)];
let count = cipher.update_into(py, data, &mut decrypted_data)?;
let final_block = cipher.finalize(py)?;
assert!(final_block.as_bytes().is_empty());
decrypted_data.truncate(count);
// Unpad the data
let mut unpadder = PKCS7UnpaddingContext::new(block_size);
let unpadded_first_blocks = unpadder.update(py, CffiBuf::from_bytes(py, &decrypted_data))?;
let unpadded_last_block = unpadder.finalize(py)?;
let unpadded_data = [
unpadded_first_blocks.as_bytes(),
unpadded_last_block.as_bytes(),
]
.concat();
Ok(unpadded_data)
}
#[pyo3::pyfunction]
fn sign_and_serialize<'p>(
py: pyo3::Python<'p>,
@ -507,8 +768,9 @@ fn load_der_pkcs7_certificates<'p>(
pub(crate) mod pkcs7_mod {
#[pymodule_export]
use super::{
encrypt_and_serialize, load_der_pkcs7_certificates, load_pem_pkcs7_certificates,
serialize_certificates, sign_and_serialize,
decrypt_der, decrypt_pem, decrypt_smime, encrypt_and_serialize,
load_der_pkcs7_certificates, load_pem_pkcs7_certificates, serialize_certificates,
sign_and_serialize,
};
}

View file

@ -103,55 +103,8 @@ fn pkcs7_verify(
Ok(())
}
#[cfg(not(CRYPTOGRAPHY_IS_BORINGSSL))]
#[pyo3::pyfunction]
#[pyo3(signature = (encoding, msg, pkey, cert_recipient, options))]
fn pkcs7_decrypt<'p>(
py: pyo3::Python<'p>,
encoding: pyo3::Bound<'p, pyo3::PyAny>,
msg: CffiBuf<'p>,
pkey: pyo3::Bound<'p, pyo3::PyAny>,
cert_recipient: pyo3::Bound<'p, PyCertificate>,
options: pyo3::Bound<'p, pyo3::types::PyList>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let p7 = if encoding.is(&types::ENCODING_DER.get(py)?) {
openssl::pkcs7::Pkcs7::from_der(msg.as_bytes())?
} else if encoding.is(&types::ENCODING_PEM.get(py)?) {
openssl::pkcs7::Pkcs7::from_pem(msg.as_bytes())?
} else {
openssl::pkcs7::Pkcs7::from_smime(msg.as_bytes())?.0
};
let mut flags = openssl::pkcs7::Pkcs7Flags::empty();
if options.contains(types::PKCS7_TEXT.get(py)?)? {
flags |= openssl::pkcs7::Pkcs7Flags::TEXT;
}
let cert_der = asn1::write_single(cert_recipient.get().raw.borrow_dependent())?;
let cert_ossl = openssl::x509::X509::from_der(&cert_der)?;
let der = types::ENCODING_DER.get(py)?;
let pkcs8 = types::PRIVATE_FORMAT_PKCS8.get(py)?;
let no_encryption = types::NO_ENCRYPTION.get(py)?.call0()?;
let pkey_bytes = pkey
.call_method1(
pyo3::intern!(py, "private_bytes"),
(der, pkcs8, no_encryption),
)?
.extract::<pyo3::pybacked::PyBackedBytes>()?;
let pkey_ossl = openssl::pkey::PKey::private_key_from_der(&pkey_bytes)?;
let result = p7.decrypt(&pkey_ossl, &cert_ossl, flags)?;
Ok(pyo3::types::PyBytes::new(py, &result))
}
#[pyo3::pymodule]
pub(crate) mod test_support {
#[cfg(not(CRYPTOGRAPHY_IS_BORINGSSL))]
#[pymodule_export]
use super::pkcs7_decrypt;
#[cfg(not(CRYPTOGRAPHY_IS_BORINGSSL))]
#[pymodule_export]
use super::pkcs7_verify;

View file

@ -320,6 +320,11 @@ pub static ASN1_TYPE_BMP_STRING: LazyPyImport =
pub static ASN1_TYPE_UNIVERSAL_STRING: LazyPyImport =
LazyPyImport::new("cryptography.x509.name", &["_ASN1Type", "UniversalString"]);
pub static PKCS7_OPTIONS: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.serialization.pkcs7",
&["PKCS7Options"],
);
pub static PKCS7_BINARY: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.serialization.pkcs7",
&["PKCS7Options", "Binary"],
@ -350,6 +355,16 @@ pub static SMIME_ENVELOPED_ENCODE: LazyPyImport = LazyPyImport::new(
&["_smime_enveloped_encode"],
);
pub static SMIME_ENVELOPED_DECODE: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.serialization.pkcs7",
&["_smime_enveloped_decode"],
);
pub static SMIME_REMOVE_TEXT_HEADERS: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.serialization.pkcs7",
&["_smime_remove_text_headers"],
);
pub static SMIME_SIGNED_ENCODE: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.serialization.pkcs7",
&["_smime_signed_encode"],

View file

@ -6,18 +6,28 @@
import email.parser
import os
import typing
from email.message import EmailMessage
import pytest
from cryptography import x509
from cryptography import exceptions, x509
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.serialization import pkcs7
from tests.x509.test_x509 import _generate_ca_and_leaf
from ...hazmat.primitives.fixtures_rsa import (
RSA_KEY_2048_ALT,
)
from ...hazmat.primitives.test_rsa import rsa_key_2048
from ...utils import load_vectors_from_file, raises_unsupported_algorithm
# Make ruff happy since we're importing fixtures that pytest patches in as
# func args
__all__ = ["rsa_key_2048"]
@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported(),
@ -966,13 +976,13 @@ class TestPKCS7EnvelopeBuilder:
b"\x20\x43\x41"
) in payload
decrypted_bytes = test_support.pkcs7_decrypt(
serialization.Encoding.SMIME,
decrypted_bytes = pkcs7.pkcs7_decrypt_smime(
enveloped,
private_key,
cert,
options,
private_key,
[o for o in options if o != pkcs7.PKCS7Options.Binary],
)
# New lines are canonicalized to '\r\n' when not using Binary
expected_data = (
data
@ -1008,12 +1018,11 @@ class TestPKCS7EnvelopeBuilder:
b"\x20\x43\x41"
) in enveloped
decrypted_bytes = test_support.pkcs7_decrypt(
serialization.Encoding.DER,
decrypted_bytes = pkcs7.pkcs7_decrypt_der(
enveloped,
private_key,
cert,
options,
private_key,
[o for o in options if o != pkcs7.PKCS7Options.Binary],
)
# New lines are canonicalized to '\r\n' when not using Binary
expected_data = (
@ -1037,13 +1046,13 @@ class TestPKCS7EnvelopeBuilder:
pkcs7.PKCS7EnvelopeBuilder().set_data(data).add_recipient(cert)
)
enveloped = builder.encrypt(serialization.Encoding.PEM, options)
decrypted_bytes = test_support.pkcs7_decrypt(
serialization.Encoding.PEM,
decrypted_bytes = pkcs7.pkcs7_decrypt_pem(
enveloped,
private_key,
cert,
options,
private_key,
[o for o in options if o != pkcs7.PKCS7Options.Binary],
)
# New lines are canonicalized to '\r\n' when not using Binary
expected_data = (
data
@ -1070,6 +1079,284 @@ class TestPKCS7EnvelopeBuilder:
assert enveloped.count(common_name_bytes) == 2
@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported()
and backend.rsa_encryption_supported(padding.PKCS1v15()),
skip_message="Requires OpenSSL with PKCS7 support and PKCS1 v1.5 padding "
"support",
)
class TestPKCS7Decrypt:
@pytest.fixture(name="data")
def fixture_data(self, backend) -> bytes:
return b"Hello world!\n"
@pytest.fixture(name="certificate")
def fixture_certificate(self, backend) -> x509.Certificate:
certificate, _ = _load_rsa_cert_key()
return certificate
@pytest.fixture(name="private_key")
def fixture_private_key(self, backend) -> rsa.RSAPrivateKey:
_, private_key = _load_rsa_cert_key()
return private_key
def test_unsupported_certificate_encryption(self, backend, private_key):
cert_non_rsa, _ = _load_cert_key()
with pytest.raises(TypeError):
pkcs7.pkcs7_decrypt_der(b"", cert_non_rsa, private_key, [])
def test_not_a_cert(self, backend, private_key):
with pytest.raises(TypeError):
pkcs7.pkcs7_decrypt_der(b"", b"wrong_type", private_key, []) # type: ignore[arg-type]
def test_not_a_pkey(self, backend, certificate):
with pytest.raises(TypeError):
pkcs7.pkcs7_decrypt_der(b"", certificate, b"wrong_type", []) # type: ignore[arg-type]
@pytest.mark.parametrize(
"invalid_options",
[
[b"invalid"],
[pkcs7.PKCS7Options.NoAttributes],
[pkcs7.PKCS7Options.Binary],
],
)
def test_pkcs7_decrypt_invalid_options(
self, backend, invalid_options, data, certificate, private_key
):
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(
data, certificate, private_key, invalid_options
)
@pytest.mark.parametrize("options", [[], [pkcs7.PKCS7Options.Text]])
def test_pkcs7_decrypt_der(
self, backend, data, certificate, private_key, options
):
# Encryption
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(serialization.Encoding.DER, options)
# Test decryption: new lines are canonicalized to '\r\n' when
# encryption has no Binary option
decrypted = pkcs7.pkcs7_decrypt_der(
enveloped, certificate, private_key, options
)
assert decrypted == data.replace(b"\n", b"\r\n")
@pytest.mark.parametrize(
"header",
[
"content-type: text/plain",
"CONTENT-TYPE: text/plain",
"MIME-Version: 1.0\r\nContent-Type: text/plain; charset='UTF-8'"
"\r\nContent-Transfer-Encoding: 7bit\r\nFrom: sender@example.com"
"\r\nTo: recipient@example.com\r\nSubject: Test Email",
],
)
def test_pkcs7_decrypt_der_text_handmade_header(
self, backend, certificate, private_key, header
):
# Encryption of data with a custom header
base_data = "Hello world!\r\n"
data = f"{header}\r\n\r\n{base_data}".encode()
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(
serialization.Encoding.DER, [pkcs7.PKCS7Options.Binary]
)
# Test decryption with text option
decrypted = pkcs7.pkcs7_decrypt_der(
enveloped, certificate, private_key, [pkcs7.PKCS7Options.Text]
)
assert decrypted == base_data.encode()
@pytest.mark.parametrize("options", [[], [pkcs7.PKCS7Options.Text]])
def test_pkcs7_decrypt_pem(
self, backend, data, certificate, private_key, options
):
# Encryption
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(serialization.Encoding.PEM, options)
# Test decryption: new lines are canonicalized to '\r\n' when
# encryption has no Binary option
decrypted = pkcs7.pkcs7_decrypt_pem(
enveloped, certificate, private_key, options
)
assert decrypted == data.replace(b"\n", b"\r\n")
def test_pkcs7_decrypt_pem_with_wrong_tag(
self, backend, data, certificate, private_key
):
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_pem(
certificate.public_bytes(serialization.Encoding.PEM),
certificate,
private_key,
[],
)
@pytest.mark.parametrize("options", [[], [pkcs7.PKCS7Options.Text]])
def test_pkcs7_decrypt_smime(
self, backend, data, certificate, private_key, options
):
# Encryption
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(serialization.Encoding.SMIME, options)
# Test decryption
decrypted = pkcs7.pkcs7_decrypt_smime(
enveloped, certificate, private_key, options
)
assert decrypted == data.replace(b"\n", b"\r\n")
def test_pkcs7_decrypt_no_encrypted_content(
self, backend, data, certificate, private_key
):
enveloped = load_vectors_from_file(
os.path.join("pkcs7", "enveloped-no-content.der"),
loader=lambda pemfile: pemfile.read(),
mode="rb",
)
# Test decryption with text option
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(enveloped, certificate, private_key, [])
def test_pkcs7_decrypt_text_no_header(
self, backend, data, certificate, private_key
):
# Encryption of data without a header (no "Text" option)
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(serialization.Encoding.DER, [])
# Test decryption with text option
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(
enveloped, certificate, private_key, [pkcs7.PKCS7Options.Text]
)
def test_pkcs7_decrypt_text_html_content_type(
self, backend, certificate, private_key
):
# Encryption of data with a text/html content type header
data = b"Content-Type: text/html\r\n\r\nHello world!<br>"
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(
serialization.Encoding.DER, [pkcs7.PKCS7Options.Binary]
)
# Test decryption with text option
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(
enveloped, certificate, private_key, [pkcs7.PKCS7Options.Text]
)
def test_smime_decrypt_no_recipient_match(
self, backend, data, certificate, rsa_key_2048: rsa.RSAPrivateKey
):
# Encrypt some data with one RSA chain
builder = (
pkcs7.PKCS7EnvelopeBuilder()
.set_data(data)
.add_recipient(certificate)
)
enveloped = builder.encrypt(serialization.Encoding.DER, [])
# Prepare another RSA chain
another_private_key = RSA_KEY_2048_ALT.private_key(
unsafe_skip_rsa_key_validation=True
)
_, another_cert = _generate_ca_and_leaf(
rsa_key_2048, another_private_key
)
# Test decryption with another RSA chain
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(
enveloped, another_cert, another_private_key, []
)
def test_smime_decrypt_unsupported_key_encryption_algorithm(
self, backend, data, certificate, private_key
):
enveloped = load_vectors_from_file(
os.path.join("pkcs7", "enveloped-rsa-oaep.pem"),
loader=lambda pemfile: pemfile.read(),
mode="rb",
)
with pytest.raises(exceptions.UnsupportedAlgorithm):
pkcs7.pkcs7_decrypt_pem(enveloped, certificate, private_key, [])
def test_smime_decrypt_unsupported_content_encryption_algorithm(
self, backend, data, certificate, private_key
):
enveloped = load_vectors_from_file(
os.path.join("pkcs7", "enveloped-aes-256-cbc.pem"),
loader=lambda pemfile: pemfile.read(),
mode="rb",
)
with pytest.raises(exceptions.UnsupportedAlgorithm):
pkcs7.pkcs7_decrypt_pem(enveloped, certificate, private_key, [])
def test_smime_decrypt_not_enveloped(
self, backend, data, certificate, private_key
):
# Create a signed email
cert, key = _load_cert_key()
options = [pkcs7.PKCS7Options.DetachedSignature]
builder = (
pkcs7.PKCS7SignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
signed = builder.sign(serialization.Encoding.DER, options)
# Test decryption failure with signed email
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_der(signed, certificate, private_key, [])
def test_smime_decrypt_smime_not_encrypted(
self, backend, certificate, private_key
):
# Create a plain email
email_message = EmailMessage()
email_message.set_content("Hello world!")
# Test decryption failure with plain email
with pytest.raises(ValueError):
pkcs7.pkcs7_decrypt_smime(
email_message.as_bytes(), certificate, private_key, []
)
@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported(),
skip_message="Requires OpenSSL with PKCS7 support",
@ -1168,3 +1455,15 @@ class TestPKCS7EnvelopeBuilderUnsupported:
def test_envelope_builder_unsupported(self, backend):
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_PADDING):
pkcs7.PKCS7EnvelopeBuilder()
@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported()
and not backend.rsa_encryption_supported(padding.PKCS1v15()),
skip_message="Requires OpenSSL with no PKCS1 v1.5 padding support",
)
class TestPKCS7DecryptUnsupported:
def test_pkcs7_decrypt_unsupported(self, backend):
cert, key = _load_rsa_cert_key()
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_PADDING):
pkcs7.pkcs7_decrypt_der(b"", cert, key, [])