Skip to content
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

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

Open
wants to merge 6 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
2 changes: 2 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,8 @@ 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`` .

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

load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates

Expand Down Expand Up @@ -53,6 +59,108 @@ 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. 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 - TODO: is there any?
ca_policy = ExtensionPolicy.webpki_defaults_ca()

# EE policy
def _validate_basic_constraints(
policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None
) -> None:
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:
if ku is not None:
# Content commitment used to be named non repudiation
if not ku.digital_signature or 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:
if eku is not None:
ep = x509.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku # type: ignore[attr-defined]
aeku = x509.ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE in eku # type: ignore[attr-defined]
if not (ep or aeku):
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
118 changes: 116 additions & 2 deletions tests/hazmat/primitives/test_pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
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.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 +130,129 @@ 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:
def test_verify_pkcs7_certificate(self):
certificate, _ = _load_cert_key()
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()

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

@pytest.fixture(name="certificate_builder")
def fixture_certificate_builder(self) -> x509.CertificateBuilder:
certificate, private_key = _load_cert_key()
return (
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)
)

def test_verify_pkcs7_certificate_wrong_bc(self, certificate_builder):
certificate, private_key = _load_cert_key()

# Add an invalid extension
extension = x509.BasicConstraints(ca=True, path_length=None)
certificate_builder = certificate_builder.add_extension(
extension, True
)

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

# Verify the certificate
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)

def test_verify_pkcs7_certificate_wrong_ku(self, certificate_builder):
certificate, private_key = _load_cert_key()

# Add an invalid extension
extension = x509.KeyUsage(
digital_signature=False,
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(
extension, True
)

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

# Verify the certificate
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)

def test_verify_pkcs7_certificate_wrong_eku(self, certificate_builder):
certificate, private_key = _load_cert_key()

# Add an invalid extension
usages = [x509.ExtendedKeyUsageOID.CLIENT_AUTH] # type: ignore[attr-defined]
extension = x509.ExtendedKeyUsage(usages)
certificate_builder = certificate_builder.add_extension(
extension, True
)

# Add an invalid extension
usages = [x509.ExtendedKeyUsageOID.CLIENT_AUTH] # type: ignore[attr-defined]
extension = x509.ExtendedKeyUsage(usages)
certificate_builder = certificate_builder.add_extension(
extension, True
)

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

# Verify the certificate
self.verify_invalid_pkcs7_certificate(pkcs7_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, ee_policy)
.build_client_verifier()
)

with pytest.raises(VerificationError):
verifier.verify(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-----
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-----
Loading