Skip to content

Commit

Permalink
implement ClientVerifier.verify
Browse files Browse the repository at this point in the history
Signed-off-by: William Woodruff <[email protected]>
  • Loading branch information
woodruffw committed Feb 8, 2024
1 parent 543f055 commit 8a540d3
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 18 deletions.
13 changes: 10 additions & 3 deletions docs/x509/verification.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ the root of trust:
:class:`cryptography.x509.general_name.DNSName`,
:class:`cryptography.x509.general_name.IPAddress`.

.. class:: VerifiedClient

.. versionadded:: 43.0.0

Type alias: A tuple of :class:`~cryptography.x509.Extension` and
a list of :class:`~cryptography.x509.Certificate`. The extension contains
a :class:`~cryptography.x509.SubjectAlternativeName` with the client's
SAN.

.. class:: ClientVerifier

.. versionadded:: 43.0.0
Expand Down Expand Up @@ -147,9 +156,7 @@ the root of trust:
:param intermediates: A :class:`list` of intermediate :class:`~cryptography.x509.Certificate` to attempt to use

:returns:
A three-tuple of the client certificate's subject,
the client certificate's SAN (or ``None``), and a ``list`` containing
the built chain.
A new instance of :class:`VerifiedClient`

:raises VerificationError: If a valid chain cannot be constructed

Expand Down
4 changes: 1 addition & 3 deletions src/cryptography/hazmat/bindings/_rust/x509.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ class ClientVerifier:
self,
leaf: x509.Certificate,
intermediates: list[x509.Certificate],
) -> tuple[
x509.Name, x509.SubjectAlternativeName | None, list[x509.Certificate]
]: ...
) -> x509.verification.VerifiedClient: ...

class ServerVerifier:
@property
Expand Down
5 changes: 5 additions & 0 deletions src/cryptography/x509/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@
import typing

from cryptography.hazmat.bindings._rust import x509 as rust_x509
from cryptography.x509.base import Certificate
from cryptography.x509.extensions import Extension, SubjectAlternativeName
from cryptography.x509.general_name import DNSName, IPAddress

__all__ = [
"Store",
"Subject",
"VerifiedClient",
"ClientVerifier",
"ServerVerifier",
"PolicyBuilder",
"VerificationError",
]

Store = rust_x509.Store
Subject = typing.Union[DNSName, IPAddress]
VerifiedClient = tuple[Extension[SubjectAlternativeName], list[Certificate]]
ClientVerifier = rust_x509.ClientVerifier
ServerVerifier = rust_x509.ServerVerifier
PolicyBuilder = rust_x509.PolicyBuilder
Expand Down
8 changes: 4 additions & 4 deletions src/rust/cryptography-x509-verification/src/policy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,10 @@ impl<'a, B: CryptoOps> Policy<'a, B> {
Criticality::Agnostic,
Some(ee::subject_alternative_name),
),
false => ExtensionValidator::MaybePresent {
criticality: Criticality::Agnostic,
validator: None,
},
// Under client validation, we return the SAN rather than verifying
// it directly against the profile. As such, we require it here
// but don't supply any custom validator logic.
false => ExtensionValidator::present(Criticality::Agnostic, None),
},
// 5280 4.2.1.9: Basic Constraints
basic_constraints: ExtensionValidator::maybe_present(
Expand Down
95 changes: 87 additions & 8 deletions src/rust/src/x509/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use cryptography_x509::certificate::Certificate;
use cryptography_x509::{
certificate::Certificate, extensions::SubjectAlternativeName, oid::SUBJECT_ALTERNATIVE_NAME_OID,
};
use cryptography_x509_verification::{
ops::{CryptoOps, VerificationCertificate},
policy::{Policy, Subject},
trust_store::Store,
types::{DNSName, IPAddress},
};
use pyo3::IntoPy;

use crate::backend::keys;
use crate::error::{CryptographyError, CryptographyResult};
use crate::types;
use crate::x509::certificate::Certificate as PyCertificate;
use crate::x509::common::{datetime_now, datetime_to_py, py_to_datetime};
use crate::x509::sign;
use crate::{asn1::oid_to_py_oid, types};
use crate::{
error::{CryptographyError, CryptographyResult},
x509,
};

pub(crate) struct PyCryptoOps {}

Expand Down Expand Up @@ -143,10 +149,7 @@ impl PolicyBuilder {
)))
})?;

