Skip to content

Commit 5c3219d

Browse files
authored
x509 Verification: base Python support for extension policies (#12432)
* Base Rust impl of python-defined ExtensionPolicies. * Improve naming to make clone apparent. * Add new API to python interface definition. * ExtensionPolicy static constructor methods test. * Fix invalid unwrap in ClientVerifier::verify * Add ClientVerifier test for when policy allows no SAN. * PyCryptoOps Clone test for coverage.
1 parent 8ab6708 commit 5c3219d

File tree

8 files changed

+201
-17
lines changed

8 files changed

+201
-17
lines changed

src/cryptography/hazmat/bindings/_rust/x509.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ class PolicyBuilder:
201201
def time(self, new_time: datetime.datetime) -> PolicyBuilder: ...
202202
def store(self, new_store: Store) -> PolicyBuilder: ...
203203
def max_chain_depth(self, new_max_chain_depth: int) -> PolicyBuilder: ...
204+
def extension_policies(
205+
self, new_ca_policy: ExtensionPolicy, new_ee_policy: ExtensionPolicy
206+
) -> PolicyBuilder: ...
204207
def build_client_verifier(self) -> ClientVerifier: ...
205208
def build_server_verifier(
206209
self, subject: x509.verification.Subject
@@ -218,6 +221,14 @@ class Policy:
218221
@property
219222
def minimum_rsa_modulus(self) -> int: ...
220223

224+
class ExtensionPolicy:
225+
@staticmethod
226+
def permit_all() -> ExtensionPolicy: ...
227+
@staticmethod
228+
def webpki_defaults_ca() -> ExtensionPolicy: ...
229+
@staticmethod
230+
def webpki_defaults_ee() -> ExtensionPolicy: ...
231+
221232
class VerifiedClient:
222233
@property
223234
def subjects(self) -> list[x509.GeneralName] | None: ...

src/cryptography/x509/verification.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
__all__ = [
1313
"ClientVerifier",
14+
"ExtensionPolicy",
15+
"Policy",
1416
"PolicyBuilder",
1517
"ServerVerifier",
1618
"Store",
@@ -26,4 +28,5 @@
2628
ServerVerifier = rust_x509.ServerVerifier
2729
PolicyBuilder = rust_x509.PolicyBuilder
2830
Policy = rust_x509.Policy
31+
ExtensionPolicy = rust_x509.ExtensionPolicy
2932
VerificationError = rust_x509.VerificationError

src/rust/cryptography-x509-verification/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub struct ValidationError<'chain, B: CryptoOps> {
4949
}
5050

5151
impl<'chain, B: CryptoOps> ValidationError<'chain, B> {
52-
pub(crate) fn new(kind: ValidationErrorKind<'chain, B>) -> Self {
52+
pub fn new(kind: ValidationErrorKind<'chain, B>) -> Self {
5353
ValidationError { kind, cert: None }
5454
}
5555

src/rust/cryptography-x509-verification/src/policy/extension.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616
ops::CryptoOps, policy::Policy, ValidationError, ValidationErrorKind, ValidationResult,
1717
};
1818

19+
#[derive(Clone)]
1920
pub struct ExtensionPolicy<'cb, B: CryptoOps> {
2021
pub authority_information_access: ExtensionValidator<'cb, B>,
2122
pub authority_key_identifier: ExtensionValidator<'cb, B>,
@@ -28,6 +29,27 @@ pub struct ExtensionPolicy<'cb, B: CryptoOps> {
2829
}
2930

3031
impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> {
32+
pub fn new_permit_all() -> Self {
33+
const fn make_permissive_validator<'cb, B: CryptoOps + 'cb>() -> ExtensionValidator<'cb, B>
34+
{
35+
ExtensionValidator::MaybePresent {
36+
criticality: Criticality::Agnostic,
37+
validator: None,
38+
}
39+
}
40+
41+
ExtensionPolicy {
42+
authority_information_access: make_permissive_validator(),
43+
authority_key_identifier: make_permissive_validator(),
44+
subject_key_identifier: make_permissive_validator(),
45+
key_usage: make_permissive_validator(),
46+
subject_alternative_name: make_permissive_validator(),
47+
basic_constraints: make_permissive_validator(),
48+
name_constraints: make_permissive_validator(),
49+
extended_key_usage: make_permissive_validator(),
50+
}
51+
}
52+
3153
pub fn new_default_webpki_ca() -> Self {
3254
ExtensionPolicy {
3355
// 5280 4.2.2.1: Authority Information Access
@@ -214,6 +236,7 @@ impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> {
214236
}
215237

216238
/// Represents different criticality states for an extension.
239+
#[derive(Clone)]
217240
pub enum Criticality {
218241
/// The extension MUST be marked as critical.
219242
Critical,
@@ -258,6 +281,7 @@ pub type MaybeExtensionValidatorCallback<'cb, B> = Arc<
258281
>;
259282

260283
/// Represents different validation states for an extension.
284+
#[derive(Clone)]
261285
pub enum ExtensionValidator<'cb, B: CryptoOps> {
262286
/// The extension MUST NOT be present.
263287
NotPresent,

src/rust/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ mod _rust {
135135
use crate::x509::sct::Sct;
136136
#[pymodule_export]
137137
use crate::x509::verify::{
138-
PolicyBuilder, PyClientVerifier, PyPolicy, PyServerVerifier, PyStore, PyVerifiedClient,
139-
VerificationError,
138+
PolicyBuilder, PyClientVerifier, PyExtensionPolicy, PyPolicy, PyServerVerifier,
139+
PyStore, PyVerifiedClient, VerificationError,
140140
};
141141
}
142142

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use super::PyCryptoOps;
2+
use cryptography_x509_verification::policy::ExtensionPolicy;
3+
4+
#[pyo3::pyclass(
5+
frozen,
6+
module = "cryptography.x509.verification",
7+
name = "ExtensionPolicy"
8+
)]
9+
pub(crate) struct PyExtensionPolicy {
10+
rust_policy: ExtensionPolicy<'static, PyCryptoOps>,
11+
}
12+
13+
impl PyExtensionPolicy {
14+
pub(super) fn clone_rust_policy(&self) -> ExtensionPolicy<'static, PyCryptoOps> {
15+
self.rust_policy.clone()
16+
}
17+
18+
fn new(rust_policy: ExtensionPolicy<'static, PyCryptoOps>) -> Self {
19+
PyExtensionPolicy { rust_policy }
20+
}
21+
}
22+
23+
#[pyo3::pymethods]
24+
impl PyExtensionPolicy {
25+
#[staticmethod]
26+
pub(crate) fn permit_all() -> Self {
27+
PyExtensionPolicy::new(ExtensionPolicy::new_permit_all())
28+
}
29+
30+
#[staticmethod]
31+
pub(crate) fn webpki_defaults_ca() -> Self {
32+
PyExtensionPolicy::new(ExtensionPolicy::new_default_webpki_ca())
33+
}
34+
35+
#[staticmethod]
36+
pub(crate) fn webpki_defaults_ee() -> Self {
37+
PyExtensionPolicy::new(ExtensionPolicy::new_default_webpki_ee())
38+
}
39+
}

src/rust/src/x509/verify/mod.rs

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use cryptography_x509_verification::trust_store::Store;
1111
use cryptography_x509_verification::types::{DNSName, IPAddress};
1212
use pyo3::types::{PyAnyMethods, PyListMethods};
1313

14+
mod extension_policy;
1415
mod policy;
1516
use super::parse_general_names;
1617
use crate::backend::keys;
@@ -20,8 +21,10 @@ use crate::utils::cstr_from_literal;
2021
use crate::x509::certificate::Certificate as PyCertificate;
2122
use crate::x509::common::{datetime_now, py_to_datetime};
2223
use crate::x509::sign;
24+
pub(crate) use extension_policy::PyExtensionPolicy;
2325
pub(crate) use policy::PyPolicy;
2426

27+
#[derive(Clone)]
2528
pub(crate) struct PyCryptoOps {}
2629

2730
impl CryptoOps for PyCryptoOps {
@@ -82,6 +85,8 @@ pub(crate) struct PolicyBuilder {
8285
time: Option<asn1::DateTime>,
8386
store: Option<pyo3::Py<PyStore>>,
8487
max_chain_depth: Option<u8>,
88+
ca_ext_policy: Option<pyo3::Py<PyExtensionPolicy>>,
89+
ee_ext_policy: Option<pyo3::Py<PyExtensionPolicy>>,
8590
}
8691

8792
impl PolicyBuilder {
@@ -90,6 +95,8 @@ impl PolicyBuilder {
9095
time: self.time.clone(),
9196
store: self.store.as_ref().map(|s| s.clone_ref(py)),
9297
max_chain_depth: self.max_chain_depth,
98+
ca_ext_policy: self.ca_ext_policy.as_ref().map(|p| p.clone_ref(py)),
99+
ee_ext_policy: self.ee_ext_policy.as_ref().map(|p| p.clone_ref(py)),
93100
}
94101
}
95102
}
@@ -102,6 +109,8 @@ impl PolicyBuilder {
102109
time: None,
103110
store: None,
104111
max_chain_depth: None,
112+
ca_ext_policy: None,
113+
ee_ext_policy: None,
105114
}
106115
}
107116

@@ -144,6 +153,22 @@ impl PolicyBuilder {
144153
})
145154
}
146155

