Skip to content

Commit 64d84e1

Browse files
committed
feat: PKCS#7 extension policies
added tests accordingly adapted the pkcs7 certificate adapted EE policy do not know if a CA policy is needed! added SAN checking (incomplete though)
1 parent 7de19cc commit 64d84e1

File tree

5 files changed

+242
-2
lines changed

5 files changed

+242
-2
lines changed

docs/development/test-vectors.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,8 @@ Custom PKCS7 Test Vectors
10011001
* ``pkcs7/enveloped-no-content.der``- A DER encoded PKCS7 file with
10021002
enveloped data, without encrypted content, with key encrypted under the
10031003
public key of ``x509/custom/ca/rsa_ca.pem``.
1004+
* ``pkcs7/ca.pem`` - A certificate adapted for S/MIME signature & verification.
1005+
Its private key is ``pkcs7/ca_key.pem`` .
10041006

10051007
Custom OpenSSH Test Vectors
10061008
~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
algorithms,
2222
)
2323
from cryptography.utils import _check_byteslike
24+
from cryptography.x509 import Certificate
25+
from cryptography.x509.verification import (
26+
Criticality,
27+
ExtensionPolicy,
28+
Policy,
29+
)
2430

2531
load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates
2632

@@ -53,6 +59,108 @@ class PKCS7Options(utils.Enum):
5359
NoCerts = "Don't embed signer certificate"
5460

5561

