Skip to content

Commit 4cfd7b1

Browse files
authored
x509 Verification: Python ExtensionPolicy builder API impl. (#12433)
* Rust impl of ExtensionPolicy builder API. * Add ExtensionPolicy builder API to .pyi * ExtensionPolicy builder API tests. * Improve naming. * Refactor test_builder_methods. * Try fix coverage. * Address PR remarks.
1 parent 699243a commit 4cfd7b1

File tree

7 files changed

+539
-12
lines changed

7 files changed

+539
-12
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,49 @@ class Policy:
221221
@property
222222
def minimum_rsa_modulus(self) -> int: ...
223223

224+
class Criticality:
225+
CRITICAL: Criticality
226+
AGNOSTIC: Criticality
227+
NON_CRITICAL: Criticality
228+
229+
type MaybeExtensionValidatorCallback[T: x509.ExtensionType] = typing.Callable[
230+
[
231+
Policy,
232+
x509.Certificate,
233+
T | None,
234+
],
235+
None,
236+
]
237+
238+
type PresentExtensionValidatorCallback[T: x509.ExtensionType] = (
239+
typing.Callable[
240+
[Policy, x509.Certificate, T],
241+
None,
242+
]
243+
)
244+
224245
class ExtensionPolicy:
225246
@staticmethod
226247
def permit_all() -> ExtensionPolicy: ...
227248
@staticmethod
228249
def webpki_defaults_ca() -> ExtensionPolicy: ...
229250
@staticmethod
230251
def webpki_defaults_ee() -> ExtensionPolicy: ...
252+
def require_not_present(
253+
self, extension_type: type[x509.ExtensionType]
254+
) -> ExtensionPolicy: ...
255+
def may_be_present[T: x509.ExtensionType](
256+
self,
257+
extension_type: type[T],
258+
criticality: Criticality,
259+
validator: MaybeExtensionValidatorCallback[T] | None,
260+
) -> ExtensionPolicy: ...
261+
def require_present[T: x509.ExtensionType](
262+
self,
263+
extension_type: type[T],
264+
criticality: Criticality,
265+
validator: PresentExtensionValidatorCallback[T] | None,
266+
) -> ExtensionPolicy: ...
231267

232268
class VerifiedClient:
233269
@property

src/cryptography/x509/verification.py

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

1212
__all__ = [
1313
"ClientVerifier",
14+
"Criticality",
1415
"ExtensionPolicy",
1516
"Policy",
1617
"PolicyBuilder",
@@ -29,4 +30,5 @@
2930
PolicyBuilder = rust_x509.PolicyBuilder
3031
Policy = rust_x509.Policy
3132
ExtensionPolicy = rust_x509.ExtensionPolicy
33+
Criticality = rust_x509.Criticality
3234
VerificationError = rust_x509.VerificationError

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, PyExtensionPolicy, PyPolicy, PyServerVerifier,
139-
PyStore, PyVerifiedClient, VerificationError,
138+
PolicyBuilder, PyClientVerifier, PyCriticality, PyExtensionPolicy, PyPolicy,
139+
PyServerVerifier, PyStore, PyVerifiedClient, VerificationError,
140140
};
141141
}
142142

