mirror of
https://github.com/saymrwulf/cryptography.git
synced 2026-05-14 20:37:55 +00:00
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:
parent
b8e5bfd4d7
commit
d6cac753c2
11 changed files with 886 additions and 80 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]: ...
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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, [])
|
||||
|
|
|
|||
BIN
vectors/cryptography_vectors/pkcs7/enveloped-no-content.der
Normal file
BIN
vectors/cryptography_vectors/pkcs7/enveloped-no-content.der
Normal file
Binary file not shown.
Loading…
Reference in a new issue