Ok(PyClientVerifier {
_policy: policy,
store,
})
Ok(PyClientVerifier { policy, store })
}

fn build_server_verifier(
Expand Down Expand Up @@ -218,11 +221,86 @@ self_cell::self_cell!(
module = "cryptography.hazmat.bindings._rust.x509"
)]
struct PyClientVerifier {
_policy: OwnedPolicy,
policy: OwnedPolicy,
#[pyo3(get)]
store: pyo3::Py<PyStore>,
}

impl PyClientVerifier {
fn as_policy(&self) -> &Policy<'_, PyCryptoOps> {
&self.policy.borrow_dependent().0
}
}

#[pyo3::pymethods]
impl PyClientVerifier {
#[getter]
fn validation_time<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult<&'p pyo3::PyAny> {
datetime_to_py(py, &self.as_policy().validation_time)
}

#[getter]
fn max_chain_depth(&self) -> u8 {
self.as_policy().max_chain_depth
}

fn verify(
&self,
py: pyo3::Python<'_>,
leaf: pyo3::Py<PyCertificate>,
intermediates: Vec<pyo3::Py<PyCertificate>>,
) -> CryptographyResult<pyo3::Py<pyo3::types::PyTuple>> {
let policy = self.as_policy();
let store = self.store.get();

let chain = cryptography_x509_verification::verify(
&VerificationCertificate::new(
leaf.get().raw.borrow_dependent().clone(),
leaf.clone_ref(py),
),
intermediates.iter().map(|i| {
VerificationCertificate::new(
i.get().raw.borrow_dependent().clone(),
i.clone_ref(py),
)
}),
policy,
store.raw.borrow_dependent(),
)
.map_err(|e| VerificationError::new_err(format!("validation failed: {e:?}")))?;

let py_chain = pyo3::types::PyList::empty(py);
for c in &chain {
py_chain.append(c.extra())?;
}

// NOTE: These `unwrap()` cannot fail, since the underlying policy
// enforces the presence of a SAN and the well-formedness of the
// extension set.
let leaf_san = &chain[0]
.certificate()
.extensions()
.unwrap()
.get_extension(&SUBJECT_ALTERNATIVE_NAME_OID)
.unwrap();

let py_san =
types::SUBJECT_ALTERNATIVE_NAME
.get(py)?
.call1((x509::parse_general_names(
py,
&leaf_san.value::<SubjectAlternativeName<'_>>()?,
)?,))?;

let py_oid = oid_to_py_oid(py, &leaf_san.extn_id)?;
let py_san_ext = types::EXTENSION
.get(py)?
.call1((py_oid, leaf_san.critical, py_san))?;

Ok((py_san_ext, py_chain).into_py(py))
}
}

#[pyo3::pyclass(
frozen,
name = "ServerVerifier",
Expand Down Expand Up @@ -377,6 +455,7 @@ impl PyStore {
}

pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult<()> {
module.add_class::<PyClientVerifier>()?;
module.add_class::<PyServerVerifier>()?;
module.add_class::<PyStore>()?;
module.add_class::<PolicyBuilder>()?;
Expand Down
25 changes: 25 additions & 0 deletions tests/x509/verification/test_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ def test_store_rejects_non_certificates(self):
Store(["not a cert"]) # type: ignore[list-item]


class TestClientVerifier:
def test_verify(self):
# expires 2018-11-16 01:15:03 UTC
leaf = _load_cert(
os.path.join("x509", "cryptography.io.pem"),
x509.load_pem_x509_certificate,
)

store = Store([leaf])

builder = PolicyBuilder().store(store)
builder = builder.time(
datetime.datetime.fromisoformat("2018-11-16T00:00:00+00:00")
)
verifier = builder.build_client_verifier()

san, chain = verifier.verify(leaf, [])
assert isinstance(san, x509.Extension)
assert isinstance(san.value, x509.SubjectAlternativeName)
assert chain == [leaf]

dns_names = san.value.get_values_for_type(x509.DNSName)
assert set(dns_names) == {"www.cryptography.io", "cryptography.io"}


class TestServerVerifier:
@pytest.mark.parametrize(
("validation_time", "valid"),
Expand Down

0 comments on commit 8a540d3

Please sign in to comment.