156+
fn extension_policies(
157+
&self,
158+
py: pyo3::Python<'_>,
159+
new_ca_policy: pyo3::Py<PyExtensionPolicy>,
160+
new_ee_policy: pyo3::Py<PyExtensionPolicy>,
161+
) -> CryptographyResult<PolicyBuilder> {
162+
// Enough to check one of the two, since they can only be set together.
163+
policy_builder_set_once_check!(self, ca_ext_policy, "extension policies");
164+
165+
Ok(PolicyBuilder {
166+
ca_ext_policy: Some(new_ca_policy),
167+
ee_ext_policy: Some(new_ee_policy),
168+
..self.py_clone(py)
169+
})
170+
}
171+
147172
fn build_client_verifier(&self, py: pyo3::Python<'_>) -> CryptographyResult<PyClientVerifier> {
148173
let store = match self.store.as_ref() {
149174
Some(s) => s.clone_ref(py),
@@ -162,8 +187,17 @@ impl PolicyBuilder {
162187
};
163188

164189
let policy_definition = OwnedPolicyDefinition::new(None, |_subject| {
165-
// TODO: Pass extension policies here once implemented in cryptography-x509-verification.
166-
PolicyDefinition::client(PyCryptoOps {}, time, self.max_chain_depth, None, None)
190+
PolicyDefinition::client(
191+
PyCryptoOps {},
192+
time,
193+
self.max_chain_depth,
194+
self.ca_ext_policy
195+
.as_ref()
196+
.map(|p| p.get().clone_rust_policy()),
197+
self.ee_ext_policy
198+
.as_ref()
199+
.map(|p| p.get().clone_rust_policy()),
200+
)
167201
});
168202

169203
let py_policy = PyPolicy {
@@ -208,14 +242,17 @@ impl PolicyBuilder {
208242
.expect("subject_owner for ServerVerifier can not be None"),
209243
)?;
210244

211-
// TODO: Pass extension policies here once implemented in cryptography-x509-verification.
212245
Ok::<PyCryptoPolicyDefinition<'_>, pyo3::PyErr>(PolicyDefinition::server(
213246
PyCryptoOps {},
214247
subject,
215248
time,
216249
self.max_chain_depth,
217-
None,
218-
None,
250+
self.ca_ext_policy
251+
.as_ref()
252+
.map(|p| p.get().clone_rust_policy()),
253+
self.ee_ext_policy
254+
.as_ref()
255+
.map(|p| p.get().clone_rust_policy()),
219256
))
220257
})?;
221258

