Skip to content

Commit 0faaffc

Browse files
authored
Add support for encrypting S/MIME messages (#10889)
* Add support for encrypting S/MIME messages * Move PKCS7 decrypt test function to Rust * Use symmetric encryption function from PKCS12 * Remove debug file write from tests * Remove unneeded backend parameter * docs and changelog
1 parent ccb3a32 commit 0faaffc

File tree

12 files changed

+632
-9
lines changed

12 files changed

+632
-9
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Changelog
6666
* :class:`~cryptography.x509.NameAttribute` now raises an exception when
6767
attempting to create a common name whose length is shorter or longer than
6868
:rfc:`5280` permits.
69+
* Added basic support for PKCS7 encryption (including SMIME) via
70+
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7EnvelopeBuilder`.
6971

7072
.. _v42-0-8:
7173

docs/hazmat/primitives/asymmetric/serialization.rst

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,37 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
10951095
-----END CERTIFICATE-----
10961096
""".strip()
10971097

1098+
ca_cert_rsa = b"""
1099+
-----BEGIN CERTIFICATE-----
1100+
MIIExzCCAq+gAwIBAgIJAOcS06ClbtbJMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV
1101+
BAMMD2NyeXB0b2dyYXBoeSBDQTAeFw0yMDA5MTQyMTQwNDJaFw00ODAxMzEyMTQw
1102+
NDJaMBoxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTCCAiIwDQYJKoZIhvcNAQEB
1103+
BQADggIPADCCAgoCggIBANBIheRc1HT4MzV5GvUbDk9CFU6DTomRApNqRmizriRq
1104+
m6OY4Ht3d71BXog6/IBkqAnZ4/XJQ40G4sVDb52k11oPvfJ/F5pc+6UqPBL+QGzY
1105+
GkJoubAqXFpI6ow0qayFNQLv0T9o4yh0QQOoGvgCmv91qmitLrZNXu4U9S76G+Di
1106+
GST+QyMkMxj+VsGRsRRBufV1urcnvFWjU6Q2+cr2cp0mMAG96NTyIskYiJ8vL03W
1107+
z4DX4klO4X47fPmDnU/OMn4SbvMZ896j1L0J04S+uVThTkxQWcFcqXhX5qM8kzcj
1108+
JUmybFlbf150j3WiucW48K/j7fJ0x9q3iUo4Gva0coScglJWcgo/BBCwFDw8NVba
1109+
7npxSRMiaS3qTv0dEFcRnvByc+7hyGxxlWdTE9tHisUI1eZVk9P9ziqNOZKscY8Z
1110+
X1+/C4M9X69Y7A8I74F5dO27IRycEgOrSo2z1NhfSwbqJr9a2TBtRsFinn8rjKBI
1111+
zNn0E5p9jO1WjxtkcjHfXXpLN8FFMvoYI9l/K+ZWDm9sboaF8jrgozSc004AFemA
1112+
H79mmCGVRKXn1vDAo4DLC6p3NiBFYQcYbW9V+beGD6srsF6xJtuY/UwtPROLWSzu
1113+
CCrZ/4BlmpNsR0ehIFFvzEKjX6rR2yp3YKlguDbMBMKMpfSGxAFwcZ7OiaxR20UH
1114+
AgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADSveDS4
1115+
y2V/N6Li2n9ChGNdCMr/45M0cl+GpL55aA36AWYMRLv0wip7MWV3yOj4mkjGBlTE
1116+
awKHH1FtetsE6B4a7M2hHhOXyXE60uUdptEx6ckGrJ1iyqu5cQUX1P+VnXbmOxfF
1117+
bl+Ugzjbgirx239rA4ezkDRuOvKcCbDOFV/gw3ZHfJ/IQeRXIQRl/y51wcnFUvFM
1118+
JEESYiijeDbEcY8r1/phmVQL0CO7WLMmTxlFj4X/TR3MTZWJQIap9GiLs5+n3QiO
1119+
jsZ3GuFOomB8oTebYkXniwbNu5hgLP/seRQzGA7B9VDZryAhCtvGgjtQh0eW2Qxt
1120+
sgmDJGOPKnKT3O5U0v3+IPLEYpe8JSzgAhhh6H1rAJRUNwP2gRcO4eOUJSkdl218
1121+
fRNT0ILzosuWxwprER9ciMQF8q0JJKMhcfHRMH0S5mWVJAIkj68KY05oCy2zNyYa
1122+
oruopKSWXe0Bzr40znm40P7xIkui2BGQMlDPpbCaEfLsLqyctfbdmMlxac/QgIfY
1123+
TltrbqmI3MNy5uqGViGFpWPCB+kD8EsJF9nlKJXlu/i55qgUr/2/2CdeWlZDBP8A
1124+
1fdzmpYpWnwhE0KobzLS2z3AwDxiY/RSWUfypLZA0K/lpaEtYB6UHMDZ0/8WqgZV
1125+
gNucCuty0cA4Kf7eX1TlAKVwH8hTkVmJc2rX
1126+
-----END CERTIFICATE-----
1127+
""".strip()
1128+
10981129

10991130
.. class:: PKCS7SignatureBuilder
11001131

@@ -1174,11 +1205,72 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
11741205
:returns bytes: The signed PKCS7 message.
11751206

11761207

1208+
.. class:: PKCS7EnvelopeBuilder
1209+
1210+
The PKCS7 envelope builder can create encrypted S/MIME messages,
1211+
which are commonly used in email. S/MIME has multiple versions,
1212+
but this implements a subset of :rfc:`5751`, also known as S/MIME
1213+
Version 3.2.
1214+
1215+
.. versionadded:: 43.0.0
1216+
1217+
.. doctest::
1218+
1219+
>>> from cryptography import x509
1220+
>>> from cryptography.hazmat.primitives import serialization
1221+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1222+
>>> cert = x509.load_pem_x509_certificate(ca_cert_rsa)
1223+
>>> options = [pkcs7.PKCS7Options.Text]
1224+
>>> pkcs7.PKCS7EnvelopeBuilder().set_data(
1225+
... b"data to encrypt"
1226+
... ).add_recipient(
1227+
... cert
1228+
... ).encrypt(
1229+
... serialization.Encoding.SMIME, options
1230+
... )
1231+
b'...'
1232+
1233+
.. method:: set_data(data)
1234+
1235+
:param data: The data to be encrypted.
1236+
:type data: :term:`bytes-like`
1237+
1238+
.. method:: add_recipient(certificate)
1239+
1240+
Add a recipient for the message. Recipients will be able to use their private keys
1241+
to decrypt the message. This method may be called multiple times to add as many recipients
1242+
as desired.
1243+
1244+
:param certificate: A :class:`~cryptography.x509.Certificate` for an intended
1245+
recipient of the encrypted message. Only certificates with public RSA keys
1246+
are currently supported.
1247+
1248+
.. method:: encrypt(encoding, options)
1249+
1250+
The message is encrypted using AES-128-CBC. The encryption key used is included in
1251+
the envelope, encrypted using the recipient's public RSA key. If multiple recipients
1252+
are specified, the key is encrypted once with each recipient's public key, and all
1253+
encrypted keys are included in the envelope (one per recipient).
1254+
1255+
:param encoding: :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
1256+
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`,
1257+
or :attr:`~cryptography.hazmat.primitives.serialization.Encoding.SMIME`.
1258+
1259+
:param options: A list of
1260+
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
1261+
this operation only
1262+
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Text` and
1263+
:attr:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Binary`
1264+
are supported.
1265+
1266+
:returns bytes: The enveloped PKCS7 message.
1267+
1268+
11771269
.. class:: PKCS7Options
11781270

11791271
.. versionadded:: 3.2
11801272

1181-
An enumeration of options for PKCS7 signature creation.
1273+
An enumeration of options for PKCS7 signature and envelope creation.
11821274

11831275
.. attribute:: Text
11841276

src/cryptography/hazmat/bindings/_rust/pkcs7.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ def serialize_certificates(
1212
certs: list[x509.Certificate],
1313
encoding: serialization.Encoding,
1414
) -> bytes: ...
15+
def encrypt_and_serialize(
16+
builder: pkcs7.PKCS7EnvelopeBuilder,
17+
encoding: serialization.Encoding,
18+
options: typing.Iterable[pkcs7.PKCS7Options],
19+
) -> bytes: ...
1520
def sign_and_serialize(
1621
builder: pkcs7.PKCS7SignatureBuilder,
1722
encoding: serialization.Encoding,

src/cryptography/hazmat/bindings/_rust/test_support.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ class TestCertificate:
1313
subject_value_tags: list[int]
1414

1515
def test_parse_certificate(data: bytes) -> TestCertificate: ...
16+
def pkcs7_decrypt(
17+
encoding: serialization.Encoding,
18+
msg: bytes,
19+
pkey: serialization.pkcs7.PKCS7PrivateKeyTypes,
20+
cert_recipient: x509.Certificate,
21+
options: list[pkcs7.PKCS7Options],
22+
) -> bytes: ...
1623
def pkcs7_verify(
1724
encoding: serialization.Encoding,
1825
sig: bytes,

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import typing
1313

1414
from cryptography import utils, x509
15+
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
1516
from cryptography.hazmat.bindings._rust import pkcs7 as rust_pkcs7
1617
from cryptography.hazmat.primitives import hashes, serialization
1718
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
@@ -177,7 +178,92 @@ def sign(
177178
return rust_pkcs7.sign_and_serialize(self, encoding, options)
178179

179180

180-
def _smime_encode(
181+
class PKCS7EnvelopeBuilder:
182+
def __init__(
183+
self,
184+
*,
185+
_data: bytes | None = None,
186+
_recipients: list[x509.Certificate] | None = None,
187+
):
188+
from cryptography.hazmat.backends.openssl.backend import (
189+
backend as ossl,
190+
)
191+
192+
if not ossl.rsa_encryption_supported(padding=padding.PKCS1v15()):
193+
raise UnsupportedAlgorithm(
194+
"RSA with PKCS1 v1.5 padding is not supported by this version"
195+
" of OpenSSL.",
196+
_Reasons.UNSUPPORTED_PADDING,
197+
)
198+
self._data = _data
199+
self._recipients = _recipients if _recipients is not None else []
200+
201+
def set_data(self, data: bytes) -> PKCS7EnvelopeBuilder:
202+
_check_byteslike("data", data)
203+
if self._data is not None:
204+
raise ValueError("data may only be set once")
205+
206+
return PKCS7EnvelopeBuilder(_data=data, _recipients=self._recipients)
207+
208+
def add_recipient(
209+
self,
210+
certificate: x509.Certificate,
211+
) -> PKCS7EnvelopeBuilder:
212+
if not isinstance(certificate, x509.Certificate):
213+
raise TypeError("certificate must be a x509.Certificate")
214+
215+
if not isinstance(certificate.public_key(), rsa.RSAPublicKey):
216+
raise TypeError("Only RSA keys are supported at this time.")
217+
218+
return PKCS7EnvelopeBuilder(
219+
_data=self._data,
220+
_recipients=[
221+
*self._recipients,
222+
certificate,
223+
],
224+
)
225+
226+
def encrypt(
227+
self,
228+
encoding: serialization.Encoding,
229+
options: typing.Iterable[PKCS7Options],
230+
) -> bytes:
231+
if len(self._recipients) == 0:
232+
raise ValueError("Must have at least one recipient")
233+
if self._data is None:
234+
raise ValueError("You must add data to encrypt")
235+
options = list(options)
236+
if not all(isinstance(x, PKCS7Options) for x in options):
237+
raise ValueError("options must be from the PKCS7Options enum")
238+
if encoding not in (
239+
serialization.Encoding.PEM,
240+
serialization.Encoding.DER,
241+
serialization.Encoding.SMIME,
242+
):
243+
raise ValueError(
244+
"Must be PEM, DER, or SMIME from the Encoding enum"
245+
)
246+
247+
# Only allow options that make sense for encryption
248+
if any(
249+
opt not in [PKCS7Options.Text, PKCS7Options.Binary]
250+
for opt in options
251+
):
252+
raise ValueError(
253+
"Only the following options are supported for encryption: "
254+
"Text, Binary"
255+
)
256+
elif PKCS7Options.Text in options and PKCS7Options.Binary in options:
257+
# OpenSSL accepts both options at the same time, but ignores Text.
258+
# We fail defensively to avoid unexpected outputs.
259+
raise ValueError(
260+
"Cannot use Binary and Text options at the same time"
261+
)
262+
263+
return rust_pkcs7.encrypt_and_serialize(self, encoding, options)
264+
265+
266+
def _smime_signed_encode(
181267
data: bytes, signature: bytes, micalg: str, text_mode: bool
182268
) -> bytes:
183269
# This function works pretty hard to replicate what OpenSSL does
@@ -225,6 +311,23 @@ def _smime_encode(
225311
return fp.getvalue()
226312

227313

314+
def _smime_enveloped_encode(data: bytes) -> bytes:
315+
m = email.message.Message()
316+
m.add_header("MIME-Version", "1.0")
317+
m.add_header("Content-Disposition", "attachment", filename="smime.p7m")
318+
m.add_header(
319+
"Content-Type",
320+
"application/pkcs7-mime",
321+
smime_type="enveloped-data",
322+
name="smime.p7m",
323+
)
324+
m.add_header("Content-Transfer-Encoding", "base64")
325+
326+
m.set_payload(email.base64mime.body_encode(data, maxlinelen=65))
327+
328+
return m.as_bytes(policy=m.policy.clone(linesep="\n", max_line_length=0))
329+
330+
228331
class OpenSSLMimePart(email.message.MIMEPart):
229332
# A MIMEPart subclass that replicates OpenSSL's behavior of not including
230333
# a newline if there are no headers.

src/rust/cryptography-x509/src/common.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ pub enum AlgorithmParameters<'a> {
136136
#[defined_by(oid::HMAC_WITH_SHA256_OID)]
137137
HmacWithSha256(asn1::Null),
138138

139+
// Used only in PKCS#7 AlgorithmIdentifiers
140+
// https://datatracker.ietf.org/doc/html/rfc3565#section-4.1
141+
//
142+
// From RFC 3565 section 4.1:
143+
// The AlgorithmIdentifier parameters field MUST be present, and the
144+
// parameters field MUST contain a AES-IV:
145+
//
146+
// AES-IV ::= OCTET STRING (SIZE(16))
147+
#[defined_by(oid::AES_128_CBC_OID)]
148+
Aes128Cbc([u8; 16]),
139149
#[defined_by(oid::AES_256_CBC_OID)]
140150
Aes256Cbc([u8; 16]),
141151

src/rust/cryptography-x509/src/pkcs7.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{certificate, common, csr, name};
66

77
pub const PKCS7_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 1);
88
pub const PKCS7_SIGNED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 2);
9+
pub const PKCS7_ENVELOPED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 3);
910
pub const PKCS7_ENCRYPTED_DATA_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 7, 6);
1011

1112
#[derive(asn1::Asn1Write)]
@@ -18,6 +19,8 @@ pub struct ContentInfo<'a> {
1819

1920
#[derive(asn1::Asn1DefinedByWrite)]
2021
pub enum Content<'a> {
22+
#[defined_by(PKCS7_ENVELOPED_DATA_OID)]
23+
EnvelopedData(asn1::Explicit<Box<EnvelopedData<'a>>, 0>),
2124
#[defined_by(PKCS7_SIGNED_DATA_OID)]
2225
SignedData(asn1::Explicit<Box<SignedData<'a>>, 0>),
2326
#[defined_by(PKCS7_DATA_OID)]
@@ -56,6 +59,21 @@ pub struct SignerInfo<'a> {
5659
pub unauthenticated_attributes: Option<csr::Attributes<'a>>,
5760
}
5861

62+
#[derive(asn1::Asn1Write)]
63+
pub struct EnvelopedData<'a> {
64+
pub version: u8,
65+
pub recipient_infos: asn1::SetOfWriter<'a, RecipientInfo<'a>>,
66+
pub encrypted_content_info: EncryptedContentInfo<'a>,
67+
}
68+
69+
#[derive(asn1::Asn1Write)]
70+
pub struct RecipientInfo<'a> {
71+
pub version: u8,
72+
pub issuer_and_serial_number: IssuerAndSerialNumber<'a>,
73+
pub key_encryption_algorithm: common::AlgorithmIdentifier<'a>,
74+
pub encrypted_key: &'a [u8],
75+
}
76+
5977
#[derive(asn1::Asn1Write)]
6078
pub struct IssuerAndSerialNumber<'a> {
6179
pub issuer: name::Name<'a>,

src/rust/src/pkcs12.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl PKCS12Certificate {
7979
}
8080
}
8181

82-
fn symmetric_encrypt(
82+
pub(crate) fn symmetric_encrypt(
8383
py: pyo3::Python<'_>,
8484
algorithm: pyo3::Bound<'_, pyo3::PyAny>,
8585
mode: pyo3::Bound<'_, pyo3::PyAny>,

0 commit comments

Comments
 (0)