62+
def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]:
63+
"""
64+
Gets the default X.509 extension policy for S/MIME. Some specifications
65+
that differ from the standard ones:
66+
- Certificates used as end entities (i.e., the cert used to sign
67+
a PKCS#7/SMIME message) should not have ca=true in their basic
68+
constraints extension.
69+
- EKU_CLIENT_AUTH_OID is not required
70+
- EKU_EMAIL_PROTECTION_OID is required
71+
"""
72+
73+
# CA policy - TODO: is there any?
74+
ca_policy = ExtensionPolicy.webpki_defaults_ca()
75+
76+
# EE policy
77+
def _validate_basic_constraints(
78+
policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None
79+
) -> None:
80+
if bc is not None and bc.ca:
81+
raise ValueError("Basic Constraints CA must be False.")
82+
83+
def _validate_key_usage(
84+
policy: Policy, cert: Certificate, ku: x509.KeyUsage | None
85+
) -> None:
86+
if ku is not None:
87+
# Content commitment used to be named non repudiation
88+
if not ku.digital_signature or ku.content_commitment:
89+
raise ValueError(
90+
"Key Usage, if specified, must have at least one of the "
91+
"digital signature or content commitment (formerly non "
92+
"repudiation) bits set."
93+
)
94+
95+
def _validate_subject_alternative_name(
96+
policy: Policy,
97+
cert: Certificate,
98+
san: x509.SubjectAlternativeName,
99+
) -> None:
100+
"""
101+
For each general name in the SAN, for those which are email addresses:
102+
- If it is an RFC822Name, general part must be ascii.
103+
- If it is an OtherName, general part must be non-ascii.
104+
"""
105+
for general_name in san:
106+
if (
107+
isinstance(general_name, x509.RFC822Name)
108+
and "@" in general_name.value
109+
and not general_name.value.split("@")[0].isascii()
110+
):
111+
raise ValueError(
112+
f"RFC822Name {general_name.value} contains non-ASCII "
113+
"characters."
114+
)
115+
if (
116+
isinstance(general_name, x509.OtherName)
117+
and "@" in general_name.value.decode()
118+
and general_name.value.decode().split("@")[0].isascii()
119+
):
120+
raise ValueError(
121+
f"OtherName {general_name.value.decode()} is ASCII, "
122+
"so must be stored in RFC822Name."
123+
)
124+
125+
def _validate_extended_key_usage(
126+
policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage | None
127+
) -> None:
128+
if eku is not None:
129+
ep = x509.ExtendedKeyUsageOID.EMAIL_PROTECTION in eku # type: ignore[attr-defined]
130+
aeku = x509.ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE in eku # type: ignore[attr-defined]
131+
if not (ep or aeku):
132+
raise ValueError(
133+
"Extended Key Usage, if specified, must include "
134+
"emailProtection or anyExtendedKeyUsage."
135+
)
136+
137+
ee_policy = (
138+
ExtensionPolicy.webpki_defaults_ee()
139+
.may_be_present(
140+
x509.BasicConstraints,
141+
Criticality.AGNOSTIC,
142+
_validate_basic_constraints,
143+
)
144+
.may_be_present(
145+
x509.KeyUsage,
146+
Criticality.CRITICAL,
147+
_validate_key_usage,
148+
)
149+
.require_present(
150+
x509.SubjectAlternativeName,
151+
Criticality.AGNOSTIC,
152+
_validate_subject_alternative_name,
153+
)
154+
.may_be_present(
155+
x509.ExtendedKeyUsage,
156+
Criticality.AGNOSTIC,
157+
_validate_extended_key_usage,
158+
)
159+
)
160+
161+
return ca_policy, ee_policy
162+
163+
56164
class PKCS7SignatureBuilder:
57165
def __init__(
58166
self,

tests/hazmat/primitives/test_pkcs7.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
1919
from cryptography.hazmat.primitives.ciphers import algorithms
2020
from cryptography.hazmat.primitives.serialization import pkcs7
21+
from cryptography.x509.verification import (
22+
PolicyBuilder,
23+
Store,
24+
VerificationError,
25+
)
2126
from tests.x509.test_x509 import _generate_ca_and_leaf
2227

2328
from ...hazmat.primitives.fixtures_rsa import (
@@ -125,20 +130,129 @@ def test_load_pkcs7_empty_certificates(self):
125130

126131
def _load_cert_key():
127132
key = load_vectors_from_file(
128-
os.path.join("x509", "custom", "ca", "ca_key.pem"),
133+
os.path.join("pkcs7", "ca_key.pem"),
129134
lambda pemfile: serialization.load_pem_private_key(
130135
pemfile.read(), None, unsafe_skip_rsa_key_validation=True
131136
),
132137
mode="rb",
133138
)
134139
cert = load_vectors_from_file(
135-
os.path.join("x509", "custom", "ca", "ca.pem"),
140+
os.path.join("pkcs7", "ca.pem"),
136141
loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()),
137142
mode="rb",
138143
)
139144
return cert, key
140145

141146

147+
class TestPKCS7VerifyCertificate:
148+
def test_verify_pkcs7_certificate(self):
149+
certificate, _ = _load_cert_key()
150+
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()
151+
152+
verifier = (
153+
PolicyBuilder()
154+
.store(Store([certificate]))
155+
.extension_policies(ca_policy, ee_policy)
156+
.build_client_verifier()
157+
)
158+
verifier.verify(certificate, [])
159+
160+
@pytest.fixture(name="certificate_builder")
161+
def fixture_certificate_builder(self) -> x509.CertificateBuilder:
162+
certificate, private_key = _load_cert_key()
163+
return (
164+
x509.CertificateBuilder()
165+
.serial_number(certificate.serial_number)
166+
.subject_name(certificate.subject)
167+
.issuer_name(certificate.issuer)
168+
.public_key(private_key.public_key())
169+
.not_valid_before(certificate.not_valid_before)
170+
.not_valid_after(certificate.not_valid_after)
171+
)
172+
173+
def test_verify_pkcs7_certificate_wrong_bc(self, certificate_builder):
174+
certificate, private_key = _load_cert_key()
175+
176+
# Add an invalid extension
177+
extension = x509.BasicConstraints(ca=True, path_length=None)
178+
certificate_builder = certificate_builder.add_extension(
179+
extension, True
180+
)
181+
182+
# Build the certificate
183+
pkcs7_certificate = certificate_builder.sign(
184+
private_key, certificate.signature_hash_algorithm, None
185+
)
186+
187+
# Verify the certificate
188+
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)
189+
190+
def test_verify_pkcs7_certificate_wrong_ku(self, certificate_builder):
191+
certificate, private_key = _load_cert_key()
192+
193+
# Add an invalid extension
194+
extension = x509.KeyUsage(
195+
digital_signature=False,
196+
content_commitment=False,
197+
key_encipherment=True,
198+
data_encipherment=True,
199+
key_agreement=True,
200+
key_cert_sign=True,
201+
crl_sign=True,
202+
encipher_only=False,
203+
decipher_only=False,
204+
)
205+
certificate_builder = certificate_builder.add_extension(
206+
extension, True
207+
)
208+
209+
# Build the certificate
210+
pkcs7_certificate = certificate_builder.sign(
211+
private_key, certificate.signature_hash_algorithm, None
212+
)
213+
214+
# Verify the certificate
215+
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)
216+
217+
def test_verify_pkcs7_certificate_wrong_eku(self, certificate_builder):
218+
certificate, private_key = _load_cert_key()
219+
220+
# Add an invalid extension
221+
usages = [x509.ExtendedKeyUsageOID.CLIENT_AUTH] # type: ignore[attr-defined]
222+
extension = x509.ExtendedKeyUsage(usages)
223+
certificate_builder = certificate_builder.add_extension(
224+
extension, True
225+
)
226+
227+
# Add an invalid extension
228+
usages = [x509.ExtendedKeyUsageOID.CLIENT_AUTH] # type: ignore[attr-defined]
229+
extension = x509.ExtendedKeyUsage(usages)
230+
certificate_builder = certificate_builder.add_extension(
231+
extension, True
232+
)
233+
234+
# Build the certificate
235+
pkcs7_certificate = certificate_builder.sign(
236+
private_key, certificate.signature_hash_algorithm, None
237+
)
238+
239+
# Verify the certificate
240+
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)
241+
242+
@staticmethod
243+
def verify_invalid_pkcs7_certificate(certificate: x509.Certificate):
244+
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()
245+
verifier = (
246+
PolicyBuilder()
247+
.store(Store([certificate]))
248+
.extension_policies(ca_policy, ee_policy)
249+
.build_client_verifier()
250+
)
251+
252+
with pytest.raises(VerificationError):
253+
verifier.verify(certificate, [])
254+
255+
142256
@pytest.mark.supported(
143257
only_if=lambda backend: backend.pkcs7_supported(),
144258
skip_message="Requires OpenSSL with PKCS7 support",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBhjCCASygAwIBAgICAwkwCgYIKoZIzj0EAwIwJzELMAkGA1UEBhMCVVMxGDAW
3+
BgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAgFw0xNzAxMDEwMTAwMDBaGA8yMTAwMDEw
4+
MTAwMDAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBD
5+
QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N
6+
2CxSJE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjRjBE
7+
MCEGA1UdEQEB/wQXMBWBE2V4YW1wbGVAZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAU
8+
/Ou02BLyyT2Zwzxn9H03feYT7fowCgYIKoZIzj0EAwIDSAAwRQIgUwIdC0Emkd6f
9+
17DeOXTlmTAhwSDJ2FTuyHESwei7wJcCIQCnr9NpBxbtJfEzxHGGyd7PxgpOLi5u
10+
rk+8QfzGMmg/fw==
11+
-----END CERTIFICATE-----
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe
3+
jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs
4+
UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF
5+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)