Skip to content

PKCS#7 signing & verification - Certificate extension policies #12465

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 2 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
6 changes: 6 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,12 @@ 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/ca.pem`` - A certificate adapted for S/MIME signature & verification.
Its private key is ``pkcs7/ca_key.pem`` .
* ``pkcs7/ca_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature
& verification. It has an ASCII subject alternative name stored as `otherName`.
* ``pkcs7/ca_non_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature
& verification. It has an non-ASCII subject alternative name stored as `rfc822Name`.

Custom OpenSSH Test Vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
121 changes: 121 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,13 @@
algorithms,
)
from cryptography.utils import _check_byteslike
from cryptography.x509 import Certificate
from cryptography.x509.oid import ExtendedKeyUsageOID
from cryptography.x509.verification import (
Criticality,
ExtensionPolicy,
Policy,
)

load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates

Expand Down Expand Up @@ -53,6 +60,120 @@ class PKCS7Options(utils.Enum):
NoCerts = "Don't embed signer certificate"


def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]:
"""
Gets the default X.509 extension policy for S/MIME, based on RFC 8550.
Visit https://www.rfc-editor.org/rfc/rfc8550#section-4.4 for more info.
"""
# CA policy
ca_policy = ExtensionPolicy.webpki_defaults_ca()

# EE policy
def _validate_basic_constraints(
policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None
) -> None:
"""
We check that Certificates used as EE (i.e., the cert used to sign
a PKCS#7/SMIME message) must not have ca=true in their basic
constraints extension. RFC 5280 doesn't impose this requirement, but we
firmly agree about it being best practice.
"""
if bc is not None and bc.ca:
raise ValueError("Basic Constraints CA must be False.")

def _validate_key_usage(
policy: Policy, cert: Certificate, ku: x509.KeyUsage | None
) -> None:
"""
Checks that the Key Usage extension, if present, has at least one of
the digital signature or content commitment (formerly non-repudiation)
bits set.
"""
if (
ku is not None
and not ku.digital_signature
and not ku.content_commitment
):
raise ValueError(
"Key Usage, if specified, must have at least one of the "
"digital signature or content commitment (formerly non "
"repudiation) bits set."
)

def _validate_subject_alternative_name(
policy: Policy,
cert: Certificate,
san: x509.SubjectAlternativeName,
) -> None:
"""
For each general name in the SAN, for those which are email addresses:
- If it is an RFC822Name, general part must be ascii.
- If it is an OtherName, general part must be non-ascii.
"""
for general_name in san:
if (
isinstance(general_name, x509.RFC822Name)
and "@" in general_name.value
and not general_name.value.split("@")[0].isascii()
):
raise ValueError(
f"RFC822Name {general_name.value} contains non-ASCII "
"characters."
)
if (
isinstance(general_name, x509.OtherName)
and "@" in general_name.value.decode()
and general_name.value.decode().split("@")[0].isascii()
):
raise ValueError(
f"OtherName {general_name.value.decode()} is ASCII, "
"so must be stored in RFC822Name."
)

def _validate_extended_key_usage(
policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage | None
) -> None:
"""
Checks that the Extended Key Usage extension, if present,
includes either emailProtection or anyExtendedKeyUsage bits.
"""
if (
eku is not None
and ExtendedKeyUsageOID.EMAIL_PROTECTION not in eku
and ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE not in eku
):
raise ValueError(
"Extended Key Usage, if specified, must include "
"emailProtection or anyExtendedKeyUsage."
)

ee_policy = (
ExtensionPolicy.webpki_defaults_ee()
.may_be_present(
x509.BasicConstraints,
Criticality.AGNOSTIC,
_validate_basic_constraints,
)
.may_be_present(
x509.KeyUsage,
Criticality.CRITICAL,
_validate_key_usage,
)
.require_present(
x509.SubjectAlternativeName,
Criticality.AGNOSTIC,
_validate_subject_alternative_name,
)
.may_be_present(
x509.ExtendedKeyUsage,
Criticality.AGNOSTIC,
_validate_extended_key_usage,
)
)

return ca_policy, ee_policy


class PKCS7SignatureBuilder:
def __init__(
self,
Expand Down
147 changes: 145 additions & 2 deletions tests/hazmat/primitives/test_pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.serialization import pkcs7
from cryptography.x509.oid import (
ExtendedKeyUsageOID,
ExtensionOID,
ObjectIdentifier,
)
from cryptography.x509.verification import (
PolicyBuilder,
Store,
VerificationError,
)
from tests.x509.test_x509 import _generate_ca_and_leaf

from ...hazmat.primitives.fixtures_rsa import (
Expand Down Expand Up @@ -125,20 +135,153 @@ def test_load_pkcs7_empty_certificates(self):

def _load_cert_key():
key = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "ca_key.pem"),
os.path.join("pkcs7", "ca_key.pem"),
lambda pemfile: serialization.load_pem_private_key(
pemfile.read(), None, unsafe_skip_rsa_key_validation=True
),
mode="rb",
)
cert = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "ca.pem"),
os.path.join("pkcs7", "ca.pem"),
loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()),
mode="rb",
)
return cert, key