@@ -345,22 +382,24 @@ impl PyClientVerifier {
345382
py_chain.append(c.extra())?;
346383
}
347384

348-
// NOTE: These `unwrap()`s cannot fail, since the underlying policy
349-
// enforces the presence of a SAN and the well-formedness of the
350-
// extension set.
351-
let leaf_san = &chain[0]
385+
// NOTE: The `unwrap()` cannot fail, since the underlying policy
386+
// enforces the well-formedness of the extension set.
387+
let subjects = match &chain[0]
352388
.certificate()
353389
.extensions()
354390
.ok()
355391
.unwrap()
356392
.get_extension(&SUBJECT_ALTERNATIVE_NAME_OID)
357-
.unwrap();
358-
359-
let leaf_gns = leaf_san.value::<SubjectAlternativeName<'_>>()?;
360-
let py_gns = parse_general_names(py, &leaf_gns)?;
393+
{
394+
Some(leaf_san) => {
395+
let leaf_gns = leaf_san.value::<SubjectAlternativeName<'_>>()?;
396+
Some(parse_general_names(py, &leaf_gns)?.unbind())
397+
}
398+
None => None,
399+
};
361400

362401
Ok(PyVerifiedClient {
363-
subjects: Some(py_gns.into()),
402+
subjects,
364403
chain: py_chain.unbind(),
365404
})
366405
}
@@ -534,3 +573,15 @@ impl PyStore {
534573
})
535574
}
536575
}
576+
577+
#[cfg(test)]
578+
mod tests {
579+
use super::PyCryptoOps;
580+
581+
#[test]
582+
fn test_crypto_ops_clone() {
583+
// Just for coverage.
584+
// The trait is needed to be able to clone ExtensionPolicy<'_, PyCryptoOps>.
585+
let _ = PyCryptoOps {}.clone();
586+
}
587+
}

