Skip to content

Commit db7eab9

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
1 parent 0fb8f93 commit db7eab9

File tree

6 files changed

+328
-2
lines changed

6 files changed

+328
-2
lines changed

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

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

2532
load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates
2633

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

5562

63+
def pkcs7_x509_extension_policies() -> tuple[ExtensionPolicy, ExtensionPolicy]:
64+
"""
65+
Gets the default X.509 extension policy for S/MIME, based on RFC 8550.
66+
Visit https://www.rfc-editor.org/rfc/rfc8550#section-4.4 for more info.
67+
"""
68+
# CA policy
69+
ca_policy = ExtensionPolicy.webpki_defaults_ca()
70+
71+
# EE policy
72+
def _validate_basic_constraints(
73+
policy: Policy, cert: Certificate, bc: x509.BasicConstraints | None
74+
) -> None:
75+
"""
76+
We check that Certificates used as EE (i.e., the cert used to sign
77+
a PKCS#7/SMIME message) must not have ca=true in their basic
78+
constraints extension. RFC 5280 doesn't impose this requirement, but we
79+
firmly agree about it being best practice.
80+
"""
81+
if bc is not None and bc.ca:
82+
raise ValueError("Basic Constraints CA must be False.")
83+
84+
def _validate_key_usage(
85+
policy: Policy, cert: Certificate, ku: x509.KeyUsage | None
86+
) -> None:
87+
"""
88+
Checks that the Key Usage extension, if present, has at least one of
89+
the digital signature or content commitment (formerly non-repudiation)
90+
bits set.
91+
"""
92+
if (
93+
ku is not None
94+
and not ku.digital_signature
95+
and not ku.content_commitment
96+
):
97+
raise ValueError(
98+
"Key Usage, if specified, must have at least one of the "
99+
"digital signature or content commitment (formerly non "
100+
"repudiation) bits set."
101+
)
102+
103+
def _validate_subject_alternative_name(
104+
policy: Policy,
105+
cert: Certificate,
106+
san: x509.SubjectAlternativeName,
107+
) -> None:
108+
"""
109+
For each general name in the SAN, for those which are email addresses:
110+
- If it is an RFC822Name, general part must be ascii.
111+
- If it is an OtherName, general part must be non-ascii.
112+
"""
113+
for general_name in san:
114+
if (
115+
isinstance(general_name, x509.RFC822Name)
116+
and "@" in general_name.value
117+
and not general_name.value.split("@")[0].isascii()
118+
):
119+
raise ValueError(
120+
f"RFC822Name {general_name.value} contains non-ASCII "
121+
"characters."
122+
)
123+
if (
124+
isinstance(general_name, x509.OtherName)
125+
and "@" in general_name.value.decode()
126+
and general_name.value.decode().split("@")[0].isascii()
127+
):
128+
raise ValueError(
129+
f"OtherName {general_name.value.decode()} is ASCII, "
130+
"so must be stored in RFC822Name."
131+
)
132+
133+
def _validate_extended_key_usage(
134+
policy: Policy, cert: Certificate, eku: x509.ExtendedKeyUsage | None
135+
) -> None:
136+
"""
137+
Checks that the Extended Key Usage extension, if present,
138+
includes either emailProtection or anyExtendedKeyUsage bits.
139+
"""
140+
if (
141+
eku is not None
142+
and ExtendedKeyUsageOID.EMAIL_PROTECTION not in eku
143+
and ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE not in eku
144+
):
145+
raise ValueError(
146+
"Extended Key Usage, if specified, must include "
147+
"emailProtection or anyExtendedKeyUsage."
148+
)
149+
150+
ee_policy = (
151+
ExtensionPolicy.webpki_defaults_ee()
152+
.may_be_present(
153+
x509.BasicConstraints,
154+
Criticality.AGNOSTIC,
155+
_validate_basic_constraints,
156+
)
157+
.may_be_present(
158+
x509.KeyUsage,
159+
Criticality.CRITICAL,
160+
_validate_key_usage,
161+
)
162+
.require_present(
163+
x509.SubjectAlternativeName,
164+
Criticality.AGNOSTIC,
165+
_validate_subject_alternative_name,
166+
)
167+
.may_be_present(
168+
x509.ExtendedKeyUsage,
169+
Criticality.AGNOSTIC,
170+
_validate_extended_key_usage,
171+
)
172+
)
173+
174+
return ca_policy, ee_policy
175+
176+
56177
class PKCS7SignatureBuilder:
57178
def __init__(
58179
self,

tests/hazmat/primitives/test_pkcs7.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
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.oid import (
22+
ExtendedKeyUsageOID,
23+
ExtensionOID,
24+
ObjectIdentifier,
25+
)
26+
from cryptography.x509.verification import (
27+
PolicyBuilder,
28+
Store,
29+
VerificationError,
30+
)
2131
from tests.x509.test_x509 import _generate_ca_and_leaf
2232

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

126136
def _load_cert_key():
127137
key = load_vectors_from_file(
128-
os.path.join("x509", "custom", "ca", "ca_key.pem"),
138+
os.path.join("pkcs7", "ca_key.pem"),
129139
lambda pemfile: serialization.load_pem_private_key(
130140
pemfile.read(), None, unsafe_skip_rsa_key_validation=True
131141
),
132142
mode="rb",
133143
)
134144
cert = load_vectors_from_file(
135-
os.path.join("x509", "custom", "ca", "ca.pem"),
145+
os.path.join("pkcs7", "ca.pem"),
136146
loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()),
137147
mode="rb",
138148
)
139149
return cert, key
140150