class TestPKCS7VerifyCertificate:
@staticmethod
def build_pkcs7_certificate(
ca: bool = False,
digital_signature: bool = True,
usages: typing.Optional[typing.List[ObjectIdentifier]] = None,
) -> x509.Certificate:
"""
This static method is a helper to build certificates allowing us
to test all cases in PKCS#7 certificate verification.
"""
# Load the standard certificate and private key
certificate, private_key = _load_cert_key()

# Basic certificate builder
certificate_builder = (
x509.CertificateBuilder()
.serial_number(certificate.serial_number)
.subject_name(certificate.subject)
.issuer_name(certificate.issuer)
.public_key(private_key.public_key())
.not_valid_before(certificate.not_valid_before)
.not_valid_after(certificate.not_valid_after)
)

# Add AuthorityKeyIdentifier extension
aki = certificate.extensions.get_extension_for_oid(
ExtensionOID.AUTHORITY_KEY_IDENTIFIER
)
certificate_builder = certificate_builder.add_extension(
aki.value, critical=False
)

# Add SubjectAlternativeName extension
san = certificate.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
certificate_builder = certificate_builder.add_extension(
san.value, critical=True
)

# Add BasicConstraints extension
bc_extension = x509.BasicConstraints(ca=ca, path_length=None)
certificate_builder = certificate_builder.add_extension(
bc_extension, False
)

# Add KeyUsage extension
ku_extension = x509.KeyUsage(
digital_signature=digital_signature,
content_commitment=False,
key_encipherment=True,
data_encipherment=True,
key_agreement=True,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False,
)
certificate_builder = certificate_builder.add_extension(
ku_extension, True
)

# Add valid ExtendedKeyUsage extension
usages = usages or [ExtendedKeyUsageOID.EMAIL_PROTECTION]
certificate_builder = certificate_builder.add_extension(
x509.ExtendedKeyUsage(usages), True
)

# Build the certificate
return certificate_builder.sign(
private_key, certificate.signature_hash_algorithm, None
)

def test_verify_pkcs7_certificate(self):
# Prepare the parameters
certificate = self.build_pkcs7_certificate()
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()

# Verify the certificate
verifier = (
PolicyBuilder()
.store(Store([certificate]))
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
.build_client_verifier()
)
verifier.verify(certificate, [])

@pytest.mark.parametrize(
"arguments",
[
{"ca": True},
{"digital_signature": False},
{"usages": [ExtendedKeyUsageOID.CLIENT_AUTH]},
],
)
def test_verify_invalid_pkcs7_certificate(self, arguments: dict):
# Prepare the parameters
certificate = self.build_pkcs7_certificate(**arguments)

# Verify the certificate
self.verify_invalid_pkcs7_certificate(certificate)

@staticmethod
def verify_invalid_pkcs7_certificate(certificate: x509.Certificate):
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()
verifier = (
PolicyBuilder()
.store(Store([certificate]))
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
.build_client_verifier()
)

with pytest.raises(VerificationError):
verifier.verify(certificate, [])

@pytest.mark.parametrize(
"filename", ["ca_non_ascii_san.pem", "ca_ascii_san.pem"]
)
def test_verify_pkcs7_certificate_wrong_san(self, filename):
# Read a certificate with an invalid SAN
pkcs7_certificate = load_vectors_from_file(
os.path.join("pkcs7", filename),
loader=lambda pemfile: x509.load_pem_x509_certificate(
pemfile.read()
),
mode="rb",
)

# Verify the certificate
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)