tests/x509/verification/test_verification.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from cryptography.hazmat._oid import ExtendedKeyUsageOID
1414
from cryptography.x509.general_name import DNSName, IPAddress
1515
from cryptography.x509.verification import (
16+
ExtensionPolicy,
1617
PolicyBuilder,
1718
Store,
1819
VerificationError,
@@ -209,6 +210,33 @@ def test_verify_fails_renders_oid(self):
209210
):
210211
verifier.verify(leaf, [])
211212

213+
def test_custom_ext_policy_no_san(self):
214+
leaf = _load_cert(
215+
os.path.join("x509", "custom", "no_sans.pem"),
216+
x509.load_pem_x509_certificate,
217+
)
218+
219+
store = Store([leaf])
220+
validation_time = datetime.datetime.fromisoformat(
221+
"2025-04-14T00:00:00+00:00"
222+
)
223+
224+
builder = PolicyBuilder().store(store)
225+
builder = builder.time(validation_time)
226+
227+
with pytest.raises(
228+
VerificationError,
229+
match="missing required extension",
230+
):
231+
builder.build_client_verifier().verify(leaf, [])
232+
233+
builder = builder.extension_policies(
234+
ExtensionPolicy.webpki_defaults_ca(), ExtensionPolicy.permit_all()
235+
)
236+
237+
verified_client = builder.build_client_verifier().verify(leaf, [])
238+
assert verified_client.subjects is None
239+
212240

213241
class TestServerVerifier:
214242
@pytest.mark.parametrize(
@@ -261,3 +289,31 @@ def test_error_message(self):
261289
match=r"<Certificate\(subject=.*?CN=www.cryptography.io.*?\)>",
262290
):
263291
verifier.verify(leaf, [])
292+
293+
294+
class TestCustomExtensionPolicies:
295+
leaf = _load_cert(
296+
os.path.join("x509", "cryptography.io.pem"),
297+
x509.load_pem_x509_certificate,
298+
)
299+
ca = _load_cert(
300+
os.path.join("x509", "rapidssl_sha256_ca_g3.pem"),
301+
x509.load_pem_x509_certificate,
302+
)
303+
store = Store([ca])
304+
validation_time = datetime.datetime.fromisoformat(
305+
"2018-11-16T00:00:00+00:00"
306+
)
307+
308+
def test_static_methods(self):
309+
builder = PolicyBuilder().store(self.store)
310+
builder = builder.time(self.validation_time).max_chain_depth(16)
311+
builder = builder.extension_policies(
312+
ExtensionPolicy.webpki_defaults_ca(),
313+
ExtensionPolicy.webpki_defaults_ee(),
314+
)
315+
316+
builder.build_client_verifier().verify(self.leaf, [])
317+
builder.build_server_verifier(DNSName("cryptography.io")).verify(
318+
self.leaf, []
319+
)

0 commit comments

Comments
 (0)