Skip to content

Add support for verifying S/MIME messages #12267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ Changelog
:meth:`VerifiedClient.subject <cryptography.x509.verification.VerifiedClient.subjects>`
property can now be `None` since a custom extension policy may allow certificates
without a Subject Alternative Name extension.
* Added support for PKCS7 decryption & encryption using AES-256 as content algorithm,
in addition to AES-128.
* Added basic support for PKCS7 verification (including S/MIME 3.2) via
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_der`,
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_pem`, and
:func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_verify_smime`.


.. _v44-0-1:

Expand Down
3 changes: 3 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,9 @@ Custom PKCS7 Test Vectors
* ``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``.
* ``pkcs7/signed-opaque.msg``- A PKCS7 signed message, signed using opaque
signing (``application/pkcs7-mime`` content type), signed under the
private key of ``x509/custom/ca/ca.pem``, ``x509/custom/ca/ca_key.pem``.

Custom OpenSSH Test Vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
202 changes: 202 additions & 0 deletions docs/hazmat/primitives/asymmetric/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,63 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
-----END PRIVATE KEY-----
""".strip()

verify_cert = b"""
-----BEGIN CERTIFICATE-----
MIID9zCCAt+gAwIBAgIQIxMA+XhyS9Ou0qAc0zPyVTANBgkqhkiG9w0BAQsFADAN
MQswCQYDVQQDDAJDQTAeFw0yNTAxMDUxMDQ4MjhaFw0yNjAxMDUxMDQ4MjhaMCUx
IzAhBgkqhkiG9w0BCQEWFGRlbW8xQHRyaXNvZnQuY29tLnBsMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre
3L8zkuGYVLCKBEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCs
BTL/EmnD3ZMAJVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6d
Hd3uVR7NSs98uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNY
nYhaOBLpUhZckyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1
MRYtgcsOTxpGf+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABo4IBOTCC
ATUwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQduUy7zqv6z3uk4fJeifohSntD
2TAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vY2EudHJpc29mdC5jb20ucGwvY3Js
MGYGCCsGAQUFBwEBBFowWDArBggrBgEFBQcwAoYfaHR0cDovL2NhLnRyaXNvZnQu
Y29tLnBsL2NhY2VydDApBggrBgEFBQcwAYYdaHR0cDovL2NhLnRyaXNvZnQuY29t
LnBsL29jc3AwHwYDVR0RBBgwFoEUZGVtbzFAdHJpc29mdC5jb20ucGwwHQYDVR0l
BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBT0/QFDFX/CCMsX356G
ImiWwPYxjDAOBgNVHQ8BAf8EBAMCA+gwDQYJKoZIhvcNAQELBQADggEBAL3Iisca
IqoFBLMox3cIhCANWO/U1eOvjDjfM/tOHn+6jci/pL/ZHgdRtqCCiaCKtJED/f/9
NFUKqcSZ9+vzW0RWLJxHgIvCSjLpoM06XClSlxjVnv62Hb1NC4FfDfnzyG+DZHus
nz/MQuXNwHntA6+JyB/HWHUie2ierQYH2mEN1XIJm5luSGwtuGaWfNz/w324ukcV
pMd3CbEOZqqfSYGWUHOVG90/OMSfKA/I0hia8Yij0X4Ny+b+bLnHaoozZwJ/UqBl
9ptbfiOOuFXJP7gt547Rp6+2C0XGJM+le0EYlUzbWE6UWgxaIRp5uc8HnUd5e4lX
br+Ixxcl3WHckkk=
-----END CERTIFICATE-----
""".strip()

verify_key = b"""
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAt0WRzh5y+QmEUjCm+iHXZLrstOSSEhiEcUre3L8zkuGYVLCK
BEvmaHQI7uCu/xdqEht6/wEBCiK+KLdGDVrD4v3A7TnmHzzhvqCsBTL/EmnD3ZMA
JVYv4uEBaFpFPSYnPswd353E6KRkFYR4RmFjG9xLTayHXOKqCF6dHd3uVR7NSs98
uhcSYRV7g4NdjmaDj8kz5HeRMfr/uqbcriJ9tu/ljFBWYSwPeiNYnYhaOBLpUhZc
kyjFDfC+UpwOBPlkK7J047urvzG21xCtVU9DMHtXMkXYe/C+WSm1MRYtgcsOTxpG
f+ujceltI2/+IUhWxr5ys7m+xM1jYaM4O1Pw0QIDAQABAoIBAEiVCdiq4HfWmAwA
7rBTZL2k9gfyGhOGmDVSJI8iPiemprCrtg1bjeXCRqNsYoHuYPjI315MpH/CILN5
WgoB72BfhN+utX+bmf/oHBh3COPe9U40YLNovdBJskgEsDU2fgZ1ykL8dbZ5HJYU
/5lICntHNJ+Pe5CCyDpGVk00zqXwwBDV7hBhbPZxXqdRwdA49yyLIdw/IlMQph9A
zuJ0cyicQ0eFSFb1nCv/11hx3RyhfZvn/V3/F3BIP1gBipc3npldvCXhM4CjNYSe
tilKiqlYt2exD95RR7NdtL16UcRRCOblgGh23qjJOIb8N4dsr8xbeeCN3A69lILo
fgVs2J0CgYEA5noMFh9GFkZFhMIBFPhTlEn+VgWfwK9gWfcyy5GlVsMfp4UA+Alc
JSqz+0y1es2yoF0N4ckFsuZuh0GFZxFg46cE6WL1mO6NyzbND8VItQ3Mb2nsJiDC
xtJCiLqekfXudbmkNkmXleOIW16ZHorkgJADs0LDehGEGJh6lTxOc7MCgYEAy5FG
FGRHGncMyhkoyw6iZC+vmcpvoiu4HfKmTIPQDm6MGS6CxGU6BcX7IgPjdQkogY7s
UUP7lYnlvR2G8u4rOqrEMhjAsbudYSry24iAvcalT5lRYud2dh/8cpamfC9TrrUt
Zd/p8/lvkLTiF7j88QB6onFtm3seagma4hUJl2sCgYAzo8zpeABgJUaWRFGxvSIc
66dM5t2wcpsIDVcYPX3qPrXs9uQMrywyN6sz9zACX+xR+geOO1hHiVHihE+7lC09
VMLI+B9HMMwcaB7yFaYAyyKvI/CBan25xoqZ0BaPZacUQZAFid+o+d4ner6cFUq1
c48gryjVRO9wA1oT7fs1+QKBgBBzPOaI8/X/iNkMD2/ZTuYptFcJNNw2DDrfUPD9
9eI0rL2cNJUKWRX+Wbz183uRseRGWHJ4u+vpqNcPe8hF1th21EP4HBpAvwcLIXT8
IuszEkjMavdDHR+OlifsZKfEa07C9Vg2MAG3NnzLITopiMcw8rgN0n2uBVcsT4fV
i2DhAoGBAIJtHUe9e8oPrasRlZ3bTFmDT+jNg+7RB8ebG8ZDqAUI3/gnklUd0+rF
nPGI8GEpjwgBxB/zg4/rYz/TEP0E2pd0beWH2vKD31kQVngbz/zhzLHCNLyKDlB4
vFHpXRHb7ddgTLjHbg6GvY/pRRCqSxWnLgNRW4m+pyLzAx/Hpk1D
-----END RSA PRIVATE KEY-----
""".strip()

.. class:: PKCS7SignatureBuilder

The PKCS7 signature builder can create both basic PKCS7 signed messages as
Expand Down Expand Up @@ -1267,6 +1324,150 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
:returns bytes: The signed PKCS7 message.


.. function:: pkcs7_verify_der(data, content=None, certificate=None, options=None)

.. versionadded:: 45.0.0

.. doctest::

>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import hashes, serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(verify_cert)
>>> key = serialization.load_pem_private_key(verify_key, None)
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
... b"data to sign"
... ).add_signer(
... cert, key, hashes.SHA256()
... ).sign(
... serialization.Encoding.DER, []
... )
>>> pkcs7.pkcs7_verify_der(signed)

Deserialize and verify a DER-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
verification succeeds, does not return anything. If the verification fails, raises an exception.

:param data: The data, encoded in DER format.
:type data: bytes

:param content: if specified, the content to verify against the signed message. If the content
is not specified, the function will look for the content in the signed message. Defaults to
None.
:type content: bytes or None

:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
the signed message. If None, the function will look for the signer certificate in the signed
message. Defaults to None.
:type certificate: :class:`~cryptography.x509.Certificate` or None

:raises ValueError: If the recipient certificate does not match any of the signers in the
PKCS7 data.

:raises ValueError: If no content is specified and no content is found in the PKCS7 data.

:raises ValueError: If the PKCS7 data is not of the signed data type.


.. function:: pkcs7_verify_pem(data, content=None, certificate=None, options=None)

.. versionadded:: 45.0.0

.. doctest::

>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import hashes, serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(verify_cert)
>>> key = serialization.load_pem_private_key(verify_key, None)
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
... b"data to sign"
... ).add_signer(
... cert, key, hashes.SHA256()
... ).sign(
... serialization.Encoding.PEM, []
... )
>>> pkcs7.pkcs7_verify_pem(signed)

Deserialize and verify a PEM-encoded PKCS7 signed message. PKCS7 (or S/MIME) has multiple
versions, but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2. If the
verification succeeds, does not return anything. If the verification fails, raises an exception.

:param data: The data, encoded in PEM format.
:type data: bytes

:param content: if specified, the content to verify against the signed message. If the content
is not specified, the function will look for the content in the signed message. Defaults to
None.
:type content: bytes or None

:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
the signed message. If None, the function will look for the signer certificate in the signed
message. Defaults to None.
:type certificate: :class:`~cryptography.x509.Certificate` or None

:raises ValueError: If the PEM data does not have the PKCS7 tag.

:raises ValueError: If the recipient certificate does not match any of the signers in the
PKCS7 data.

:raises ValueError: If no content is specified and no content is found in the PKCS7 data.

:raises ValueError: If the PKCS7 data is not of the signed data type.


.. function:: pkcs7_verify_smime(data, content=None, certificate=None, options=None)

.. versionadded:: 45.0.0

.. doctest::

>>> from cryptography import x509
>>> from cryptography.hazmat.primitives import hashes, serialization
>>> from cryptography.hazmat.primitives.serialization import pkcs7
>>> cert = x509.load_pem_x509_certificate(verify_cert)
>>> key = serialization.load_pem_private_key(verify_key, None)
>>> signed = pkcs7.PKCS7SignatureBuilder().set_data(
... b"data to sign"
... ).add_signer(
... cert, key, hashes.SHA256()
... ).sign(
... serialization.Encoding.SMIME, []
... )
>>> pkcs7.pkcs7_verify_smime(signed)

Verify a PKCS7 signed message stored in a MIME message, by reading it, extracting the content
(if any) and signature, deserializing the signature and verifying it against the content. PKCS7
(or S/MIME) has multiple versions, but this supports a subset of :rfc:`5751`, also known as
S/MIME Version 3.2. If the verification succeeds, does not return anything. If the verification
fails, raises an exception.

:param data: The data, encoded in MIME format.
:type data: bytes

:param content: if specified, the content to verify against the signed message. If the content
is not specified, the function will look for the content in the MIME message and in the
signature. Defaults to None.
:type content: bytes or None

:param certificate: if specified, a :class:`~cryptography.x509.Certificate` to verify against
the signed message. If None, the function will look for the signer certificate in the signed
message. Defaults to None.
:type certificate: :class:`~cryptography.x509.Certificate` or None

:raises ValueError: If the MIME message is not a S/MIME signed message: content type is
different than ``multipart/signed`` or ``application/pkcs7-mime``.

:raises ValueError: If the MIME message is a malformed ``multipart/signed`` S/MIME message: not
multipart, or multipart with more than 2 parts (content & signature).

:raises ValueError: If the recipient certificate does not match any of the signers in the
PKCS7 data.

:raises ValueError: If no content is specified and no content is found in the PKCS7 data.

:raises ValueError: If the PKCS7 data is not of the signed data type.

.. class:: PKCS7EnvelopeBuilder

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


Serialization Formats
~~~~~~~~~~~~~~~~~~~~~

Expand Down
25 changes: 20 additions & 5 deletions src/cryptography/hazmat/bindings/_rust/pkcs7.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ def encrypt_and_serialize(
encoding: serialization.Encoding,
options: Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def sign_and_serialize(
builder: pkcs7.PKCS7SignatureBuilder,
encoding: serialization.Encoding,
options: Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def decrypt_der(
data: bytes,
certificate: x509.Certificate,
Expand All @@ -42,6 +37,26 @@ def decrypt_smime(
private_key: rsa.RSAPrivateKey,
options: Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def sign_and_serialize(
builder: pkcs7.PKCS7SignatureBuilder,
encoding: serialization.Encoding,
options: Iterable[pkcs7.PKCS7Options],
) -> bytes: ...
def verify_der(
signature: bytes,
content: bytes | None = None,
certificate: x509.Certificate | None = None,
) -> None: ...
def verify_pem(
signature: bytes,
content: bytes | None = None,
certificate: x509.Certificate | None = None,
) -> None: ...
def verify_smime(
signature: bytes,
content: bytes | None = None,
certificate: x509.Certificate | None = None,
) -> None: ...
def load_pem_pkcs7_certificates(
data: bytes,
) -> list[x509.Certificate]: ...
Expand Down
92 changes: 92 additions & 0 deletions src/cryptography/hazmat/primitives/serialization/pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
algorithms,
)
from cryptography.utils import _check_byteslike
from cryptography.x509 import Certificate
from cryptography.x509.verification import (
Criticality,
ExtensionPolicy,
Policy,
PolicyBuilder,
Store,
)

load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates

Expand Down Expand Up @@ -186,6 +194,11 @@ def sign(
return rust_pkcs7.sign_and_serialize(self, encoding, options)


pkcs7_verify_der = rust_pkcs7.verify_der
pkcs7_verify_pem = rust_pkcs7.verify_pem
pkcs7_verify_smime = rust_pkcs7.verify_smime


class PKCS7EnvelopeBuilder:
def __init__(
self,
Expand Down Expand Up @@ -358,6 +371,85 @@ def _smime_signed_encode(
return fp.getvalue()


def _smime_signed_decode(data: bytes) -> tuple[bytes | None, bytes]:
message = email.message_from_bytes(data)
content_type = message.get_content_type()
if content_type == "multipart/signed":
payload = message.get_payload()
if not isinstance(payload, list):
raise ValueError(
"Malformed multipart/signed message: must be multipart"
)
assert isinstance(payload[0], email.message.Message), (
"Malformed multipart/signed message: first part (content) "
"must be a MIME message"
)
assert isinstance(payload[1], email.message.Message), (
"Malformed multipart/signed message: second part (signature) "
"must be a MIME message"
)
return (
bytes(payload[0].get_payload(decode=True)),
bytes(payload[1].get_payload(decode=True)),
)
elif content_type == "application/pkcs7-mime":
return None, bytes(message.get_payload(decode=True))
else:
raise ValueError("Not an S/MIME signed message")


def get_smime_x509_extension_policies() -> tuple[
ExtensionPolicy, ExtensionPolicy
]:
"""
Gets the default X.509 extension policy for S/MIME. Some specifications
that differ from the standard ones:
- Certificates used as end entities (i.e., the cert used to sign
a PKCS#7/SMIME message) should not have ca=true in their basic
constraints extension.
- EKU_CLIENT_AUTH_OID is not required
- EKU_EMAIL_PROTECTION_OID is required
"""

# CA policy
def _validate_ca(
policy: Policy, cert: Certificate, bc: x509.BasicConstraints
):
assert not bc.ca

ca_policy = ExtensionPolicy.permit_all().require_present(
x509.BasicConstraints,
Criticality.AGNOSTIC,
_validate_ca,
)

# EE policy
def _validate_eku(
policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage
):
# Checking for EKU_EMAIL_PROTECTION_OID
assert x509.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku # type: ignore[attr-defined]

ee_policy = ExtensionPolicy.permit_all().require_present(
x509.ExtendedKeyUsage,
Criticality.AGNOSTIC,
_validate_eku,
)

return ca_policy, ee_policy


def _verify_pkcs7_certificates(certificates: list[x509.Certificate]) -> None:
builder = (
PolicyBuilder()
.store(Store(certificates))
.extension_policies(*get_smime_x509_extension_policies())
)

verifier = builder.build_client_verifier()
verifier.verify(certificates[0], certificates[1:])


def _smime_enveloped_encode(data: bytes) -> bytes:
m = email.message.Message()
m.add_header("MIME-Version", "1.0")
Expand Down
Loading
Loading