@pytest.mark.supported(
only_if=lambda backend: backend.pkcs7_supported(),
skip_message="Requires OpenSSL with PKCS7 support",
Expand Down
11 changes: 11 additions & 0 deletions vectors/cryptography_vectors/pkcs7/ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBhjCCASygAwIBAgICAwkwCgYIKoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAW
BgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEw
MTAwMDAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD
QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N
2CxSJE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjRjBE
MCEGA1UdEQEB/wQXMBWBE2V4YW1wbGVAZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAU
/Ou02BLyyT2Zwzxn9H03feYT7fowCgYIKoZIzj0EAwIDSAAwRQIgUwIdC0Emkd6f
17DeOXTlmTAhwSDJ2FTuyHESwei7wJcCIQCnr9NpBxbtJfEzxHGGyd7PxgpOLi5u
rk+8QfzGMmg/fw==
-----END CERTIFICATE-----
23 changes: 23 additions & 0 deletions vectors/cryptography_vectors/pkcs7/ca_ascii_san.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID3DCCAsSgAwIBAgIUGJw032ss5tmRmaY8x41pL5lqqRYwDQYJKoZIhvcNAQEL
BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN
SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTg0
NzQ1WhcNMjYwNjA5MTg0NzQ1WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs
aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs
ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLWXuy3atOjhb8g
fa5AC5me9PqRqcqV63e+NIe8IaKioCM5Sl+3jhKb5DdPIjfQYbHbwPtY+rFSP364
dBZoJpCDG4gcD6H3eS5JGc8Uz62l+oBNuFoU3EZiUNMF0k17vs/6CGeyt53+D9DJ
PG6Wv87nAAoK97r1rLdC8Of97QpUV/st+YDP7/LOH8CxJZOnbiUdekzo0dCQkk7n
17hJCYN1Y98VrlZFY25ny2TURUgK7lIjduEUb0dugYiepjzp7ZV8184kpAD/PtLT
czA1S8e6kySd5wbJSFcKxrk/j/cccUGLMyKPlMZgsHZUm/2DOLWLljxbEjCOxb1G
8+EpR9kCAwEAAaNQME4wLQYDVR0RBCYwJKAiBggrBgEFBQcICaAWDBRyZXRvdXJu
ZUBleGFtcGxlLmNvbTAdBgNVHQ4EFgQUm24AOQAmOInCPZPDUagXXw+BEl0wDQYJ
KoZIhvcNAQELBQADggEBAGgLqsx27sS28t1okxT1MU6QhfAn/Yw07Nhk3cpNKGnh
edrPPTXvJc05qHuQIqOiFIJ4SojbQ2+bVZwo7V3Jhspx9T+Gkb/Dn3rHpAfOXuaJ
RqJ777Cor2seAKv07jerGnEULYW8JcezZDGbv6ViC0oEgazwTzahfynrUMJ2DJRX
tnNdczDsGw+DVMvOBzcSE/aEzhd4ghgVq5aFS05wzhN/fTWKiN4tpEAG6y95gU73
29O3y1W3dLjblTZJvXNtgCjMT6R3OVeWAsqyXDprFrZWZucCj8opIxRf6jpZlRfJ
qW+57pkefhg3q4MFjn08BOKpYwOdRouGE4l96dGBDwM=
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions vectors/cryptography_vectors/pkcs7/ca_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe
jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs
UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF
-----END PRIVATE KEY-----
23 changes: 23 additions & 0 deletions vectors/cryptography_vectors/pkcs7/ca_non_ascii_san.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDzzCCAregAwIBAgIUAX/xKTtlMllrK5ng0+OkmnxxIugwDQYJKoZIhvcNAQEL
BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN
SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTgw
NzE4WhcNMjYwNjA5MTgwNzE4WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs
aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs
ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOxyV/ZsaGn7dOcZ
6ODFcnmwjPCKRASFeDtOMYoGrlALb9zA+UMuMB63dTZ8ofWsDgLLGhw86njfSYad
RslOw8Bki9lKiS1RhS/RbnDSBWB2wJzniyFn/qI2F93WbgqHMOnzzJcAkc/YPU0T
iyvNpjD3Q/xObcp7ouBJJmFSvLybSTJtFrVzkpIbDZYrn0KyKtgTCPc/r9D04u+u
scSACvTRjePsEZIgRkVgfVpdBmy1KeJmx2NqS8Yev+y+0e9q3t8Ga/j/CnPFXlEl
iBHciFtkKdd2HrPLJMXBKhMn2KagLJSSdABNApi8qULIpOnrEE8FepKCzkptFyS1
5g0H3u0CAwEAAaNDMEEwIAYDVR0RBBkwF4EVcmV0b3VybsOpQGV4YW1wbGUuY29t
MB0GA1UdDgQWBBTthtqdM0IoehNymXnqMPX1joF1LzANBgkqhkiG9w0BAQsFAAOC
AQEApQZ3vOuBgNg1U26c4l0VSCU5q73Lecbgjc42AhEp9FyP7ratj4MyH7RGr4io
vl0wWROFBnzliW5ZA8CP3Ux4AbqgtxcFPBRHACjmrpoSFHmW7bpzRnqwJKwXsOGJ
ZhjA/2o91lEJr0UNhpvSGyR+xCkuvw83mvM1rmE19yNMElv96x/DPVQV2ocsffOb
kS7pIpvXX3pSIj7Up0Xrz+bSyhJlsO3sO5bREshyvuiRivm9AjBVRY/BtbFY6DcV
9javEitCw93BgImIs0CXGpZUrvphX8muWVct5xpKj64/Yo0hIYystX+xVl3EjTRf
B7pH2DE+cXg99p7L6RoYtlOeRA==
-----END CERTIFICATE-----
Loading