141151

152+
class TestPKCS7VerifyCertificate:
153+
@staticmethod
154+
def build_pkcs7_certificate(
155+
ca: bool = False,
156+
digital_signature: bool = True,
157+
usages: typing.Optional[typing.List[ObjectIdentifier]] = None,
158+
) -> x509.Certificate:
159+
"""
160+
This static method is a helper to build certificates allowing us
161+
to test all cases in PKCS#7 certificate verification.
162+
"""
163+
# Load the standard certificate and private key
164+
certificate, private_key = _load_cert_key()
165+
166+
# Basic certificate builder
167+
certificate_builder = (
168+
x509.CertificateBuilder()
169+
.serial_number(certificate.serial_number)
170+
.subject_name(certificate.subject)
171+
.issuer_name(certificate.issuer)
172+
.public_key(private_key.public_key())
173+
.not_valid_before(certificate.not_valid_before)
174+
.not_valid_after(certificate.not_valid_after)
175+
)
176+
177+
# Add AuthorityKeyIdentifier extension
178+
aki = certificate.extensions.get_extension_for_oid(
179+
ExtensionOID.AUTHORITY_KEY_IDENTIFIER
180+
)
181+
certificate_builder = certificate_builder.add_extension(
182+
aki.value, critical=False
183+
)
184+
185+
# Add SubjectAlternativeName extension
186+
san = certificate.extensions.get_extension_for_oid(
187+
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
188+
)
189+
certificate_builder = certificate_builder.add_extension(
190+
san.value, critical=True
191+
)
192+
193+
# Add BasicConstraints extension
194+
bc_extension = x509.BasicConstraints(ca=ca, path_length=None)
195+
certificate_builder = certificate_builder.add_extension(
196+
bc_extension, False
197+
)
198+
199+
# Add KeyUsage extension
200+
ku_extension = x509.KeyUsage(
201+
digital_signature=digital_signature,
202+
content_commitment=False,
203+
key_encipherment=True,
204+
data_encipherment=True,
205+
key_agreement=True,
206+
key_cert_sign=True,
207+
crl_sign=True,
208+
encipher_only=False,
209+
decipher_only=False,
210+
)
211+
certificate_builder = certificate_builder.add_extension(
212+
ku_extension, True
213+
)
214+
215+
# Add valid ExtendedKeyUsage extension
216+
usages = usages or [ExtendedKeyUsageOID.EMAIL_PROTECTION]
217+
certificate_builder = certificate_builder.add_extension(
218+
x509.ExtendedKeyUsage(usages), True
219+
)
220+
221+
# Build the certificate
222+
return certificate_builder.sign(
223+
private_key, certificate.signature_hash_algorithm, None
224+
)
225+
226+
def test_verify_pkcs7_certificate(self):
227+
# Prepare the parameters
228+
certificate = self.build_pkcs7_certificate()
229+
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()
230+
231+
# Verify the certificate
232+
verifier = (
233+
PolicyBuilder()
234+
.store(Store([certificate]))
235+
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
236+
.build_client_verifier()
237+
)
238+
verifier.verify(certificate, [])
239+
240+
@pytest.mark.parametrize(
241+
"arguments",
242+
[
243+
{"ca": True},
244+
{"digital_signature": False},
245+
{"usages": [ExtendedKeyUsageOID.CLIENT_AUTH]},
246+
],
247+
)
248+
def test_verify_invalid_pkcs7_certificate(self, arguments: dict):
249+
# Prepare the parameters
250+
certificate = self.build_pkcs7_certificate(**arguments)
251+
252+
# Verify the certificate
253+
self.verify_invalid_pkcs7_certificate(certificate)
254+
255+
@staticmethod
256+
def verify_invalid_pkcs7_certificate(certificate: x509.Certificate):
257+
ca_policy, ee_policy = pkcs7.pkcs7_x509_extension_policies()
258+
verifier = (
259+
PolicyBuilder()
260+
.store(Store([certificate]))
261+
.extension_policies(ca_policy=ca_policy, ee_policy=ee_policy)
262+
.build_client_verifier()
263+
)
264+
265+
with pytest.raises(VerificationError):
266+
verifier.verify(certificate, [])
267+
268+
@pytest.mark.parametrize(
269+
"filename", ["ca_non_ascii_san.pem", "ca_ascii_san.pem"]
270+
)
271+
def test_verify_pkcs7_certificate_wrong_san(self, filename):
272+
# Read a certificate with an invalid SAN
273+
pkcs7_certificate = load_vectors_from_file(
274+
os.path.join("pkcs7", filename),
275+
loader=lambda pemfile: x509.load_pem_x509_certificate(
276+
pemfile.read()
277+
),
278+
mode="rb",
279+
)
280+
281+
# Verify the certificate
282+
self.verify_invalid_pkcs7_certificate(pkcs7_certificate)
283+
284+
142285
@pytest.mark.supported(
143286
only_if=lambda backend: backend.pkcs7_supported(),
144287
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIID3DCCAsSgAwIBAgIUGJw032ss5tmRmaY8x41pL5lqqRYwDQYJKoZIhvcNAQEL
3+
BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
4+
DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN
5+
SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTg0
6+
NzQ1WhcNMjYwNjA5MTg0NzQ1WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs
7+
aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs
8+
ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl
9+
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLWXuy3atOjhb8g
10+
fa5AC5me9PqRqcqV63e+NIe8IaKioCM5Sl+3jhKb5DdPIjfQYbHbwPtY+rFSP364
11+
dBZoJpCDG4gcD6H3eS5JGc8Uz62l+oBNuFoU3EZiUNMF0k17vs/6CGeyt53+D9DJ
12+
PG6Wv87nAAoK97r1rLdC8Of97QpUV/st+YDP7/LOH8CxJZOnbiUdekzo0dCQkk7n
13+
17hJCYN1Y98VrlZFY25ny2TURUgK7lIjduEUb0dugYiepjzp7ZV8184kpAD/PtLT
14+
czA1S8e6kySd5wbJSFcKxrk/j/cccUGLMyKPlMZgsHZUm/2DOLWLljxbEjCOxb1G
15+
8+EpR9kCAwEAAaNQME4wLQYDVR0RBCYwJKAiBggrBgEFBQcICaAWDBRyZXRvdXJu
16+
ZUBleGFtcGxlLmNvbTAdBgNVHQ4EFgQUm24AOQAmOInCPZPDUagXXw+BEl0wDQYJ
17+
KoZIhvcNAQELBQADggEBAGgLqsx27sS28t1okxT1MU6QhfAn/Yw07Nhk3cpNKGnh
18+
edrPPTXvJc05qHuQIqOiFIJ4SojbQ2+bVZwo7V3Jhspx9T+Gkb/Dn3rHpAfOXuaJ
19+
RqJ777Cor2seAKv07jerGnEULYW8JcezZDGbv6ViC0oEgazwTzahfynrUMJ2DJRX
20+
tnNdczDsGw+DVMvOBzcSE/aEzhd4ghgVq5aFS05wzhN/fTWKiN4tpEAG6y95gU73
21+
29O3y1W3dLjblTZJvXNtgCjMT6R3OVeWAsqyXDprFrZWZucCj8opIxRf6jpZlRfJ
22+
qW+57pkefhg3q4MFjn08BOKpYwOdRouGE4l96dGBDwM=
23+
-----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-----
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDzzCCAregAwIBAgIUAX/xKTtlMllrK5ng0+OkmnxxIugwDQYJKoZIhvcNAQEL
3+
BQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
4+
DVNhbiBGcmFuY2lzY28xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEWMBQGA1UECwwN
5+
SVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjUwNjA5MTgw
6+
NzE4WhcNMjYwNjA5MTgwNzE4WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2Fs
7+
aWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBs
8+
ZSBDb3JwMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRQwEgYDVQQDDAtleGFtcGxl
9+
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOxyV/ZsaGn7dOcZ
10+
6ODFcnmwjPCKRASFeDtOMYoGrlALb9zA+UMuMB63dTZ8ofWsDgLLGhw86njfSYad
11+
RslOw8Bki9lKiS1RhS/RbnDSBWB2wJzniyFn/qI2F93WbgqHMOnzzJcAkc/YPU0T
12+
iyvNpjD3Q/xObcp7ouBJJmFSvLybSTJtFrVzkpIbDZYrn0KyKtgTCPc/r9D04u+u
13+
scSACvTRjePsEZIgRkVgfVpdBmy1KeJmx2NqS8Yev+y+0e9q3t8Ga/j/CnPFXlEl
14+
iBHciFtkKdd2HrPLJMXBKhMn2KagLJSSdABNApi8qULIpOnrEE8FepKCzkptFyS1
15+
5g0H3u0CAwEAAaNDMEEwIAYDVR0RBBkwF4EVcmV0b3VybsOpQGV4YW1wbGUuY29t
16+
MB0GA1UdDgQWBBTthtqdM0IoehNymXnqMPX1joF1LzANBgkqhkiG9w0BAQsFAAOC
17+
AQEApQZ3vOuBgNg1U26c4l0VSCU5q73Lecbgjc42AhEp9FyP7ratj4MyH7RGr4io
18+
vl0wWROFBnzliW5ZA8CP3Ux4AbqgtxcFPBRHACjmrpoSFHmW7bpzRnqwJKwXsOGJ
19+
ZhjA/2o91lEJr0UNhpvSGyR+xCkuvw83mvM1rmE19yNMElv96x/DPVQV2ocsffOb
20+
kS7pIpvXX3pSIj7Up0Xrz+bSyhJlsO3sO5bREshyvuiRivm9AjBVRY/BtbFY6DcV
21+
9javEitCw93BgImIs0CXGpZUrvphX8muWVct5xpKj64/Yo0hIYystX+xVl3EjTRf
22+
B7pH2DE+cXg99p7L6RoYtlOeRA==
23+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)