Skip to content

Commit bdfd759

Browse files
committed
PKCS7 signing (first version)
no certificate verification as of now handling PEM, DER, SMIME formats added tests & documentation accordingly
1 parent fed4a18 commit bdfd759

File tree

9 files changed

+576
-6
lines changed

9 files changed

+576
-6
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ Changelog
3535
:meth:`VerifiedClient.subject <cryptography.x509.verification.VerifiedClient.subjects>`
3636
property can now be `None` since a custom extension policy may allow certificates
3737
without a Subject Alternative Name extension.
38+
* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm,
39+
in addition to AES-128.
40+
* Added basic support for PKCS7 verification (including S/MIME 3.2) via
41+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_der`,
42+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_pem`, and
43+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_smime`.
44+
3845

3946
.. _v44-0-1:
4047

docs/development/test-vectors.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,9 @@ Custom PKCS7 Test Vectors
948948
* ``pkcs7/enveloped-no-content.der``- A DER encoded PKCS7 file with
949949
enveloped data, without encrypted content, with key encrypted under the
950950
public key of ``x509/custom/ca/rsa_ca.pem``.
951+
* ``pkcs7/signed-opaque.msg``- A PKCS7 signed message, signed using opaque
952+
signing (``application/pkcs7-mime`` content type), signed under the
953+
private key of ``x509/custom/ca/ca.pem``, ``x509/custom/ca/ca_key.pem``.
951954

952955
Custom OpenSSH Test Vectors
953956
~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/hazmat/primitives/asymmetric/serialization.rst

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,154 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
12671267
:returns bytes: The signed PKCS7 message.
12681268

12691269

1270+
.. function:: pkcs7_verify_der(data, content, certificate, options)
1271+
1272+
.. versionadded:: 45.0.0
1273+
1274+
.. doctest::
1275+
1276+
>>> from cryptography import x509
1277+
>>> from cryptography.hazmat.primitives import hashes, serialization
1278+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1279+
>>> cert = x509.load_pem_x509_certificate(ca_cert)
1280+
>>> key = serialization.load_pem_private_key(ca_key, None)
1281+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1282+
... b"data to sign"
1283+
... ).add_signer(
1284+
... cert, key, hashes.SHA256()
1285+
... ).sign(
1286+
... serialization.Encoding.DER, []
1287+
... )
1288+
>>> pkcs7.pkcs7_verify_der(signed, None, cert, [])
1289+
1290+
Deserialize and verify a DER-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
1291+
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
1292+
verification succeeds, does not return anything. If the verification fails, raises an exception.
1293+
1294+
:param data: The data, encoded in DER format.
1295+
:type data: bytes
1296+
1297+
:param content: if specified, the content to verify against the signed message. If the content
1298+
is not specified, the function will look for the content in the signed message.
1299+
:type data: bytes or None
1300+
1301+
:param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed
1302+
message.
1303+
1304+
:param options: A list of
1305+
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
1306+
this operation, no options are supported as of now.
1307+
1308+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1309+
PKCS7 data.
1310+
1311+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1312+
1313+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1314+
1315+
1316+
.. function:: pkcs7_verify_pem(data, content, certificate, options)
1317+
1318+
.. versionadded:: 45.0.0
1319+
1320+
.. doctest::
1321+
1322+
>>> from cryptography import x509
1323+
>>> from cryptography.hazmat.primitives import hashes, serialization
1324+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1325+
>>> cert = x509.load_pem_x509_certificate(ca_cert)
1326+
>>> key = serialization.load_pem_private_key(ca_key, None)
1327+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1328+
... b"data to sign"
1329+
... ).add_signer(
1330+
... cert, key, hashes.SHA256()
1331+
... ).sign(
1332+
... serialization.Encoding.PEM, []
1333+
... )
1334+
>>> pkcs7.pkcs7_verify_pem(signed, None, cert, [])
1335+
1336+
Deserialize and verify a PEM-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
1337+
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
1338+
verification succeeds, does not return anything. If the verification fails, raises an exception.
1339+
1340+
:param data: The data, encoded in PEM format.
1341+
:type data: bytes
1342+
1343+
:param content: if specified, the content to verify against the signed message. If the content
1344+
is not specified, the function will look for the content in the signed message.
1345+
:type data: bytes or None
1346+
1347+
:param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed
1348+
message.
1349+
1350+
:param options: A list of
1351+
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
1352+
this operation, no options are supported as of now.
1353+
1354+
:raises ValueError: If the PEM data does not have the PKCS7 tag.
1355+
1356+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1357+
PKCS7 data.
1358+
1359+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1360+
1361+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1362+
1363+
1364+
.. function:: pkcs7_verify_smime(data, content, certificate, options)
1365+
1366+
.. versionadded:: 45.0.0
1367+
1368+
.. doctest::
1369+
1370+
>>> from cryptography import x509
1371+
>>> from cryptography.hazmat.primitives import hashes, serialization
1372+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1373+
>>> cert = x509.load_pem_x509_certificate(ca_cert)
1374+
>>> key = serialization.load_pem_private_key(ca_key, None)
1375+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1376+
... b"data to sign"
1377+
... ).add_signer(
1378+
... cert, key, hashes.SHA256()
1379+
... ).sign(
1380+
... serialization.Encoding.SMIME, []
1381+
... )
1382+
>>> pkcs7.pkcs7_verify_smime(signed, None, cert, [])
1383+
1384+
Verify a PKCS7 signed message stored in a MIME message, by reading it, extracting the content
1385+
(if any) and signature, deserializing the signature and verifying it against the content. PKCS7
1386+
(or S/MIME) has multiple versions, but this supports a subset of :rfc:`5751`, also known as
1387+
S/MIME Version 3.2. If the verification succeeds, does not return anything. If the verification
1388+
fails, raises an exception.
1389+
1390+
:param data: The data, encoded in MIME format.
1391+
:type data: bytes
1392+
1393+
:param content: if specified, the content to verify against the signed message. If the content
1394+
is not specified, the function will look for the content in the MIME message and in the
1395+
signature.
1396+
:type data: bytes or None
1397+
1398+
:param certificate: A :class:`~cryptography.x509.Certificate` to verify against the signed
1399+
message.
1400+
1401+
:param options: A list of
1402+
:class:`~cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options`. For
1403+
this operation, no options are supported as of now.
1404+
1405+
:raises ValueError: If the MIME message is not a S/MIME signed message: content type is
1406+
different than ``multipart/signed`` or ``application/pkcs7-mime``.
1407+
1408+
:raises ValueError: If the MIME message is a malformed ``multipart/signed`` S/MIME message: not
1409+
multipart, or multipart with more than 2 parts (content & signature).
1410+
1411+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1412+
PKCS7 data.
1413+
1414+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1415+
1416+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1417+
12701418
.. class:: PKCS7EnvelopeBuilder
12711419

12721420
The PKCS7 envelope builder can create encrypted S/MIME messages,

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ def encrypt_and_serialize(
1919
encoding: serialization.Encoding,
2020
options: Iterable[pkcs7.PKCS7Options],
2121
) -> bytes: ...
22-
def sign_and_serialize(
23-
builder: pkcs7.PKCS7SignatureBuilder,
24-
encoding: serialization.Encoding,
25-
options: Iterable[pkcs7.PKCS7Options],
26-
) -> bytes: ...
2722
def decrypt_der(
2823
data: bytes,
2924
certificate: x509.Certificate,
@@ -42,6 +37,29 @@ def decrypt_smime(
4237
private_key: rsa.RSAPrivateKey,
4338
options: Iterable[pkcs7.PKCS7Options],
4439
) -> bytes: ...
40+
def sign_and_serialize(
41+
builder: pkcs7.PKCS7SignatureBuilder,
42+
encoding: serialization.Encoding,
43+
options: Iterable[pkcs7.PKCS7Options],
44+
) -> bytes: ...
45+
def verify_der(
46+
signature: bytes,
47+
content: bytes | None,
48+
certificate: x509.Certificate,
49+
options: Iterable[pkcs7.PKCS7Options],
50+
) -> None: ...
51+
def verify_pem(
52+
signature: bytes,
53+
content: bytes | None,
54+
certificate: x509.Certificate,
55+
options: Iterable[pkcs7.PKCS7Options],
56+
) -> None: ...
57+
def verify_smime(
58+
signature: bytes,
59+
content: bytes | None,
60+
certificate: x509.Certificate,
61+
options: Iterable[pkcs7.PKCS7Options],
62+
) -> None: ...
4563
def load_pem_pkcs7_certificates(
4664
data: bytes,
4765
) -> list[x509.Certificate]: ...

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ def sign(
186186
return rust_pkcs7.sign_and_serialize(self, encoding, options)
187187

188188

189+
pkcs7_verify_der = rust_pkcs7.verify_der
190+
pkcs7_verify_pem = rust_pkcs7.verify_pem
191+
pkcs7_verify_smime = rust_pkcs7.verify_smime
192+
193+
189194
class PKCS7EnvelopeBuilder:
190195
def __init__(
191196
self,
@@ -358,6 +363,35 @@ def _smime_signed_encode(
358363
return fp.getvalue()
359364

360365

366+
def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]:
367+
message = email.message_from_bytes(data)
368+
content_type = message.get_content_type()
369+
if content_type == "multipart/signed":
370+
payload = message.get_payload()
371+
if not isinstance(payload, list):
372+
raise ValueError(
373+
"Malformed multipart/signed message: must be multipart"
374+
)
375+
if not isinstance(payload[0], email.message.Message):
376+
raise ValueError(
377+
"Malformed multipart/signed message: first part (content) "
378+
"must be a MIME message"
379+
)
380+
if not isinstance(payload[1], email.message.Message):
381+
raise ValueError(
382+
"Malformed multipart/signed message: second part (signature) "
383+
"must be a MIME message"
384+
)
385+
return (
386+
bytes(payload[0].get_payload(decode=True)),
387+
bytes(payload[1].get_payload(decode=True)),
388+
)
389+
elif content_type == "application/pkcs7-mime":
390+
return None, bytes(message.get_payload(decode=True))
391+
else:
392+
raise ValueError("Not an S/MIME signed message")
393+
394+
361395
def _smime_enveloped_encode(data: bytes) -> bytes:
362396
m = email.message.Message()
363397
m.add_header("MIME-Version", "1.0")

0 commit comments

Comments
 (0)