src/rust/src/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ pub static REASON_FLAGS: LazyPyImport = LazyPyImport::new("cryptography.x509", &
181181
pub static ATTRIBUTE: LazyPyImport = LazyPyImport::new("cryptography.x509", &["Attribute"]);
182182
pub static ATTRIBUTES: LazyPyImport = LazyPyImport::new("cryptography.x509", &["Attributes"]);
183183

184+
pub static EXTENSION_TYPE: LazyPyImport =
185+
LazyPyImport::new("cryptography.x509", &["ExtensionType"]);
186+
184187
pub static CRL_NUMBER: LazyPyImport = LazyPyImport::new("cryptography.x509", &["CRLNumber"]);
185188
pub static DELTA_CRL_INDICATOR: LazyPyImport =
186189
LazyPyImport::new("cryptography.x509", &["DeltaCRLIndicator"]);

src/rust/src/x509/verify/extension_policy.rs

Lines changed: 252 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,56 @@
1+
use std::collections::HashSet;
2+
use std::sync::Arc;
3+
4+
use cryptography_x509::oid::{
5+
AUTHORITY_INFORMATION_ACCESS_OID, AUTHORITY_KEY_IDENTIFIER_OID, BASIC_CONSTRAINTS_OID,
6+
EXTENDED_KEY_USAGE_OID, KEY_USAGE_OID, NAME_CONSTRAINTS_OID, SUBJECT_ALTERNATIVE_NAME_OID,
7+
SUBJECT_KEY_IDENTIFIER_OID,
8+
};
9+
10+
use cryptography_x509::extensions::Extension;
11+
12+
use cryptography_x509_verification::ops::VerificationCertificate;
13+
use cryptography_x509_verification::policy::{
14+
Criticality, ExtensionPolicy, ExtensionValidator, MaybeExtensionValidatorCallback, Policy,
15+
PresentExtensionValidatorCallback,
16+
};
17+
use cryptography_x509_verification::{ValidationError, ValidationErrorKind, ValidationResult};
18+
use pyo3::types::PyAnyMethods;
19+
use pyo3::types::PyTypeMethods;
20+
use pyo3::{intern, PyResult};
21+
22+
use crate::asn1::py_oid_to_oid;
23+
24+
use crate::types;
25+
use crate::x509::certificate::parse_cert_ext;
26+
127
use super::PyCryptoOps;
2-
use cryptography_x509_verification::policy::ExtensionPolicy;
28+
29+
#[pyo3::pyclass(
30+
frozen,
31+
eq,
32+
module = "cryptography.x509.verification",
33+
name = "Criticality"
34+
)]
35+
#[derive(PartialEq, Eq, Clone)]
36+
pub(crate) enum PyCriticality {
37+
#[pyo3(name = "CRITICAL")]
38+
Critical,
39+
#[pyo3(name = "AGNOSTIC")]
40+
Agnostic,
41+
#[pyo3(name = "NON_CRITICAL")]
42+
NonCritical,
43+
}
44+
45+
impl From<PyCriticality> for Criticality {
46+
fn from(criticality: PyCriticality) -> Criticality {
47+
match criticality {
48+
PyCriticality::Critical => Criticality::Critical,
49+
PyCriticality::Agnostic => Criticality::Agnostic,
50+
PyCriticality::NonCritical => Criticality::NonCritical,
51+
}
52+
}
53+
}
354

455
#[pyo3::pyclass(
556
frozen,
@@ -8,6 +59,7 @@ use cryptography_x509_verification::policy::ExtensionPolicy;
859
)]
960
pub(crate) struct PyExtensionPolicy {
1061
inner_policy: ExtensionPolicy<'static, PyCryptoOps>,
62+
already_set_oids: HashSet<asn1::ObjectIdentifier>,
1163
}
1264

1365
impl PyExtensionPolicy {
@@ -16,10 +68,63 @@ impl PyExtensionPolicy {
1668
}
1769

1870
fn new(inner_policy: ExtensionPolicy<'static, PyCryptoOps>) -> Self {
19-
PyExtensionPolicy { inner_policy }
71+
PyExtensionPolicy {
72+
inner_policy,
73+
already_set_oids: HashSet::new(),
74+
}
75+
}
76+
77+
fn with_assigned_validator(
78+
&self,
79+
oid: asn1::ObjectIdentifier,
80+
validator: ExtensionValidator<'static, PyCryptoOps>,
81+
) -> PyResult<PyExtensionPolicy> {
82+
if self.already_set_oids.contains(&oid) {
83+
return Err(pyo3::exceptions::PyValueError::new_err(format!(
84+
"ExtensionPolicy already configured for extension with OID {oid}"
85+
)));
86+
}
87+
88+
let mut policy = self.inner_policy.clone();
89+
match oid {
90+
AUTHORITY_INFORMATION_ACCESS_OID => policy.authority_information_access = validator,
91+
AUTHORITY_KEY_IDENTIFIER_OID => policy.authority_key_identifier = validator,
92+
SUBJECT_KEY_IDENTIFIER_OID => policy.subject_key_identifier = validator,
93+
KEY_USAGE_OID => policy.key_usage = validator,
94+
SUBJECT_ALTERNATIVE_NAME_OID => policy.subject_alternative_name = validator,
95+
BASIC_CONSTRAINTS_OID => policy.basic_constraints = validator,
96+
NAME_CONSTRAINTS_OID => policy.name_constraints = validator,
97+
EXTENDED_KEY_USAGE_OID => policy.extended_key_usage = validator,
98+
_ => {
99+
return Err(pyo3::exceptions::PyValueError::new_err(format!(
100+
"Unsupported extension OID: {}",
101+
oid
102+
)))
103+
}
104+
}
105+
106+
let mut already_set_oids = self.already_set_oids.clone();
107+
already_set_oids.insert(oid);
108+
Ok(PyExtensionPolicy {
109+
inner_policy: policy,
110+
already_set_oids,
111+
})
20112
}
21113
}
22114

