Skip to content

Commit 7487008

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 5e9e164 commit 7487008

File tree

7 files changed

+327
-2
lines changed

7 files changed

+327
-2
lines changed

docs/development/test-vectors.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,14 @@ Custom PKCS7 Test Vectors
10031003
* ``pkcs7/enveloped-no-content.der``- A DER encoded PKCS7 file with
10041004
enveloped data, without encrypted content, with key encrypted under the
10051005
public key of ``x509/custom/ca/rsa_ca.pem``.
1006+
* ``pkcs7/ca.pem`` - A certificate adapted for S/MIME signature & verification.
1007+
Its private key is ``pkcs7/ca_key.pem`` .
1008+
* ``pkcs7/ca.pem`` - A certificate adapted for S/MIME signature & verification.
1009+
Its private key is ``pkcs7/ca_key.pem`` .
1010+
* ``pkcs7/ca_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature
1011+
& verification. It has an ASCII subject alternative name stored as `otherName`.
1012+
* ``pkcs7/ca_non_ascii_san.pem`` - An invalid certificate adapted for S/MIME signature
1013+
& verification. It has an non-ASCII subject alternative name stored as `rfc822Name`.
10061014

10071015
Custom OpenSSH Test Vectors
10081016
~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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