Skip to content

Commit e6ddf30

Browse files
committed
PKCS7 signing
handling PEM, DER, SMIME formats added tests & documentation accordingly doing assertions for now, to please mypy added more test coverage updated tests to avoid unsupported algorithm first failing code for certificate verification handling mixed types with Cow feat: functions now have optional keyword arguments certificate is now optional feat: handling RSA case feat: No signature parameter adapted tests accordingly fix: adapted docmentation fix: passed into Cow again coverage: added one test case one more test case changing the error message for clarity two more test cases handled loading the load_der func for all backends removed options for now first draft of smime extension policy added back changelog integrated built-in verifier using existing vectors :) minor doc modification removed old vectors
1 parent ceb8b2a commit e6ddf30

File tree

10 files changed

+782
-8
lines changed

10 files changed

+782
-8
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Changelog
2121
* Removed the deprecated ``CAST5``, ``SEED``, ``IDEA``, and ``Blowfish``
2222
classes from the cipher module. These are still available in
2323
:doc:`/hazmat/decrepit/index`.
24+
* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm,
25+
in addition to AES-128.
26+
* Added basic support for PKCS7 verification (including S/MIME 3.2) via
27+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_der`,
28+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_pem`, and
29+
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_smime`.
2430

2531
.. _v45-0-7:
2632

docs/development/test-vectors.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,9 @@ Custom PKCS7 Test Vectors
10081008
* ``pkcs7/non-ascii-san.pem`` - An invalid certificate adapted for S/MIME
10091009
signature & verification. It has an non-ASCII subject alternative name stored
10101010
as `rfc822Name`.
1011+
* ``pkcs7/signed-opaque.msg``- A PKCS7 signed message, signed using opaque
1012+
signing (``application/pkcs7-mime`` content type), signed under the
1013+
private key of ``x509/custom/ca/ca.pem``, ``x509/custom/ca/ca_key.pem``.
10111014

10121015
Custom OpenSSH Test Vectors
10131016
~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/hazmat/primitives/asymmetric/serialization.rst

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,28 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
12621262
-----END PRIVATE KEY-----
12631263
""".strip()
12641264

1265+
verify_cert = b"""
1266+
-----BEGIN CERTIFICATE-----
1267+
MIIBhjCCASygAwIBAgICAwkwCgYIKoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAW
1268+
BgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEw
1269+
MTAwMDAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD
1270+
QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N
1271+
2CxSJE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjRjBE
1272+
MCEGA1UdEQEB/wQXMBWBE2V4YW1wbGVAZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAU
1273+
/Ou02BLyyT2Zwzxn9H03feYT7fowCgYIKoZIzj0EAwIDSAAwRQIgUwIdC0Emkd6f
1274+
17DeOXTlmTAhwSDJ2FTuyHESwei7wJcCIQCnr9NpBxbtJfEzxHGGyd7PxgpOLi5u
1275+
rk+8QfzGMmg/fw==
1276+
-----END CERTIFICATE-----
1277+
""".strip()
1278+
1279+
verify_key = b"""
1280+
-----BEGIN PRIVATE KEY-----
1281+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe
1282+
jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs
1283+
UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF
1284+
-----END PRIVATE KEY-----
1285+
""".strip()
1286+
12651287
.. class:: PKCS7SignatureBuilder
12661288

12671289
The PKCS7 signature builder can create both basic PKCS7 signed messages as
@@ -1340,6 +1362,150 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
13401362
:returns bytes: The signed PKCS7 message.
13411363

13421364

1365+
.. function:: pkcs7_verify_der(data, content=None, certificate=None, options=None)
1366+
1367+
.. versionadded:: 45.0.0
1368+
1369+
.. doctest::
1370+
1371+
>>> from cryptography import x509
1372+
>>> from cryptography.hazmat.primitives import hashes, serialization
1373+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1374+
>>> cert = x509.load_pem_x509_certificate(verify_cert)
1375+
>>> key = serialization.load_pem_private_key(verify_key, None)
1376+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1377+
... b"data to sign"
1378+
... ).add_signer(
1379+
... cert, key, hashes.SHA256()
1380+
... ).sign(
1381+
... serialization.Encoding.DER, []
1382+
... )
1383+
>>> pkcs7.pkcs7_verify_der(signed)
1384+
1385+
Deserialize and verify a DER-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
1386+
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
1387+
verification succeeds, does not return anything. If the verification fails, raises an exception.
1388+
1389+
:param data: The data, encoded in DER format.
1390+
:type data: bytes
1391+
1392+
:param content: if specified, the content to verify against the signed message. If the content
1393+
is not specified, the function will look for the content in the signed message. Defaults to
1394+
None.
1395+
:type content: bytes or None
1396+
1397+
:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
1398+
the signed message. If None, the function will look for the signer certificate in the signed
1399+
message. Defaults to None.
1400+
:type certificate: :class:`~cryptography.x509.Certificate` or None
1401+
1402+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1403+
PKCS7 data.
1404+
1405+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1406+
1407+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1408+
1409+
1410+
.. function:: pkcs7_verify_pem(data, content=None, certificate=None, options=None)
1411+
1412+
.. versionadded:: 45.0.0
1413+
1414+
.. doctest::
1415+
1416+
>>> from cryptography import x509
1417+
>>> from cryptography.hazmat.primitives import hashes, serialization
1418+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1419+
>>> cert = x509.load_pem_x509_certificate(verify_cert)
1420+
>>> key = serialization.load_pem_private_key(verify_key, None)
1421+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1422+
... b"data to sign"
1423+
... ).add_signer(
1424+
... cert, key, hashes.SHA256()
1425+
... ).sign(
1426+
... serialization.Encoding.PEM, []
1427+
... )
1428+
>>> pkcs7.pkcs7_verify_pem(signed)
1429+
1430+
Deserialize and verify a PEM-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
1431+
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
1432+
verification succeeds, does not return anything. If the verification fails, raises an exception.
1433+
1434+
:param data: The data, encoded in PEM format.
1435+
:type data: bytes
1436+
1437+
:param content: if specified, the content to verify against the signed message. If the content
1438+
is not specified, the function will look for the content in the signed message. Defaults to
1439+
None.
1440+
:type content: bytes or None
1441+
1442+
:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
1443+
the signed message. If None, the function will look for the signer certificate in the signed
1444+
message. Defaults to None.
1445+
:type certificate: :class:`~cryptography.x509.Certificate` or None
1446+
1447+
:raises ValueError: If the PEM data does not have the PKCS7 tag.
1448+
1449+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1450+
PKCS7 data.
1451+
1452+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1453+
1454+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1455+
1456+
1457+
.. function:: pkcs7_verify_smime(data, content=None, certificate=None, options=None)
1458+
1459+
.. versionadded:: 45.0.0
1460+
1461+
.. doctest::
1462+
1463+
>>> from cryptography import x509
1464+
>>> from cryptography.hazmat.primitives import hashes, serialization
1465+
>>> from cryptography.hazmat.primitives.serialization import pkcs7
1466+
>>> cert = x509.load_pem_x509_certificate(verify_cert)
1467+
>>> key = serialization.load_pem_private_key(verify_key, None)
1468+
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
1469+
... b"data to sign"
1470+
... ).add_signer(
1471+
... cert, key, hashes.SHA256()
1472+
... ).sign(
1473+
... serialization.Encoding.SMIME, []
1474+
... )
1475+
>>> pkcs7.pkcs7_verify_smime(signed)
1476+
1477+
Verify a PKCS7 signed message stored in a MIME message, by reading it, extracting the content
1478+
(if any) and signature, deserializing the signature and verifying it against the content. PKCS7
1479+
(or S/MIME) has multiple versions, but this supports a subset of :rfc:`5751`, also known as
1480+
S/MIME Version 3.2. If the verification succeeds, does not return anything. If the verification
1481+
fails, raises an exception.
1482+
1483+
:param data: The data, encoded in MIME format.
1484+
:type data: bytes
1485+
1486+
:param content: if specified, the content to verify against the signed message. If the content
1487+
is not specified, the function will look for the content in the MIME message and in the
1488+
signature. Defaults to None.
1489+
:type content: bytes or None
1490+
1491+
:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
1492+
the signed message. If None, the function will look for the signer certificate in the signed
1493+
message. Defaults to None.
1494+
:type certificate: :class:`~cryptography.x509.Certificate` or None
1495+
1496+
:raises ValueError: If the MIME message is not a S/MIME signed message: content type is
1497+
different than ``multipart/signed`` or ``application/pkcs7-mime``.
1498+
1499+
:raises ValueError: If the MIME message is a malformed ``multipart/signed`` S/MIME message: not
1500+
multipart, or multipart with more than 2 parts (content & signature).
1501+
1502+
:raises ValueError: If the recipient certificate does not match any of the signers in the
1503+
PKCS7 data.
1504+
1505+
:raises ValueError: If no content is specified and no content is found in the PKCS7 data.
1506+
1507+
:raises ValueError: If the PKCS7 data is not of the signed data type.
1508+
13431509
.. class:: PKCS7EnvelopeBuilder
13441510

13451511
The PKCS7 envelope builder can create encrypted S/MIME messages,
@@ -1633,6 +1799,7 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
16331799
obtain the signer's certificate by other means (for example from a
16341800
previously signed message).
16351801

1802+
16361803
Serialization Formats
16371804
~~~~~~~~~~~~~~~~~~~~~
16381805

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

Lines changed: 20 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,26 @@ 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 = None,
48+
certificate: x509.Certificate | None = None,
49+
) -> None: ...
50+
def verify_pem(
51+
signature: bytes,
52+
content: bytes | None = None,
53+
certificate: x509.Certificate | None = None,
54+
) -> None: ...
55+
def verify_smime(
56+
signature: bytes,
57+
content: bytes | None = None,
58+
certificate: x509.Certificate | None = None,
59+
) -> None: ...
4560
def load_pem_pkcs7_certificates(
4661
data: bytes,
4762
) -> list[x509.Certificate]: ...

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
Criticality,
2828
ExtensionPolicy,
2929
Policy,
30+
PolicyBuilder,
31+
Store,
3032
)
3133

3234
load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates
@@ -307,6 +309,11 @@ def sign(
307309
return rust_pkcs7.sign_and_serialize(self, encoding, options)
308310

309311

312+
pkcs7_verify_der = rust_pkcs7.verify_der
313+
pkcs7_verify_pem = rust_pkcs7.verify_pem
314+
pkcs7_verify_smime = rust_pkcs7.verify_smime
315+
316+
310317
class PKCS7EnvelopeBuilder:
311318
def __init__(
312319
self,
@@ -479,6 +486,45 @@ def _smime_signed_encode(
479486
return fp.getvalue()
480487

481488

489+
def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]:
490+
message = email.message_from_bytes(data)
491+
content_type = message.get_content_type()
492+
if content_type == "multipart/signed":
493+
payload = message.get_payload()
494+
if not isinstance(payload, list):
495+
raise ValueError(
496+
"Malformed multipart/signed message: must be multipart"
497+
)
498+
assert isinstance(payload[0], email.message.Message), (
499+
"Malformed multipart/signed message: first part (content) "
500+
"must be a MIME message"
501+
)
502+
assert isinstance(payload[1], email.message.Message), (
503+
"Malformed multipart/signed message: second part (signature) "
504+
"must be a MIME message"
505+
)
506+
return (
507+
bytes(payload[0].get_payload(decode=True)),
508+
bytes(payload[1].get_payload(decode=True)),
509+
)
510+
elif content_type == "application/pkcs7-mime":
511+
return None, bytes(message.get_payload(decode=True))
512+
else:
513+
raise ValueError("Not an S/MIME signed message")
514+
515+
516+
def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None:
517+
ca_policy, ee_policy = pkcs7_x509_extension_policies()
518+
verifier = (
519+
PolicyBuilder()
520+
.store(Store(certificates))
521+
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
522+
.build_client_verifier()
523+
)
524+
525+
verifier.verify(certificates[0], certificates[1:])
526+
527+
482528
def _smime_enveloped_encode(data: bytes) -> bytes:
483529
m = email.message.Message()
484530
m.add_header("MIME-Version", "1.0")

0 commit comments

Comments
 (0)