115+
fn oid_from_py_extension_type(
116+
py: pyo3::Python<'_>,
117+
extension_type: pyo3::Bound<'_, pyo3::types::PyType>,
118+
) -> pyo3::PyResult<asn1::ObjectIdentifier> {
119+
if !extension_type.is_subclass(&types::EXTENSION_TYPE.get(py)?)? {
120+
return Err(pyo3::exceptions::PyTypeError::new_err(
121+
"extension_type must be a subclass of ExtensionType",
122+
));
123+
}
124+
125+
py_oid_to_oid(extension_type.getattr(intern!(py, "oid"))?)
126+
}
127+
23128
#[pyo3::pymethods]
24129
impl PyExtensionPolicy {
25130
#[staticmethod]
@@ -36,4 +141,149 @@ impl PyExtensionPolicy {
36141
pub(crate) fn webpki_defaults_ee() -> Self {
37142
PyExtensionPolicy::new(ExtensionPolicy::new_default_webpki_ee())
38143
}
144+
145+
pub(crate) fn require_not_present(
146+
&self,
147+
py: pyo3::Python<'_>,
148+
extension_type: pyo3::Bound<'_, pyo3::types::PyType>,
149+
) -> pyo3::PyResult<PyExtensionPolicy> {
150+
let oid = oid_from_py_extension_type(py, extension_type)?;
151+
self.with_assigned_validator(oid, ExtensionValidator::NotPresent)
152+
}
153+
154+
#[pyo3(signature = (extension_type, criticality, validator_cb))]
155+
pub(crate) fn may_be_present(
156+
&self,
157+
py: pyo3::Python<'_>,
158+
extension_type: pyo3::Bound<'_, pyo3::types::PyType>,
159+
criticality: PyCriticality,
160+
validator_cb: Option<pyo3::PyObject>,
161+
) -> pyo3::PyResult<PyExtensionPolicy> {
162+
let oid = oid_from_py_extension_type(py, extension_type)?;
163+
self.with_assigned_validator(
164+
oid,
165+
ExtensionValidator::MaybePresent {
166+
criticality: criticality.into(),
167+
validator: validator_cb.map(wrap_maybe_validator_callback),
168+
},
169+
)
170+
}
171+
172+
#[pyo3(signature = (extension_type, criticality, validator_cb))]
173+
pub(crate) fn require_present(
174+
&self,
175+
py: pyo3::Python<'_>,
176+
extension_type: pyo3::Bound<'_, pyo3::types::PyType>,
177+
criticality: PyCriticality,
178+
validator_cb: Option<pyo3::PyObject>,
179+
) -> pyo3::PyResult<PyExtensionPolicy> {
180+
let oid = oid_from_py_extension_type(py, extension_type)?;
181+
self.with_assigned_validator(
182+
oid,
183+
ExtensionValidator::Present {
184+
criticality: criticality.into(),
185+
validator: validator_cb.map(wrap_present_validator_callback),
186+
},
187+
)
188+
}
189+
}
190+
191+
fn wrap_maybe_validator_callback(
192+
py_cb: pyo3::PyObject,
193+
) -> MaybeExtensionValidatorCallback<'static, PyCryptoOps> {
194+
Arc::new(
195+
move |policy: &Policy<'_, PyCryptoOps>,
196+
cert: &VerificationCertificate<'_, PyCryptoOps>,
197+
ext: Option<&Extension<'_>>| {
198+
pyo3::Python::with_gil(|py| {
199+
invoke_py_validator_callback(
200+
py,
201+
&py_cb,
202+
(
203+
policy.extra.clone_ref(py),
204+
cert.extra().clone_ref(py),
205+
make_py_extension(py, ext)?,
206+
),
207+
)
208+
})
209+
},
210+
)
211+
}
212+
213+
fn wrap_present_validator_callback(
214+
py_cb: pyo3::PyObject,
215+
) -> PresentExtensionValidatorCallback<'static, PyCryptoOps> {
216+
Arc::new(
217+
move |policy: &Policy<'_, PyCryptoOps>,
218+
cert: &VerificationCertificate<'_, PyCryptoOps>,
219+
ext: &Extension<'_>| {
220+
pyo3::Python::with_gil(|py| {
221+
invoke_py_validator_callback(
222+
py,
223+
&py_cb,
224+
(
225+
policy.extra.clone_ref(py),
226+
cert.extra().clone_ref(py),
227+
make_py_extension(py, Some(ext))?.unwrap(),
228+
),
229+
)
230+
})
231+
},
232+
)
233+
}
234+
235+
fn make_py_extension<'chain, 'p>(
236+
py: pyo3::Python<'p>,
237+
ext: Option<&Extension<'p>>,
238+
) -> ValidationResult<'chain, Option<pyo3::Bound<'p, pyo3::types::PyAny>>, PyCryptoOps> {
239+
Ok(match ext {
240+
None => None,
241+
Some(ext) => parse_cert_ext(py, ext).map_err(|e| {
242+
ValidationError::new(ValidationErrorKind::Other(format!(
243+
"{e} (while converting Extension to Python object)"
244+
)))
245+
})?,
246+
})
247+
}
248+
249+
fn invoke_py_validator_callback<'py>(
250+
py: pyo3::Python<'py>,
251+
py_cb: &pyo3::PyObject,
252+
args: impl pyo3::IntoPyObject<'py, Target = pyo3::types::PyTuple>,
253+
) -> ValidationResult<'static, (), PyCryptoOps> {
254+
let result = py_cb.bind(py).call1(args).map_err(|e| {
255+
ValidationError::new(ValidationErrorKind::Other(format!(
256+
"Python extension validator failed: {}",
257+
e
258+
)))
259+
})?;
260+
261+
if !result.is_none() {
262+
let error_kind =
263+
ValidationErrorKind::Other("Python validator must return None.".to_string());
264+
Err(ValidationError::new(error_kind))
265+
} else {
266+
Ok(())
267+
}
268+
}
269+
270+
#[cfg(test)]
271+
mod tests {
272+
use cryptography_x509::extensions::Extension;
273+
274+
#[test]
275+
fn test_make_py_extension_fail() {
276+
pyo3::Python::with_gil(|py| {
277+
let invalid_extension = Extension {
278+
// SubjectAlternativeName
279+
extn_id: asn1::ObjectIdentifier::from_string("2.5.29.17").unwrap(),
280+
critical: false,
281+
extn_value: &[],
282+
};
283+
let result = super::make_py_extension(py, Some(&invalid_extension));
284+
assert!(result.is_err());
285+
let error = result.unwrap_err();
286+
assert!(format!("{error}").contains("(while converting Extension to Python object)"));
287+
})
288+
}
39289
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::utils::cstr_from_literal;
2121
use crate::x509::certificate::Certificate as PyCertificate;
2222
use crate::x509::common::{datetime_now, py_to_datetime};
2323
use crate::x509::sign;
24-
pub(crate) use extension_policy::PyExtensionPolicy;
24+
pub(crate) use extension_policy::{PyCriticality, PyExtensionPolicy};
2525
pub(crate) use policy::PyPolicy;
2626

2727
#[derive(Clone)]

0 commit comments

Comments
 (0)