Skip to content

Commit 794db04

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

File tree

9 files changed

+573
-6
lines changed

9 files changed

+573
-6
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Changelog
1212
``cryptography`` release.
1313
* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm,
1414
in addition to AES-128.
15+
* Added basic support for PKCS7 verification (including S/MIME 3.2) via
16+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_der`,
17+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_pem`, and
18+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_smime`.
1519

1620
.. _v44-0-0:
1721

docs/development/test-vectors.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,9 @@ Custom PKCS7 Test Vectors
887887
* ``pkcs7/enveloped-no-content.der``- A DER encoded PKCS7 file with
888888
enveloped data, without encrypted content, with key encrypted under the
889889
public key of ``x509/custom/ca/rsa_ca.pem``.
890+
* ``pkcs7/signed-opaque.msg``- A PKCS7 signed message, signed using opaque
891+
signing (``application/pkcs7-mime`` content type), signed under the
892+
private key of ``x509/custom/ca/ca.pem``, ``x509/custom/ca/ca_key.pem``.
890893

891894
Custom OpenSSH Test Vectors
892895
~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/hazmat/primitives/asymmetric/serialization.rst

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

12561256

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

12591407
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)