#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms)]
#[cfg(not(feature = "std"))]
#[macro_use]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::vec::Vec;
#[cfg(not(feature = "std"))]
use alloc::borrow::Cow;
#[cfg(feature = "std")]
use std::borrow::Cow;
use der::Tagged;
use signature::Error as SignatureError;
use spki::{AlgorithmIdentifierRef, SubjectPublicKeyInfoRef};
use x509_cert::Certificate;
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde_der;
pub use x509_cert::ext::pkix::constraints::name::NameConstraints;
type GeneralSubtrees = x509_cert::ext::pkix::constraints::name::GeneralSubtrees;
#[derive(Clone, Debug)]
pub struct DerError {
#[cfg_attr(not(feature = "std"), allow(dead_code))]
inner: Option<der::Error>,
message: BoxStr,
}
impl PartialEq for DerError {
fn eq(&self, other: &Self) -> bool {
self.message == other.message
}
}
impl Eq for DerError {}
#[cfg(not(feature = "std"))]
type BoxStr = alloc::boxed::Box<str>;
#[cfg(feature = "std")]
type BoxStr = std::boxed::Box<str>;
impl DerError {
#[must_use]
pub fn new(e: der::Error) -> Self {
#[cfg(feature = "std")]
let message = e.to_string().into_boxed_str();
#[cfg(not(feature = "std"))]
let message = {
use core::fmt::Write as _;
let mut s = alloc::string::String::new();
let _ = write!(&mut s, "{}", e);
s.into_boxed_str()
};
Self {
inner: Some(e),
message,
}
}
}
impl core::fmt::Display for DerError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.message)
}
}
#[cfg(feature = "std")]
impl std::error::Error for DerError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.inner.as_ref().map(|e| e as &dyn std::error::Error)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
const _: () = {
use serde::{Deserializer, Serializer};
#[derive(serde::Serialize, serde::Deserialize)]
struct Wire<'a> {
#[serde(borrow)]
message: Cow<'a, str>,
}
impl serde::Serialize for DerError {
fn serialize<S: Serializer>(&self, s: S) -> core::result::Result<S::Ok, S::Error> {
Wire {
message: Cow::Borrowed(&self.message),
}
.serialize(s)
}
}
impl<'de> serde::Deserialize<'de> for DerError {
fn deserialize<D: Deserializer<'de>>(d: D) -> core::result::Result<Self, D::Error> {
let Wire { message } = Wire::deserialize(d)?;
Ok(Self {
inner: None,
message: message.into_owned().into_boxed_str(),
})
}
}
};
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Error {
SignatureInvalid {
index: usize,
},
MalformedCertificate {
index: usize,
},
ValidityPeriod {
index: usize,
},
ChainBroken {
index: usize,
},
NoTrustedPath,
PathTooLong,
NotCA {
index: usize,
},
KeyUsageMissing {
index: usize,
},
CrlSignMissing {
index: usize,
},
UnhandledCriticalExtension {
index: usize,
},
NameConstraintViolation {
index: usize,
},
PolicyViolation {
index: usize,
},
Der(DerError),
ValidityPeriodExceedsMax {
index: usize,
},
AlgorithmNotAllowed {
index: usize,
},
KeyTooSmall {
index: usize,
},
MissingSan,
MissingRfc822San,
MissingEku,
MissingLeafPolicyOid {
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
required: der::asn1::ObjectIdentifier,
},
SubjectDnAttrRuleUnmet,
DuplicateCertificate {
first: usize,
second: usize,
},
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::SignatureInvalid { index } => {
write!(f, "signature invalid at chain index {index}")
}
Self::ValidityPeriod { index } => {
write!(f, "validity period check failed at chain index {index}")
}
Self::MalformedCertificate { index } => {
write!(f, "malformed certificate at chain index {index}")
}
Self::ChainBroken { index } => {
write!(f, "issuer/subject linkage broken at chain index {index}")
}
Self::NoTrustedPath => write!(f, "no path to a trusted anchor"),
Self::PathTooLong => write!(f, "path length exceeds maximum"),
Self::NotCA { index } => write!(f, "certificate at index {index} is not a CA"),
Self::KeyUsageMissing { index } => {
write!(f, "keyCertSign missing at chain index {index}")
}
Self::CrlSignMissing { index } => {
write!(f, "cRLSign missing at chain index {index}")
}
Self::UnhandledCriticalExtension { index } => {
write!(f, "unhandled critical extension at chain index {index}")
}
Self::NameConstraintViolation { index } => {
write!(f, "name constraints violated at certificate index {index}")
}
Self::PolicyViolation { index } => {
write!(f, "certificate policy violation at chain index {index}")
}
Self::Der(e) => write!(f, "DER error: {e}"),
Self::ValidityPeriodExceedsMax { index } => {
write!(f, "validity period exceeds maximum at chain index {index}")
}
Self::AlgorithmNotAllowed { index } => {
write!(f, "signature algorithm not allowed at chain index {index}")
}
Self::KeyTooSmall { index } => {
write!(f, "RSA key too small at chain index {index}")
}
Self::MissingSan => write!(f, "leaf certificate is missing SubjectAltName"),
Self::MissingRfc822San => write!(
f,
"leaf certificate SubjectAltName contains no rfc822Name entry"
),
Self::MissingEku => {
write!(
f,
"leaf certificate is missing required ExtendedKeyUsage OID(s)"
)
}
Self::MissingLeafPolicyOid { required } => {
write!(
f,
"leaf certificate is missing required CertificatePolicies OID {required}"
)
}
Self::SubjectDnAttrRuleUnmet => {
write!(
f,
"leaf certificate Subject DN does not satisfy the required attribute rule"
)
}
Self::DuplicateCertificate { first, second } => {
write!(
f,
"duplicate certificate (issuer+serial) at chain indices {first} and {second}"
)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Der(e) => Some(e),
Self::SignatureInvalid { .. }
| Self::MalformedCertificate { .. }
| Self::ValidityPeriod { .. }
| Self::ChainBroken { .. }
| Self::NoTrustedPath
| Self::PathTooLong
| Self::NotCA { .. }
| Self::KeyUsageMissing { .. }
| Self::CrlSignMissing { .. }
| Self::UnhandledCriticalExtension { .. }
| Self::NameConstraintViolation { .. }
| Self::PolicyViolation { .. }
| Self::ValidityPeriodExceedsMax { .. }
| Self::AlgorithmNotAllowed { .. }
| Self::KeyTooSmall { .. }
| Self::MissingSan
| Self::MissingRfc822San
| Self::MissingEku
| Self::MissingLeafPolicyOid { .. }
| Self::SubjectDnAttrRuleUnmet
| Self::DuplicateCertificate { .. } => None,
}
}
}
impl From<der::Error> for Error {
fn from(e: der::Error) -> Self {
Self::Der(DerError::new(e))
}
}
pub type Result<T> = core::result::Result<T, Error>;
pub trait SignatureVerifier {
fn verify_signature(
&self,
algorithm: AlgorithmIdentifierRef<'_>,
issuer_spki: SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct TrustAnchor {
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub subject: x509_cert::name::Name,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::option"))]
pub name_constraints: Option<x509_cert::ext::pkix::constraints::name::NameConstraints>,
}
impl TrustAnchor {
#[must_use]
pub const fn new(
subject: x509_cert::name::Name,
subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
) -> Self {
Self {
subject,
subject_public_key_info,
name_constraints: None,
}
}
#[must_use]
pub fn from_cert(cert: Certificate) -> Self {
let name_constraints = find_cert_ext(&cert, OID_NAME_CONSTRAINTS);
Self {
subject: cert.tbs_certificate.subject,
subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
name_constraints,
}
}
}
impl From<&Certificate> for TrustAnchor {
fn from(cert: &Certificate) -> Self {
Self {
subject: cert.tbs_certificate.subject.clone(),
subject_public_key_info: cert.tbs_certificate.subject_public_key_info.clone(),
name_constraints: find_cert_ext(cert, OID_NAME_CONSTRAINTS),
}
}
}
impl TryFrom<Certificate> for TrustAnchor {
type Error = DerError;
fn try_from(cert: Certificate) -> core::result::Result<Self, Self::Error> {
let name_constraints =
try_find_cert_ext(&cert, OID_NAME_CONSTRAINTS).map_err(DerError::new)?;
Ok(Self {
subject: cert.tbs_certificate.subject,
subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
name_constraints,
})
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DnAttrRule {
Field(
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
der::asn1::ObjectIdentifier,
),
AllOf(Vec<DnAttrRule>),
AnyOf(Vec<DnAttrRule>),
}
fn dn_contains_oid(subject: &x509_cert::name::Name, oid: &der::asn1::ObjectIdentifier) -> bool {
subject
.0
.iter()
.any(|rdn| rdn.0.iter().any(|ava| ava.oid == *oid))
}
fn evaluate_dn_attr_rule(subject: &x509_cert::name::Name, rule: &DnAttrRule) -> bool {
match rule {
DnAttrRule::Field(oid) => dn_contains_oid(subject, oid),
DnAttrRule::AllOf(rules) => rules.iter().all(|r| evaluate_dn_attr_rule(subject, r)),
DnAttrRule::AnyOf(rules) => rules.iter().any(|r| evaluate_dn_attr_rule(subject, r)),
}
}
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidationPolicy {
pub max_path_len: u8,
pub current_time_unix: u64,
pub enforce_key_usage: bool,
pub require_crl_sign_on_cas: bool,
pub initial_explicit_policy: bool,
pub initial_any_policy_inhibit: bool,
pub initial_policy_mapping_inhibit: bool,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::vec"))]
pub initial_policy_set: Vec<der::asn1::ObjectIdentifier>,
pub max_validity_secs: Option<u64>,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::option_vec"))]
pub allowed_signature_algs: Option<Vec<der::asn1::ObjectIdentifier>>,
pub min_rsa_key_bits: Option<u32>,
pub require_subject_alt_name: bool,
pub require_rfc822_san: bool,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::option_vec"))]
pub required_leaf_eku: Option<Vec<der::asn1::ObjectIdentifier>>,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::option_vec"))]
pub required_leaf_policy_oids: Option<Vec<der::asn1::ObjectIdentifier>>,
pub required_leaf_subject_dn_attrs: Option<DnAttrRule>,
}
impl ValidationPolicy {
#[must_use]
pub fn new(now_unix: u64) -> Self {
Self {
current_time_unix: now_unix,
..Default::default()
}
}
}
impl Default for ValidationPolicy {
fn default() -> Self {
Self {
max_path_len: 10,
current_time_unix: 0, enforce_key_usage: true,
require_crl_sign_on_cas: false,
initial_explicit_policy: false,
initial_any_policy_inhibit: false,
initial_policy_mapping_inhibit: false,
initial_policy_set: Vec::new(),
max_validity_secs: None,
allowed_signature_algs: None,
min_rsa_key_bits: None,
require_subject_alt_name: false,
require_rfc822_san: false,
required_leaf_eku: None,
required_leaf_policy_oids: None,
required_leaf_subject_dn_attrs: None,
}
}
}
pub trait Profile {
fn id(&self) -> &'static str;
fn version(&self) -> &'static str;
#[must_use]
fn policy(&self, now_unix: u64) -> ValidationPolicy;
fn policy_oids(&self) -> &[der::asn1::ObjectIdentifier];
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ValidatedPath {
pub anchor_index: usize,
pub depth: usize,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub leaf_subject: x509_cert::name::Name,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub leaf_issuer: x509_cert::name::Name,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub leaf_serial: x509_cert::serial_number::SerialNumber,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub leaf_spki: spki::SubjectPublicKeyInfoOwned,
pub valid_policy_tree: Option<Vec<PolicyTreeNode>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct PolicyTreeNode {
pub depth: usize,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der"))]
pub valid_policy: der::asn1::ObjectIdentifier,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::vec"))]
pub expected_policy_set: Vec<der::asn1::ObjectIdentifier>,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_der::vec"))]
pub qualifiers: Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo>,
}
impl ValidatedPath {
pub fn policy_qualifiers(
&self,
) -> impl Iterator<
Item = (
&der::asn1::ObjectIdentifier,
&x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo,
),
> + '_ {
self.valid_policy_tree
.as_ref()
.into_iter()
.flat_map(|tree| tree.iter())
.flat_map(|node| node.qualifiers.iter().map(move |q| (&node.valid_policy, q)))
}
}
pub fn validate_path<V>(
chain: &[Certificate],
anchors: &[TrustAnchor],
policy: &ValidationPolicy,
verifier: &V,
) -> Result<ValidatedPath>
where
V: SignatureVerifier,
{
check_inputs(chain, anchors)?;
check_oid_consistency(chain)?;
let num_non_si_intermediates = chain[1..]
.iter()
.filter(|c| !is_self_issued_cert(c))
.count();
if num_non_si_intermediates > policy.max_path_len as usize {
return Err(Error::PathTooLong);
}
let last_cert = chain.last().ok_or(Error::NoTrustedPath)?;
let is_self_issued = names_match(
&last_cert.tbs_certificate.issuer,
&last_cert.tbs_certificate.subject,
);
let mut last_err = Error::NoTrustedPath;
for (anchor_index, anchor) in anchors.iter().enumerate() {
if !names_match(&anchor.subject, &last_cert.tbs_certificate.issuer) {
continue;
}
if is_self_issued
&& !spki_key_matches(
&anchor.subject_public_key_info,
&last_cert.tbs_certificate.subject_public_key_info,
)
{
continue;
}
match chain_walk(chain, anchor, policy, verifier) {
Ok(final_policy_tree) => {
let leaf_tbs = &chain[0].tbs_certificate;
let valid_policy_tree = final_policy_tree.map(|nodes| {
nodes
.into_iter()
.map(|n| PolicyTreeNode {
depth: n.depth,
valid_policy: n.valid_policy,
expected_policy_set: n.expected_policy_set,
qualifiers: n.qualifiers,
})
.collect()
});
return Ok(ValidatedPath {
anchor_index,
depth: chain.len().saturating_sub(1),
leaf_subject: leaf_tbs.subject.clone(),
leaf_issuer: leaf_tbs.issuer.clone(),
leaf_serial: leaf_tbs.serial_number.clone(),
leaf_spki: leaf_tbs.subject_public_key_info.clone(),
valid_policy_tree,
});
}
Err(e) => last_err = e,
}
}
Err(last_err)
}
pub fn validate_path_with_profile<V, P>(
chain: &[Certificate],
anchors: &[TrustAnchor],
profile: &P,
now_unix: u64,
verifier: &V,
) -> Result<ValidatedPath>
where
V: SignatureVerifier,
P: Profile,
{
let mut policy = profile.policy(now_unix);
policy.current_time_unix = now_unix;
validate_path(chain, anchors, &policy, verifier)
}
fn spki_key_matches(
a: &spki::SubjectPublicKeyInfoOwned,
b: &spki::SubjectPublicKeyInfoOwned,
) -> bool {
a.algorithm.oid == b.algorithm.oid && a.subject_public_key == b.subject_public_key
}
fn check_inputs(chain: &[Certificate], anchors: &[TrustAnchor]) -> Result<()> {
if chain.is_empty() || anchors.is_empty() {
return Err(Error::NoTrustedPath);
}
for i in 0..chain.len() {
for j in (i + 1)..chain.len() {
let a = &chain[i].tbs_certificate;
let b = &chain[j].tbs_certificate;
if names_match(&a.issuer, &b.issuer) && a.serial_number == b.serial_number {
return Err(Error::DuplicateCertificate {
first: i,
second: j,
});
}
}
}
Ok(())
}
fn check_oid_consistency(chain: &[Certificate]) -> Result<()> {
for (index, cert) in chain.iter().enumerate() {
if cert.signature_algorithm.oid != cert.tbs_certificate.signature.oid {
return Err(Error::MalformedCertificate { index });
}
}
Ok(())
}
const OID_KEY_USAGE: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
const OID_BASIC_CONSTRAINTS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.19");
const OID_SUBJECT_ALT_NAME: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.17");
const OID_EXTENDED_KEY_USAGE: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.37");
const OID_NAME_CONSTRAINTS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.30");
const OID_CERTIFICATE_POLICIES: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.32");
const OID_POLICY_MAPPINGS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.33");
const OID_POLICY_CONSTRAINTS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.36");
const OID_INHIBIT_ANY_POLICY: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.54");
const OID_ANY_POLICY: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.32.0");
const OID_EMAIL_ADDRESS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.1");
const HANDLED_CRITICAL_OIDS: &[der::asn1::ObjectIdentifier] = &[
OID_KEY_USAGE,
OID_BASIC_CONSTRAINTS,
OID_SUBJECT_ALT_NAME,
OID_EXTENDED_KEY_USAGE,
OID_NAME_CONSTRAINTS,
OID_CERTIFICATE_POLICIES,
OID_POLICY_MAPPINGS,
OID_POLICY_CONSTRAINTS,
OID_INHIBIT_ANY_POLICY,
];
fn check_critical_extensions(cert: &Certificate, index: usize) -> Result<()> {
for ext in cert.tbs_certificate.extensions.as_deref().unwrap_or(&[]) {
if ext.critical && !HANDLED_CRITICAL_OIDS.contains(&ext.extn_id) {
return Err(Error::UnhandledCriticalExtension { index });
}
}
Ok(())
}
#[derive(Clone, Debug)]
struct PolicyNode {
depth: usize,
valid_policy: der::asn1::ObjectIdentifier,
expected_policy_set: Vec<der::asn1::ObjectIdentifier>,
qualifiers: Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo>,
}
fn cert_any_policy_qualifiers(
cp: &x509_cert::ext::pkix::certpolicy::CertificatePolicies,
) -> Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo> {
cp.0.iter()
.find(|pi| pi.policy_identifier == OID_ANY_POLICY)
.and_then(|pi| pi.policy_qualifiers.clone())
.unwrap_or_default()
}
fn init_policy_tree() -> Vec<PolicyNode> {
vec![PolicyNode {
depth: 0,
valid_policy: OID_ANY_POLICY,
expected_policy_set: vec![OID_ANY_POLICY],
qualifiers: Vec::new(),
}]
}
fn prune_policy_tree(tree: &mut Vec<PolicyNode>, cert_depth: usize) {
let mut d = cert_depth;
loop {
let prune_depth = d - 1; if prune_depth == 0 {
break; }
let child_policies: Vec<der::asn1::ObjectIdentifier> = tree
.iter()
.filter(|n| n.depth == d)
.map(|n| n.valid_policy)
.collect();
tree.retain(|n| {
if n.depth != prune_depth {
return true; }
child_policies
.iter()
.any(|cp| n.expected_policy_set.contains(cp))
});
d -= 1;
}
}
fn policy_tree_post_op_prune(tree: &mut Vec<PolicyNode>, prune_depth: usize) -> bool {
if prune_depth > 0 {
prune_policy_tree(tree, prune_depth);
}
!tree.iter().any(|nd| nd.depth >= 1)
}
fn has_key_cert_sign(cert: &Certificate) -> der::Result<Option<bool>> {
use x509_cert::ext::pkix::KeyUsage;
try_find_cert_ext::<KeyUsage>(cert, OID_KEY_USAGE).map(|opt| opt.map(|ku| ku.key_cert_sign()))
}
fn has_crl_sign(cert: &Certificate) -> der::Result<Option<bool>> {
use x509_cert::ext::pkix::KeyUsage;
try_find_cert_ext::<KeyUsage>(cert, OID_KEY_USAGE).map(|opt| opt.map(|ku| ku.crl_sign()))
}
fn find_cert_ext<T: der::DecodeOwned>(
cert: &Certificate,
oid: der::asn1::ObjectIdentifier,
) -> Option<T> {
cert.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == oid)
.and_then(|e| T::from_der(e.extn_value.as_bytes()).ok())
}
fn try_find_cert_ext<T: der::DecodeOwned>(
cert: &Certificate,
oid: der::asn1::ObjectIdentifier,
) -> der::Result<Option<T>> {
cert.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == oid)
.map_or(Ok(None), |e| T::from_der(e.extn_value.as_bytes()).map(Some))
}
fn cert_subject_alt_names(
cert: &Certificate,
index: usize,
) -> crate::Result<Option<x509_cert::ext::pkix::SubjectAltName>> {
try_find_cert_ext(cert, OID_SUBJECT_ALT_NAME).map_err(|_| Error::MalformedCertificate { index })
}
fn cert_name_constraints(
cert: &Certificate,
index: usize,
) -> crate::Result<Option<NameConstraints>> {
let nc = try_find_cert_ext::<NameConstraints>(cert, OID_NAME_CONSTRAINTS)
.map_err(|_| Error::MalformedCertificate { index })?;
if let Some(nc) = &nc {
let subtrees_iter = nc
.permitted_subtrees
.iter()
.flatten()
.chain(nc.excluded_subtrees.iter().flatten());
for st in subtrees_iter {
if st.minimum != 0 || st.maximum.is_some() {
return Err(Error::MalformedCertificate { index });
}
}
}
Ok(nc)
}
fn time_to_unix_secs(t: &x509_cert::time::Time) -> u64 {
t.to_unix_duration().as_secs()
}
fn check_validity(cert: &Certificate, now_unix: u64, index: usize) -> Result<()> {
let not_before = time_to_unix_secs(&cert.tbs_certificate.validity.not_before);
let not_after = time_to_unix_secs(&cert.tbs_certificate.validity.not_after);
if now_unix >= not_before && now_unix <= not_after {
Ok(())
} else {
Err(Error::ValidityPeriod { index })
}
}
#[must_use]
pub fn names_match(a: &x509_cert::name::Name, b: &x509_cert::name::Name) -> bool {
let a_rdns = a.0.as_slice();
let b_rdns = b.0.as_slice();
if a_rdns.len() != b_rdns.len() {
return false;
}
for (a_rdn, b_rdn) in a_rdns.iter().zip(b_rdns) {
let a_avas = a_rdn.0.as_slice();
let b_avas = b_rdn.0.as_slice();
if a_avas.len() != b_avas.len() {
return false;
}
for a_ava in a_avas {
let found = b_avas.iter().any(|b_ava| {
b_ava.oid == a_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
});
if !found {
return false;
}
}
for b_ava in b_avas {
let found = a_avas.iter().any(|a_ava| {
a_ava.oid == b_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
});
if !found {
return false;
}
}
}
true
}
pub fn cert_is_ca(cert: &Certificate) -> core::result::Result<bool, DerError> {
use der::Decode as _;
use x509_cert::ext::pkix::BasicConstraints;
let Some(ext) = cert
.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|e| e.extn_id == OID_BASIC_CONSTRAINTS)
else {
return Ok(false);
};
let bc = BasicConstraints::from_der(ext.extn_value.as_bytes()).map_err(DerError::new)?;
Ok(bc.ca)
}
fn is_self_issued_cert(cert: &Certificate) -> bool {
!cert.tbs_certificate.subject.is_empty()
&& names_match(&cert.tbs_certificate.subject, &cert.tbs_certificate.issuer)
}
fn cert_has_san_identity(cert: &Certificate) -> bool {
if !cert.tbs_certificate.subject.is_empty() {
return false;
}
cert.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.any(|ext| ext.extn_id == OID_SUBJECT_ALT_NAME && ext.critical)
}
fn ava_values_match(a: &der::Any, b: &der::Any) -> bool {
let a_str = any_to_str_bytes(a);
let b_str = any_to_str_bytes(b);
match (a_str, b_str) {
(Some(a_bytes), Some(b_bytes)) => normalized_eq(a_bytes.as_ref(), b_bytes.as_ref()),
(None, None) => a.tag() == b.tag() && a.value() == b.value(),
_ => false,
}
}
fn any_to_str_bytes(a: &der::Any) -> Option<Cow<'_, [u8]>> {
use der::Tag;
match a.tag() {
Tag::Utf8String | Tag::PrintableString | Tag::Ia5String | Tag::VisibleString => {
Some(Cow::Borrowed(a.value()))
}
Tag::BmpString => bmp_string_to_utf8(a.value()).map(Cow::Owned),
_ => None,
}
}
fn bmp_string_to_utf8(bytes: &[u8]) -> Option<Vec<u8>> {
if bytes.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity((bytes.len() / 2) * 3);
let mut buf = [0u8; 4];
for chunk in bytes.chunks_exact(2) {
let cp = u16::from_be_bytes([chunk[0], chunk[1]]);
let ch = char::from_u32(u32::from(cp))?;
let s = ch.encode_utf8(&mut buf);
out.extend_from_slice(s.as_bytes());
}
Some(out)
}
fn normalized_eq(a: &[u8], b: &[u8]) -> bool {
NormalizedIter::new(a).eq(NormalizedIter::new(b))
}
struct NormalizedIter<'a> {
bytes: &'a [u8],
pos: usize,
pending_space: bool,
}
impl<'a> NormalizedIter<'a> {
fn new(bytes: &'a [u8]) -> Self {
let start = bytes.iter().position(|&b| b != b' ').unwrap_or(bytes.len());
let end = bytes[start..]
.iter()
.rposition(|&b| b != b' ')
.map_or(start, |i| start + i + 1);
Self {
bytes: &bytes[start..end],
pos: 0,
pending_space: false,
}
}
}
impl Iterator for NormalizedIter<'_> {
type Item = u8;
fn next(&mut self) -> Option<u8> {
if self.pending_space {
self.pending_space = false;
while self.pos < self.bytes.len() && self.bytes[self.pos] == b' ' {
self.pos += 1;
}
}
if self.pos >= self.bytes.len() {
return None;
}
let b = self.bytes[self.pos];
self.pos += 1;
if b == b' ' {
self.pending_space = true;
Some(b' ')
} else {
Some(b.to_ascii_lowercase())
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct NcTypeMask(u32);
impl NcTypeMask {
const EMPTY: Self = Self(0);
const RFC822: Self = Self(1 << 0);
const DNS: Self = Self(1 << 1);
const DIRECTORY_NAME: Self = Self(1 << 2);
const URI: Self = Self(1 << 3);
const IP_ADDRESS: Self = Self(1 << 4);
const fn intersects(self, other: Self) -> bool {
self.0 & other.0 != 0
}
}
impl core::ops::BitOr for NcTypeMask {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl core::ops::BitOrAssign for NcTypeMask {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
const fn name_type_bit(name: &x509_cert::ext::pkix::name::GeneralName) -> NcTypeMask {
use x509_cert::ext::pkix::name::GeneralName;
match name {
GeneralName::Rfc822Name(_) => NcTypeMask::RFC822,
GeneralName::DnsName(_) => NcTypeMask::DNS,
GeneralName::DirectoryName(_) => NcTypeMask::DIRECTORY_NAME,
GeneralName::UniformResourceIdentifier(_) => NcTypeMask::URI,
GeneralName::IpAddress(_) => NcTypeMask::IP_ADDRESS,
_ => NcTypeMask::EMPTY,
}
}
fn dn_within_subtree(subject: &x509_cert::name::Name, constraint: &x509_cert::name::Name) -> bool {
let c_rdns = &constraint.0;
let s_rdns = &subject.0;
if c_rdns.len() > s_rdns.len() {
return false;
}
c_rdns.iter().zip(s_rdns.iter()).all(|(c_rdn, s_rdn)| {
if c_rdn.0.len() != s_rdn.0.len() {
return false;
}
c_rdn.0.iter().all(|c_ava| {
s_rdn
.0
.iter()
.any(|s_ava| c_ava.oid == s_ava.oid && ava_values_match(&c_ava.value, &s_ava.value))
})
})
}
fn same_nc_variant(
a: &x509_cert::ext::pkix::name::GeneralName,
b: &x509_cert::ext::pkix::name::GeneralName,
) -> bool {
name_type_bit(a) != NcTypeMask::EMPTY && name_type_bit(a) == name_type_bit(b)
}
fn name_matches_subtree(
name: &x509_cert::ext::pkix::name::GeneralName,
subtree: &x509_cert::ext::pkix::constraints::name::GeneralSubtree,
) -> bool {
use x509_cert::ext::pkix::name::GeneralName;
match (name, &subtree.base) {
(GeneralName::DnsName(subj), GeneralName::DnsName(constr)) => {
matches_dns_name(subj.as_str(), constr.as_str())
}
(GeneralName::DirectoryName(subj), GeneralName::DirectoryName(constr)) => {
dn_within_subtree(subj, constr)
}
(GeneralName::Rfc822Name(subj), GeneralName::Rfc822Name(constr)) => {
matches_rfc822_name(subj.as_str(), constr.as_str())
}
(
GeneralName::UniformResourceIdentifier(subj),
GeneralName::UniformResourceIdentifier(constr),
) => matches_uri(subj.as_str(), constr.as_str()),
(GeneralName::IpAddress(subj), GeneralName::IpAddress(constr)) => {
matches_ip_address(subj.as_bytes(), constr.as_bytes())
}
_ => false,
}
}
fn matches_dns_name(subject: &str, constraint: &str) -> bool {
if constraint.is_empty() {
return false;
}
if let Some(suffix) = constraint.strip_prefix('.') {
if subject.eq_ignore_ascii_case(suffix) {
return false;
}
let dot_suffix = constraint; subject.len() > dot_suffix.len()
&& subject[subject.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
} else {
subject.eq_ignore_ascii_case(constraint)
|| (subject.len() > constraint.len() + 1
&& subject.as_bytes()[subject.len() - constraint.len() - 1] == b'.'
&& subject[subject.len() - constraint.len()..].eq_ignore_ascii_case(constraint))
}
}
fn matches_rfc822_name(subject: &str, constraint: &str) -> bool {
if constraint.contains('@') {
return subject.eq_ignore_ascii_case(constraint);
}
let Some((_, domain)) = subject.split_once('@') else {
return false; };
if let Some(suffix) = constraint.strip_prefix('.') {
if domain.eq_ignore_ascii_case(suffix) {
return false; }
let dot_suffix = constraint;
domain.len() > dot_suffix.len()
&& domain[domain.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
} else {
domain.eq_ignore_ascii_case(constraint)
}
}
fn matches_uri_host(host: &str, constraint: &str) -> bool {
if constraint.is_empty() {
return false;
}
if let Some(suffix) = constraint.strip_prefix('.') {
if host.eq_ignore_ascii_case(suffix) {
return false;
}
let dot_suffix = constraint;
host.len() > dot_suffix.len()
&& host[host.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
} else {
host.eq_ignore_ascii_case(constraint)
}
}
fn matches_uri(subject_uri: &str, constraint: &str) -> bool {
let host = if let Some(after_scheme) = subject_uri.find("://") {
let rest = &subject_uri[after_scheme + 3..];
let rest = rest.split_once('@').map_or(rest, |(_, h)| h);
let host_end = rest.find(['/', '?', '#', ':']).unwrap_or(rest.len());
&rest[..host_end]
} else {
return false; };
matches_uri_host(host, constraint)
}
fn matches_ip_address(subject_bytes: &[u8], constraint_bytes: &[u8]) -> bool {
let (expected_subj_len, half) = match constraint_bytes.len() {
8 => (4usize, 4usize),
32 => (16usize, 16usize),
_ => return false,
};
if subject_bytes.len() != expected_subj_len {
return false;
}
let (addr, mask) = constraint_bytes.split_at(half);
subject_bytes
.iter()
.zip(addr.iter().zip(mask.iter()))
.all(|(s, (a, m))| s & m == a & m)
}
#[cfg(feature = "p256")]
const OID_ECDSA_P256_SHA256: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
#[cfg(feature = "p256")]
#[cfg_attr(docsrs, doc(cfg(feature = "p256")))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct EcdsaP256Verifier;
#[cfg(feature = "p256")]
impl SignatureVerifier for EcdsaP256Verifier {
fn verify_signature(
&self,
algorithm: spki::AlgorithmIdentifierRef<'_>,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
if algorithm.oid != OID_ECDSA_P256_SHA256 {
return Err(SignatureError::new());
}
let vk = VerifyingKey::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
let sig = DerSignature::try_from(signature).map_err(|_| SignatureError::new())?;
vk.verify(message, &sig).map_err(|_| SignatureError::new())
}
}
#[cfg(feature = "p384")]
const OID_ECDSA_P384_SHA384: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3");
#[cfg(feature = "p384")]
#[cfg_attr(docsrs, doc(cfg(feature = "p384")))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct EcdsaP384Verifier;
#[cfg(feature = "p384")]
impl SignatureVerifier for EcdsaP384Verifier {
fn verify_signature(
&self,
algorithm: spki::AlgorithmIdentifierRef<'_>,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
use p384::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
if algorithm.oid != OID_ECDSA_P384_SHA384 {
return Err(SignatureError::new());
}
let vk = VerifyingKey::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
let sig = DerSignature::try_from(signature).map_err(|_| SignatureError::new())?;
vk.verify(message, &sig).map_err(|_| SignatureError::new())
}
}
#[cfg(feature = "rsa")]
const OID_SHA256_WITH_RSA: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
#[cfg(feature = "rsa")]
const OID_SHA384_WITH_RSA: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
#[cfg(feature = "rsa")]
const OID_SHA512_WITH_RSA: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
#[cfg(feature = "rsa")]
#[cfg_attr(docsrs, doc(cfg(feature = "rsa")))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct RsaPkcs1v15Sha256Verifier;
#[cfg(feature = "rsa")]
impl SignatureVerifier for RsaPkcs1v15Sha256Verifier {
fn verify_signature(
&self,
algorithm: spki::AlgorithmIdentifierRef<'_>,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::signature::Verifier as _;
use sha2::Sha256;
if algorithm.oid != OID_SHA256_WITH_RSA {
return Err(SignatureError::new());
}
let vk =
VerifyingKey::<Sha256>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
vk.verify(message, &sig).map_err(|_| SignatureError::new())
}
}
#[cfg(feature = "rsa")]
#[cfg_attr(docsrs, doc(cfg(feature = "rsa")))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct RsaPkcs1v15Sha384Verifier;
#[cfg(feature = "rsa")]
impl SignatureVerifier for RsaPkcs1v15Sha384Verifier {
fn verify_signature(
&self,
algorithm: spki::AlgorithmIdentifierRef<'_>,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::signature::Verifier as _;
use sha2::Sha384;
if algorithm.oid != OID_SHA384_WITH_RSA {
return Err(SignatureError::new());
}
let vk =
VerifyingKey::<Sha384>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
vk.verify(message, &sig).map_err(|_| SignatureError::new())
}
}
#[cfg(feature = "rsa")]
#[cfg_attr(docsrs, doc(cfg(feature = "rsa")))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct RsaPkcs1v15Sha512Verifier;
#[cfg(feature = "rsa")]
impl SignatureVerifier for RsaPkcs1v15Sha512Verifier {
fn verify_signature(
&self,
algorithm: spki::AlgorithmIdentifierRef<'_>,
issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::signature::Verifier as _;
use sha2::Sha512;
if algorithm.oid != OID_SHA512_WITH_RSA {
return Err(SignatureError::new());
}
let vk =
VerifyingKey::<Sha512>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
vk.verify(message, &sig).map_err(|_| SignatureError::new())
}
}
const OID_RSA_ENCRYPTION: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
fn rsa_public_key_bits(spki: &spki::SubjectPublicKeyInfoOwned) -> Option<u32> {
use der::{asn1::UintRef, Reader};
if spki.algorithm.oid != OID_RSA_ENCRYPTION {
return None; }
let raw = spki.subject_public_key.as_bytes()?;
let modulus_byte_len: usize = der::SliceReader::new(raw)
.ok()?
.sequence(|r| {
let modulus: UintRef<'_> = r.decode()?;
let modulus_len = modulus.as_bytes().len();
let _ = r.tlv_bytes()?;
Ok(modulus_len)
})
.ok()?;
u32::try_from(modulus_byte_len.saturating_mul(8)).ok()
}
struct WorkingState<'a> {
working_spki: &'a spki::SubjectPublicKeyInfoOwned,
working_issuer_name: &'a x509_cert::name::Name,
working_issuer_is_san_identity: bool,
nc_permitted: Option<GeneralSubtrees>,
nc_excluded: GeneralSubtrees,
nc_constrained_types: NcTypeMask,
explicit_policy: u32,
inhibit_any: u32,
policy_mapping: u32,
policy_tree: Option<Vec<PolicyNode>>,
}
fn enforce_signature_alg_allowlist(
cert: &Certificate,
policy: &ValidationPolicy,
index: usize,
) -> Result<()> {
if let Some(allowed) = &policy.allowed_signature_algs {
if !allowed.contains(&cert.signature_algorithm.oid) {
return Err(Error::AlgorithmNotAllowed { index });
}
}
Ok(())
}
fn check_max_validity(cert: &Certificate, policy: &ValidationPolicy, index: usize) -> Result<()> {
if let Some(max_secs) = policy.max_validity_secs {
let not_before = cert
.tbs_certificate
.validity
.not_before
.to_unix_duration()
.as_secs();
let not_after = cert
.tbs_certificate
.validity
.not_after
.to_unix_duration()
.as_secs();
if not_after.saturating_sub(not_before) > max_secs {
return Err(Error::ValidityPeriodExceedsMax { index });
}
}
Ok(())
}
fn check_min_rsa_bits(cert: &Certificate, policy: &ValidationPolicy, index: usize) -> Result<()> {
if let Some(min_bits) = policy.min_rsa_key_bits {
if let Some(actual_bits) =
rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info)
{
if actual_bits < min_bits {
return Err(Error::KeyTooSmall { index });
}
}
}
Ok(())
}
fn check_issuer_linkage(
cert: &Certificate,
working_issuer_name: &x509_cert::name::Name,
working_issuer_is_san_identity: bool,
index: usize,
) -> Result<()> {
if !working_issuer_is_san_identity
&& !names_match(working_issuer_name, &cert.tbs_certificate.issuer)
{
return Err(Error::ChainBroken { index });
}
Ok(())
}
fn verify_cert_signature<V: SignatureVerifier>(
cert: &Certificate,
working_spki: &spki::SubjectPublicKeyInfoOwned,
verifier: &V,
index: usize,
) -> Result<()> {
use der::Encode;
use spki::der::referenced::OwnedToRef as _;
let tbs_bytes_owned = {
let mut buf = Vec::new();
cert.tbs_certificate
.encode_to_vec(&mut buf)
.map_err(|e| Error::Der(DerError::new(e)))?;
buf
};
let tbs_bytes: &[u8] = &tbs_bytes_owned;
verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
working_spki.owned_to_ref(),
tbs_bytes,
cert.signature.raw_bytes(),
)
.map_err(|_| Error::SignatureInvalid { index })?;
Ok(())
}
fn chain_walk<V: SignatureVerifier>(
chain: &[Certificate],
anchor: &TrustAnchor,
policy: &ValidationPolicy,
verifier: &V,
) -> Result<Option<Vec<PolicyNode>>> {
use x509_cert::ext::pkix::{InhibitAnyPolicy, PolicyConstraints, PolicyMappings};
let (initial_nc_permitted, initial_nc_excluded) = match &anchor.name_constraints {
None => (None, GeneralSubtrees::default()),
Some(nc) => (
nc.permitted_subtrees.clone(),
nc.excluded_subtrees.clone().unwrap_or_default(),
),
};
let initial_nc_constrained_types: NcTypeMask =
initial_nc_permitted
.as_ref()
.map_or(NcTypeMask::EMPTY, |permitted| {
let mut bits = NcTypeMask::EMPTY;
for st in permitted {
bits |= name_type_bit(&st.base);
}
bits
});
let n = chain.len();
let n_u32 = u32::try_from(n).unwrap_or(u32::MAX);
let initial_explicit_policy: u32 = if policy.initial_explicit_policy {
0
} else {
n_u32.saturating_add(1)
};
let initial_inhibit_any: u32 = if policy.initial_any_policy_inhibit {
0
} else {
n_u32.saturating_add(1)
};
let initial_policy_mapping: u32 = if policy.initial_policy_mapping_inhibit {
0
} else {
n_u32.saturating_add(1)
};
let mut state = WorkingState {
working_spki: &anchor.subject_public_key_info,
working_issuer_name: &anchor.subject,
working_issuer_is_san_identity: false,
nc_permitted: initial_nc_permitted,
nc_excluded: initial_nc_excluded,
nc_constrained_types: initial_nc_constrained_types,
explicit_policy: initial_explicit_policy,
inhibit_any: initial_inhibit_any,
policy_mapping: initial_policy_mapping,
policy_tree: Some(init_policy_tree()),
};
for i in (0..chain.len()).rev() {
let cert = &chain[i];
enforce_signature_alg_allowlist(cert, policy, i)?;
verify_cert_signature(cert, state.working_spki, verifier, i)?;
check_issuer_linkage(
cert,
state.working_issuer_name,
state.working_issuer_is_san_identity,
i,
)?;
check_validity(cert, policy.current_time_unix, i)?;
check_max_validity(cert, policy, i)?;
check_min_rsa_bits(cert, policy, i)?;
check_critical_extensions(cert, i)?;
let cert_depth = n - i;
let cert_cp: Option<x509_cert::ext::pkix::certpolicy::CertificatePolicies> =
try_find_cert_ext(cert, OID_CERTIFICATE_POLICIES)
.map_err(|_| Error::MalformedCertificate { index: i })?;
if let Some(tree) = &mut state.policy_tree {
if let Some(cp_ext) = &cert_cp {
let mut new_nodes: Vec<PolicyNode> = Vec::new();
let mut has_any_policy = false;
for policy_info in &cp_ext.0 {
let p_oid = &policy_info.policy_identifier;
if p_oid == &OID_ANY_POLICY {
has_any_policy = true;
continue;
}
let policy_qualifiers: Vec<_> =
policy_info.policy_qualifiers.clone().unwrap_or_default();
let mut matched_via_i = false;
for _parent in tree.iter().filter(|parent| {
parent.depth == cert_depth - 1 && parent.expected_policy_set.contains(p_oid)
}) {
matched_via_i = true;
new_nodes.push(PolicyNode {
depth: cert_depth,
valid_policy: *p_oid,
expected_policy_set: vec![*p_oid],
qualifiers: policy_qualifiers.clone(),
});
}
if !matched_via_i {
let has_any_parent = tree.iter().any(|parent| {
parent.depth == cert_depth - 1 && parent.valid_policy == OID_ANY_POLICY
});
if has_any_parent {
new_nodes.push(PolicyNode {
depth: cert_depth,
valid_policy: *p_oid,
expected_policy_set: vec![*p_oid],
qualifiers: policy_qualifiers,
});
}
}
}
if has_any_policy {
let may_expand = state.inhibit_any > 0 || (i > 0 && is_self_issued_cert(cert));
if may_expand {
let any_policy_qualifiers = cert_any_policy_qualifiers(cp_ext);
let already_covered: Vec<der::asn1::ObjectIdentifier> =
new_nodes.iter().map(|nd| nd.valid_policy).collect();
for parent in tree.iter().filter(|nd| nd.depth == cert_depth - 1) {
for ep in &parent.expected_policy_set {
if !already_covered.contains(ep) {
new_nodes.push(PolicyNode {
depth: cert_depth,
valid_policy: *ep,
expected_policy_set: vec![*ep],
qualifiers: any_policy_qualifiers.clone(),
});
}
}
}
}
}
tree.extend(new_nodes);
if policy_tree_post_op_prune(tree, cert_depth) {
state.policy_tree = None;
}
} else {
state.policy_tree = None;
}
}
if state.explicit_policy == 0 && state.policy_tree.is_none() {
return Err(Error::PolicyViolation { index: i });
}
let san = cert_subject_alt_names(cert, i)?;
if i == 0 || !is_self_issued_cert(cert) {
check_name_constraints(
cert,
san.as_ref(),
state.nc_permitted.as_ref(),
&state.nc_excluded,
state.nc_constrained_types,
i,
)?;
}
if i == 0 && policy.require_subject_alt_name {
let san_is_nonempty = san.as_ref().is_some_and(|s| !s.0.is_empty());
if !san_is_nonempty {
return Err(Error::MissingSan);
}
}
if i == 0 {
if let Some(required_ekus) = &policy.required_leaf_eku {
use x509_cert::ext::pkix::ExtendedKeyUsage;
match try_find_cert_ext::<ExtendedKeyUsage>(cert, OID_EXTENDED_KEY_USAGE)
.map_err(|_| Error::MalformedCertificate { index: 0 })?
{
None => {
if !required_ekus.is_empty() {
return Err(Error::MissingEku);
}
}
Some(eku) => {
for req_oid in required_ekus {
if !eku.0.iter().any(|e| e == req_oid) {
return Err(Error::MissingEku);
}
}
}
}
}
}
if i == 0 {
if let Some(required_policy_oids) = &policy.required_leaf_policy_oids {
match &cert_cp {
None => {
if let Some(first) = required_policy_oids.first() {
return Err(Error::MissingLeafPolicyOid { required: *first });
}
}
Some(cp_ext) => {
for req_oid in required_policy_oids {
if !cp_ext.0.iter().any(|pi| pi.policy_identifier == *req_oid) {
return Err(Error::MissingLeafPolicyOid { required: *req_oid });
}
}
}
}
}
}
if i == 0 {
if let Some(dn_rule) = &policy.required_leaf_subject_dn_attrs {
if !evaluate_dn_attr_rule(&cert.tbs_certificate.subject, dn_rule) {
return Err(Error::SubjectDnAttrRuleUnmet);
}
}
}
if i == 0 && policy.require_subject_alt_name && policy.require_rfc822_san {
use x509_cert::ext::pkix::name::GeneralName;
let has_rfc822 = san.as_ref().is_some_and(|s| {
s.0.iter()
.any(|name| matches!(name, GeneralName::Rfc822Name(_)))
});
if !has_rfc822 {
return Err(Error::MissingRfc822San);
}
}
if i > 0 {
let bc = try_find_cert_ext::<x509_cert::ext::pkix::BasicConstraints>(
cert,
OID_BASIC_CONSTRAINTS,
)
.map_err(|_| Error::MalformedCertificate { index: i })?;
if !bc.as_ref().is_some_and(|b| b.ca) {
return Err(Error::NotCA { index: i });
}
if policy.enforce_key_usage
&& has_key_cert_sign(cert).map_err(|_| Error::MalformedCertificate { index: i })?
== Some(false)
{
return Err(Error::KeyUsageMissing { index: i });
}
if policy.require_crl_sign_on_cas
&& has_crl_sign(cert).map_err(|_| Error::MalformedCertificate { index: i })?
== Some(false)
{
return Err(Error::CrlSignMissing { index: i });
}
if let Some(path_len) = bc.and_then(|b| b.path_len_constraint) {
let effective_depth = chain[1..i]
.iter()
.filter(|c| !is_self_issued_cert(c))
.count();
if effective_depth > path_len as usize {
return Err(Error::PathTooLong);
}
}
if let Some(pm) = try_find_cert_ext::<PolicyMappings>(cert, OID_POLICY_MAPPINGS)
.map_err(|_| Error::MalformedCertificate { index: i })?
{
for mapping in &pm.0 {
if mapping.issuer_domain_policy == OID_ANY_POLICY
|| mapping.subject_domain_policy == OID_ANY_POLICY
{
return Err(Error::PolicyViolation { index: i });
}
}
if let Some(tree) = &mut state.policy_tree {
if state.policy_mapping > 0 {
let any_policy_qualifiers = cert_cp
.as_ref()
.map(cert_any_policy_qualifiers)
.unwrap_or_default();
for mapping in &pm.0 {
let idp = &mapping.issuer_domain_policy;
let sdp = &mapping.subject_domain_policy;
let mut found = false;
for node in tree.iter_mut() {
if node.depth == cert_depth && &node.valid_policy == idp {
found = true;
node.expected_policy_set.retain(|p| p != idp);
if !node.expected_policy_set.contains(sdp) {
node.expected_policy_set.push(*sdp);
}
}
}
if !found {
let has_any = tree.iter().any(|nd| {
nd.depth == cert_depth && nd.valid_policy == OID_ANY_POLICY
});
if has_any {
tree.push(PolicyNode {
depth: cert_depth,
valid_policy: *idp,
expected_policy_set: vec![*sdp],
qualifiers: any_policy_qualifiers.clone(),
});
}
}
}
} else {
let mapped_policies: Vec<der::asn1::ObjectIdentifier> =
pm.0.iter().map(|m| m.issuer_domain_policy).collect();
tree.retain(|nd| {
nd.depth != cert_depth || !mapped_policies.contains(&nd.valid_policy)
});
let _ = policy_tree_post_op_prune(tree, cert_depth);
}
}
}
if let Some(tree) = state.policy_tree.as_mut() {
if policy_tree_post_op_prune(tree, 0) {
state.policy_tree = None;
}
}
if !is_self_issued_cert(cert) {
state.explicit_policy = state.explicit_policy.saturating_sub(1);
state.policy_mapping = state.policy_mapping.saturating_sub(1);
state.inhibit_any = state.inhibit_any.saturating_sub(1);
}
if let Some(pc) = try_find_cert_ext::<PolicyConstraints>(cert, OID_POLICY_CONSTRAINTS)
.map_err(|_| Error::MalformedCertificate { index: i })?
{
if let Some(req) = pc.require_explicit_policy {
state.explicit_policy = state.explicit_policy.min(req);
}
if let Some(ipm) = pc.inhibit_policy_mapping {
state.policy_mapping = state.policy_mapping.min(ipm);
}
}
if let Some(iap) = try_find_cert_ext::<InhibitAnyPolicy>(cert, OID_INHIBIT_ANY_POLICY)
.map_err(|_| Error::MalformedCertificate { index: i })?
{
state.inhibit_any = state.inhibit_any.min(iap.0);
}
if let Some(nc) = cert_name_constraints(cert, i)? {
if let Some(new_permitted) = nc.permitted_subtrees {
for entry in &new_permitted {
state.nc_constrained_types |= name_type_bit(&entry.base);
}
match state.nc_permitted.as_mut() {
None => {
state.nc_permitted = Some(new_permitted);
}
Some(current) => {
let mut result = GeneralSubtrees::default();
for n in &new_permitted {
let same_type_in_current: GeneralSubtrees = current
.iter()
.filter(|c| same_nc_variant(&c.base, &n.base))
.cloned()
.collect();
if same_type_in_current.is_empty() {
result.push(n.clone());
} else if same_type_in_current
.iter()
.any(|c| name_matches_subtree(&n.base, c))
{
result.push(n.clone());
}
}
for c in current.iter() {
let same_type_in_new: GeneralSubtrees = new_permitted
.iter()
.filter(|n| same_nc_variant(&n.base, &c.base))
.cloned()
.collect();
if same_type_in_new.is_empty() {
result.push(c.clone());
} else if same_type_in_new
.iter()
.any(|n| name_matches_subtree(&c.base, n))
{
let same_type_in_result: &[_] = result.as_slice();
let already_in_result = same_type_in_result.iter().any(|e| {
same_nc_variant(&e.base, &c.base)
&& name_matches_subtree(&e.base, c)
&& name_matches_subtree(&c.base, e)
});
if !already_in_result {
result.push(c.clone());
}
}
}
*current = result;
}
}
}
if let Some(new_excluded) = nc.excluded_subtrees {
for new_entry in &new_excluded {
let already_present = state.nc_excluded.iter().any(|existing| {
same_nc_variant(&existing.base, &new_entry.base)
&& name_matches_subtree(&existing.base, new_entry)
&& name_matches_subtree(&new_entry.base, existing)
});
if !already_present {
state.nc_excluded.push(new_entry.clone());
}
}
}
}
}
state.working_spki = &cert.tbs_certificate.subject_public_key_info;
state.working_issuer_name = &cert.tbs_certificate.subject;
state.working_issuer_is_san_identity = cert_has_san_identity(cert);
}
{
let leaf = &chain[0];
if !is_self_issued_cert(leaf) {
state.explicit_policy = state.explicit_policy.saturating_sub(1);
}
if let Some(pc) = try_find_cert_ext::<PolicyConstraints>(leaf, OID_POLICY_CONSTRAINTS)
.map_err(|_| Error::MalformedCertificate { index: 0 })?
{
if let Some(req) = pc.require_explicit_policy {
state.explicit_policy = state.explicit_policy.min(req);
}
}
}
if !policy.initial_policy_set.is_empty() {
if let Some(tree) = &mut state.policy_tree {
let leaf_depth = n;
let vpns_indices: Vec<usize> = tree
.iter()
.enumerate()
.filter(|(_, nd)| {
nd.depth >= 1
&& tree
.iter()
.any(|p| p.depth == nd.depth - 1 && p.valid_policy == OID_ANY_POLICY)
})
.map(|(idx, _)| idx)
.collect();
let to_delete_vpns: Vec<(usize, der::asn1::ObjectIdentifier)> = vpns_indices
.iter()
.filter(|&&idx| {
tree[idx].valid_policy != OID_ANY_POLICY
&& !policy.initial_policy_set.contains(&tree[idx].valid_policy)
})
.map(|&idx| (tree[idx].depth, tree[idx].valid_policy))
.collect();
if !to_delete_vpns.is_empty() {
tree.retain(|nd| {
!to_delete_vpns
.iter()
.any(|(d, vp)| nd.depth == *d && &nd.valid_policy == vp)
});
for d in 2..=leaf_depth {
let parent_depth = d - 1;
let reachable: Vec<der::asn1::ObjectIdentifier> = tree
.iter()
.filter(|nd| nd.depth == parent_depth)
.flat_map(|nd| nd.expected_policy_set.iter().copied())
.collect();
let any_parent = tree
.iter()
.any(|nd| nd.depth == parent_depth && nd.valid_policy == OID_ANY_POLICY);
tree.retain(|nd| {
if nd.depth != d {
return true;
}
reachable.contains(&nd.valid_policy) || any_parent
});
}
}
let has_leaf_any = tree
.iter()
.any(|nd| nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY);
if has_leaf_any {
let leaf_any_qualifiers: Vec<_> = tree
.iter()
.find(|nd| nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY)
.map(|nd| nd.qualifiers.clone())
.unwrap_or_default();
let leaf_policies: Vec<der::asn1::ObjectIdentifier> = tree
.iter()
.filter(|nd| nd.depth == leaf_depth)
.map(|nd| nd.valid_policy)
.collect();
let mut additions = Vec::new();
for p_oid in &policy.initial_policy_set {
if !leaf_policies.contains(p_oid) {
additions.push(PolicyNode {
depth: leaf_depth,
valid_policy: *p_oid,
expected_policy_set: vec![*p_oid],
qualifiers: leaf_any_qualifiers.clone(),
});
}
}
tree.extend(additions);
tree.retain(|nd| !(nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY));
}
if policy_tree_post_op_prune(tree, leaf_depth) {
state.policy_tree = None;
}
}
}
if state.explicit_policy == 0 && state.policy_tree.is_none() {
return Err(Error::PolicyViolation { index: 0 });
}
Ok(state.policy_tree)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CheckMode {
Excluded,
Permitted,
}
fn check_subject_dn_against_subtrees(
subject: &x509_cert::name::Name,
subject_is_empty: bool,
subtrees: &[x509_cert::ext::pkix::constraints::name::GeneralSubtree],
mode: CheckMode,
nc_constrained_types: NcTypeMask,
index: usize,
) -> crate::Result<()> {
use x509_cert::ext::pkix::name::GeneralName;
if subject_is_empty {
return Ok(());
}
let subject_constrained = nc_constrained_types.intersects(NcTypeMask::DIRECTORY_NAME);
let dn_matches_any = subtrees.iter().any(|st| {
if let GeneralName::DirectoryName(constr) = &st.base {
dn_within_subtree(subject, constr)
} else {
false
}
});
match mode {
CheckMode::Excluded => {
if dn_matches_any {
return Err(Error::NameConstraintViolation { index });
}
}
CheckMode::Permitted => {
if subject_constrained && !dn_matches_any {
return Err(Error::NameConstraintViolation { index });
}
}
}
Ok(())
}
fn check_san_against_subtrees(
san: Option<&x509_cert::ext::pkix::SubjectAltName>,
subtrees: &[x509_cert::ext::pkix::constraints::name::GeneralSubtree],
mode: CheckMode,
nc_constrained_types: NcTypeMask,
index: usize,
) -> crate::Result<()> {
use x509_cert::ext::pkix::name::GeneralName;
let Some(san_ext) = san else {
return Ok(());
};
let type_constrained =
|name: &GeneralName| -> bool { nc_constrained_types.intersects(name_type_bit(name)) };
for name in &san_ext.0 {
match mode {
CheckMode::Excluded => {
if subtrees.iter().any(|st| name_matches_subtree(name, st)) {
return Err(Error::NameConstraintViolation { index });
}
}
CheckMode::Permitted => {
if type_constrained(name)
&& !subtrees.iter().any(|st| name_matches_subtree(name, st))
{
return Err(Error::NameConstraintViolation { index });
}
}
}
}
Ok(())
}
fn check_name_constraints(
cert: &x509_cert::Certificate,
san: Option<&x509_cert::ext::pkix::SubjectAltName>,
nc_permitted: Option<&GeneralSubtrees>,
nc_excluded: &GeneralSubtrees,
nc_constrained_types: NcTypeMask,
index: usize,
) -> crate::Result<()> {
use x509_cert::ext::pkix::name::GeneralName;
let subject = &cert.tbs_certificate.subject;
let subject_is_empty = subject.0.is_empty();
let check_names = |subtrees: &[x509_cert::ext::pkix::constraints::name::GeneralSubtree],
mode: CheckMode|
-> crate::Result<()> {
check_subject_dn_against_subtrees(
subject,
subject_is_empty,
subtrees,
mode,
nc_constrained_types,
index,
)?;
check_san_against_subtrees(san, subtrees, mode, nc_constrained_types, index)?;
Ok(())
};
check_names(nc_excluded.as_slice(), CheckMode::Excluded)?;
if let Some(permitted) = nc_permitted {
check_names(permitted.as_slice(), CheckMode::Permitted)?;
}
let has_rfc822_excluded = nc_excluded
.iter()
.any(|st| matches!(st.base, GeneralName::Rfc822Name(_)));
let has_rfc822_constraint =
nc_constrained_types.intersects(NcTypeMask::RFC822) || has_rfc822_excluded;
if has_rfc822_constraint && !subject_is_empty {
let permitted_rfc822_storage: Option<GeneralSubtrees> =
if nc_constrained_types.intersects(NcTypeMask::RFC822) {
Some(
nc_permitted
.map(|p| {
p.iter()
.filter(|st| matches!(st.base, GeneralName::Rfc822Name(_)))
.cloned()
.collect()
})
.unwrap_or_default(),
)
} else {
None
};
let permitted_rfc822: Option<&[x509_cert::ext::pkix::constraints::name::GeneralSubtree]> =
permitted_rfc822_storage.as_deref();
for rdn in &subject.0 {
for ava in rdn.0.iter() {
if ava.oid != OID_EMAIL_ADDRESS {
continue;
}
let Ok(email_ia5) = ava.value.decode_as::<der::asn1::Ia5StringRef<'_>>() else {
continue;
};
let email_str = email_ia5.as_str();
for st in nc_excluded {
if let GeneralName::Rfc822Name(constraint) = &st.base {
if matches_rfc822_name(email_str, constraint.as_str()) {
return Err(Error::NameConstraintViolation { index });
}
}
}
if let Some(permitted) = permitted_rfc822 {
if !permitted.iter().any(|st| {
if let GeneralName::Rfc822Name(constraint) = &st.base {
matches_rfc822_name(email_str, constraint.as_str())
} else {
false
}
}) {
return Err(Error::NameConstraintViolation { index });
}
}
}
}
}
Ok(())
}
#[cfg(any(feature = "p256", feature = "p384", feature = "rsa"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "p256", feature = "p384", feature = "rsa")))
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DefaultVerifier;
#[cfg(any(feature = "p256", feature = "p384", feature = "rsa"))]
impl SignatureVerifier for DefaultVerifier {
fn verify_signature(
&self,
algorithm: AlgorithmIdentifierRef<'_>,
issuer_spki: SubjectPublicKeyInfoRef<'_>,
message: &[u8],
signature: &[u8],
) -> core::result::Result<(), SignatureError> {
let oid = algorithm.oid;
#[cfg(feature = "p256")]
if oid == OID_ECDSA_P256_SHA256 {
return EcdsaP256Verifier.verify_signature(algorithm, issuer_spki, message, signature);
}
#[cfg(feature = "p384")]
if oid == OID_ECDSA_P384_SHA384 {
return EcdsaP384Verifier.verify_signature(algorithm, issuer_spki, message, signature);
}
#[cfg(feature = "rsa")]
if oid == OID_SHA256_WITH_RSA {
return RsaPkcs1v15Sha256Verifier.verify_signature(
algorithm,
issuer_spki,
message,
signature,
);
}
#[cfg(feature = "rsa")]
if oid == OID_SHA384_WITH_RSA {
return RsaPkcs1v15Sha384Verifier.verify_signature(
algorithm,
issuer_spki,
message,
signature,
);
}
#[cfg(feature = "rsa")]
if oid == OID_SHA512_WITH_RSA {
return RsaPkcs1v15Sha512Verifier.verify_signature(
algorithm,
issuer_spki,
message,
signature,
);
}
Err(SignatureError::new())
}
}
const _: fn() = || {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<ValidatedPath>();
_assert_send_sync::<Error>();
_assert_send_sync::<TrustAnchor>();
_assert_send_sync::<ValidationPolicy>();
};
#[cfg(all(test, feature = "p256"))]
mod tests_ecdsa_p256 {
use super::*;
use der::Decode;
#[test]
fn verify_p256_self_signed() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let verifier = EcdsaP256Verifier;
assert!(
verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.is_ok(),
"self-signed P-256 cert should verify"
);
}
}
#[cfg(all(test, feature = "p384"))]
mod tests_ecdsa_p384 {
use super::*;
use der::Decode;
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/ec-p384-sha384.der");
#[test]
fn verify_p384_self_signed() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let cert = Certificate::from_der(FIXTURE).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let verifier = EcdsaP384Verifier;
verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.expect("self-signed P-384 cert should verify");
}
#[test]
fn tampered_signature_rejected() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let cert = Certificate::from_der(FIXTURE).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let mut sig_bytes = cert.signature.raw_bytes().to_vec();
let mid = sig_bytes.len() / 2;
sig_bytes[mid] ^= 0x01;
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let verifier = EcdsaP384Verifier;
assert!(
verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
&sig_bytes,
)
.is_err(),
"tampered signature must not verify"
);
}
#[test]
fn wrong_oid_rejected() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let cert = Certificate::from_der(FIXTURE).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let wrong_alg = spki::AlgorithmIdentifierRef {
oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2"), parameters: None,
};
let verifier = EcdsaP384Verifier;
assert!(
verifier
.verify_signature(wrong_alg, spki_ref, &tbs_der, sig_bytes)
.is_err(),
"verifier must reject OIDs other than ecdsa-with-SHA384"
);
}
#[test]
fn default_verifier_dispatches_to_p384() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let cert = Certificate::from_der(FIXTURE).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
DefaultVerifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.expect("DefaultVerifier must dispatch to EcdsaP384Verifier for ecdsa-with-SHA384");
}
}
#[cfg(all(test, feature = "rsa"))]
mod tests_rsa {
use super::*;
use der::Decode;
#[test]
fn verify_rsa_pkcs1v15_sha256_self_signed() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let verifier = RsaPkcs1v15Sha256Verifier;
assert!(
verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.is_ok(),
"self-signed RSA cert should verify"
);
}
#[test]
fn verify_rsa_pkcs1v15_sha384_self_signed() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha384.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
RsaPkcs1v15Sha384Verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.expect("self-signed RSA SHA-384 cert should verify");
}
#[test]
fn rsa_pkcs1v15_sha384_tampered_signature_rejected() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha384.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let mut sig_bytes = cert.signature.raw_bytes().to_vec();
let mid = sig_bytes.len() / 2;
sig_bytes[mid] ^= 0x01;
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
assert!(
RsaPkcs1v15Sha384Verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
&sig_bytes,
)
.is_err(),
"tampered RSA SHA-384 signature must not verify"
);
}
#[test]
fn verify_rsa_pkcs1v15_sha512_self_signed() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha512.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
RsaPkcs1v15Sha512Verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.expect("self-signed RSA SHA-512 cert should verify");
}
#[test]
fn rsa_pkcs1v15_sha512_tampered_signature_rejected() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha512.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let mut sig_bytes = cert.signature.raw_bytes().to_vec();
let mid = sig_bytes.len() / 2;
sig_bytes[mid] ^= 0x01;
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
assert!(
RsaPkcs1v15Sha512Verifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
&sig_bytes,
)
.is_err(),
"tampered RSA SHA-512 signature must not verify"
);
}
#[test]
fn rsa_pkcs1v15_wrong_oid_rejected() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha384.der");
let cert = Certificate::from_der(der).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
let sha384_alg = cert.signature_algorithm.owned_to_ref();
assert!(RsaPkcs1v15Sha512Verifier
.verify_signature(sha384_alg, spki_ref.clone(), &tbs_der, sig_bytes)
.is_err());
assert!(RsaPkcs1v15Sha256Verifier
.verify_signature(sha384_alg, spki_ref, &tbs_der, sig_bytes)
.is_err());
}
#[test]
fn default_verifier_dispatches_to_rsa_sha384_and_sha512() {
use der::Encode as _;
use spki::der::referenced::OwnedToRef as _;
for fixture in [
&include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha384.der")[..],
&include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha512.der")[..],
] {
let cert = Certificate::from_der(fixture).expect("parse cert");
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
DefaultVerifier
.verify_signature(
cert.signature_algorithm.owned_to_ref(),
spki_ref,
&tbs_der,
sig_bytes,
)
.expect("DefaultVerifier must dispatch RSA SHA-384/512 by OID");
}
}
#[test]
fn spki_key_matches_ignores_null_vs_absent_params() {
let der_bytes = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
let cert = Certificate::from_der(der_bytes).expect("parse cert");
let cert_spki = &cert.tbs_certificate.subject_public_key_info;
let spki_no_params: spki::SubjectPublicKeyInfoOwned = spki::SubjectPublicKeyInfoOwned {
algorithm: spki::AlgorithmIdentifier {
oid: cert_spki.algorithm.oid,
parameters: None,
},
subject_public_key: cert_spki.subject_public_key.clone(),
};
assert_ne!(cert_spki, &spki_no_params);
assert!(super::spki_key_matches(cert_spki, &spki_no_params));
}
#[test]
fn self_issued_rsa_anchor_absent_params_not_no_trusted_path() {
const NOW: u64 = 1_780_272_000;
let der_bytes = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
let cert = Certificate::from_der(der_bytes).expect("parse cert");
let cert_spki = &cert.tbs_certificate.subject_public_key_info;
let anchor = TrustAnchor::new(
cert.tbs_certificate.subject.clone(),
spki::SubjectPublicKeyInfoOwned {
algorithm: spki::AlgorithmIdentifier {
oid: cert_spki.algorithm.oid,
parameters: None,
},
subject_public_key: cert_spki.subject_public_key.clone(),
},
);
let policy = ValidationPolicy {
current_time_unix: NOW,
..Default::default()
};
let result = validate_path(&[cert], &[anchor], &policy, &RsaPkcs1v15Sha256Verifier);
assert!(
!matches!(result, Err(Error::NoTrustedPath)),
"guard must not return NoTrustedPath for same key with different param encoding; got: {result:?}"
);
}
}
#[cfg(test)]
mod tests_normalized_iter {
use super::normalized_eq;
#[test]
fn identical_strings_equal() {
assert!(normalized_eq(b"hello", b"hello"));
}
#[test]
fn case_folding() {
assert!(normalized_eq(b"Hello", b"hello"));
assert!(normalized_eq(b"HELLO WORLD", b"hello world"));
}
#[test]
fn leading_spaces_stripped() {
assert!(normalized_eq(b" hello", b"hello"));
}
#[test]
fn trailing_spaces_stripped() {
assert!(normalized_eq(b"hello ", b"hello"));
assert!(normalized_eq(b"hello ", b"hello"));
}
#[test]
fn internal_spaces_collapsed() {
assert!(normalized_eq(b"hello world", b"hello world"));
assert!(normalized_eq(b"hello world", b"hello world"));
}
#[test]
fn combined_normalization() {
assert!(normalized_eq(b" Hello World ", b"hello world"));
}
#[test]
fn empty_and_whitespace_only() {
assert!(normalized_eq(b"", b""));
assert!(normalized_eq(b" ", b""));
assert!(normalized_eq(b" ", b" "));
}
#[test]
fn different_strings_not_equal() {
assert!(!normalized_eq(b"hello", b"world"));
assert!(!normalized_eq(b"ab", b"abc"));
}
#[test]
fn internal_then_trailing_space_no_trailing_emit() {
assert!(
normalized_eq(b"ab ", b"ab"),
"trailing spaces must not be emitted"
);
assert!(
normalized_eq(b"ab cd ", b"ab cd"),
"internal double-space collapses; trailing spaces stripped"
);
}
}
#[cfg(test)]
mod tests_bmp_string {
use super::Vec;
use super::{ava_values_match, bmp_string_to_utf8};
use der::asn1::Any;
use der::Tag;
fn ucs2_be_ascii(s: &str) -> Vec<u8> {
let mut v = Vec::with_capacity(s.len() * 2);
for b in s.bytes() {
assert!(
b.is_ascii(),
"ucs2_be_ascii test helper only supports ASCII input"
);
v.push(0x00);
v.push(b);
}
v
}
#[test]
fn ascii_round_trip() {
let src = "Foo Co";
let ucs2 = ucs2_be_ascii(src);
let utf8 = bmp_string_to_utf8(&ucs2).expect("well-formed UCS-2 ASCII");
assert_eq!(utf8, src.as_bytes());
}
#[test]
fn non_ascii_bmp_code_point() {
let ucs2 = vec![0x30, 0x42];
let utf8 = bmp_string_to_utf8(&ucs2).expect("U+3042 is a valid BMP code point");
assert_eq!(utf8, vec![0xE3, 0x81, 0x82]);
}
#[test]
fn odd_length_returns_none() {
let malformed = vec![0x00, 0x46, 0x00]; assert_eq!(bmp_string_to_utf8(&malformed), None);
}
#[test]
fn surrogate_returns_none() {
assert_eq!(bmp_string_to_utf8(&[0xD8, 0x00]), None);
assert_eq!(bmp_string_to_utf8(&[0xDC, 0x00]), None);
assert_eq!(bmp_string_to_utf8(&[0xDF, 0xFF]), None);
}
#[test]
fn empty_input_round_trip() {
let utf8 = bmp_string_to_utf8(&[]).expect("empty UCS-2 is well-formed (zero units)");
assert!(utf8.is_empty());
}
#[test]
fn bmp_matches_utf8_same_text() {
let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Foo Co")).unwrap();
let utf8 = Any::new(Tag::Utf8String, b"Foo Co".to_vec()).unwrap();
assert!(ava_values_match(&bmp, &utf8));
assert!(ava_values_match(&utf8, &bmp));
}
#[test]
fn bmp_matches_printable_same_text() {
let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Acme CA")).unwrap();
let printable = Any::new(Tag::PrintableString, b"Acme CA".to_vec()).unwrap();
assert!(ava_values_match(&bmp, &printable));
assert!(ava_values_match(&printable, &bmp));
}
#[test]
fn bmp_matches_utf8_case_insensitive_ascii() {
let bmp_upper = Any::new(Tag::BmpString, ucs2_be_ascii("FOO CO")).unwrap();
let utf8_lower = Any::new(Tag::Utf8String, b"foo co".to_vec()).unwrap();
assert!(ava_values_match(&bmp_upper, &utf8_lower));
}
#[test]
fn bmp_matches_utf8_whitespace_collapsed() {
let bmp = Any::new(Tag::BmpString, ucs2_be_ascii(" Foo Co ")).unwrap();
let utf8 = Any::new(Tag::Utf8String, b"foo co".to_vec()).unwrap();
assert!(ava_values_match(&bmp, &utf8));
}
#[test]
fn bmp_does_not_match_utf8_different_text() {
let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Foo Co")).unwrap();
let utf8 = Any::new(Tag::Utf8String, b"Bar Co".to_vec()).unwrap();
assert!(!ava_values_match(&bmp, &utf8));
}
#[test]
fn malformed_bmp_does_not_match_well_formed_utf8() {
let malformed_bmp = Any::new(Tag::BmpString, vec![0x00, 0x46, 0x00]).unwrap();
let utf8 = Any::new(Tag::Utf8String, b"F".to_vec()).unwrap();
assert!(!ava_values_match(&malformed_bmp, &utf8));
assert!(!ava_values_match(&utf8, &malformed_bmp));
}
#[test]
fn bmp_matches_utf8_non_ascii() {
let bmp = Any::new(Tag::BmpString, vec![0x30, 0x42]).unwrap();
let utf8 = Any::new(Tag::Utf8String, vec![0xE3, 0x81, 0x82]).unwrap();
assert!(ava_values_match(&bmp, &utf8));
}
#[test]
fn bmp_matches_bmp_same_text() {
let a = Any::new(Tag::BmpString, ucs2_be_ascii("Acme")).unwrap();
let b = Any::new(Tag::BmpString, ucs2_be_ascii("acme")).unwrap();
assert!(ava_values_match(&a, &b));
}
}
#[cfg(all(test, feature = "p256"))]
mod tests_validate_path {
use super::*;
use der::Decode;
const GRY_NOW: u64 = 1_780_272_000;
fn load(bytes: &[u8]) -> Certificate {
Certificate::from_der(bytes).expect("parse cert")
}
fn policy_at(t: u64) -> ValidationPolicy {
ValidationPolicy {
current_time_unix: t,
..Default::default()
}
}
#[test]
fn one_cert_chain_ok() {
let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
let anchors = [TrustAnchor::from_cert(cert.clone())];
let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
.expect("1-cert chain must validate");
assert_eq!(result.anchor_index, 0);
assert_eq!(result.depth, 0);
}
#[test]
fn two_cert_chain_ok() {
let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
let anchors = [TrustAnchor::from_cert(root)];
let result = validate_path(
&[leaf, int_cert],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier,
)
.expect("2-cert chain must validate");
assert_eq!(result.anchor_index, 0);
assert_eq!(result.depth, 1);
}
#[test]
fn validated_path_exposes_leaf_subject_issuer_serial_spki() {
let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
let leaf_bytes = include_bytes!("../tests/fixtures/gry-leaf.der");
let leaf = load(leaf_bytes);
let oracle_leaf =
Certificate::from_der(leaf_bytes).expect("oracle: leaf fixture must parse");
let anchors = [TrustAnchor::from_cert(root)];
let result = validate_path(
&[leaf, int_cert],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier,
)
.expect("2-cert chain must validate");
assert_eq!(
result.leaf_subject, oracle_leaf.tbs_certificate.subject,
"ValidatedPath.leaf_subject must equal chain[0].tbs_certificate.subject"
);
assert_eq!(
result.leaf_issuer, oracle_leaf.tbs_certificate.issuer,
"ValidatedPath.leaf_issuer must equal chain[0].tbs_certificate.issuer"
);
assert_eq!(
result.leaf_serial, oracle_leaf.tbs_certificate.serial_number,
"ValidatedPath.leaf_serial must equal chain[0].tbs_certificate.serial_number"
);
assert_eq!(
result.leaf_spki, oracle_leaf.tbs_certificate.subject_public_key_info,
"ValidatedPath.leaf_spki must equal chain[0].tbs_certificate.subject_public_key_info"
);
assert_ne!(
result.leaf_subject, result.leaf_issuer,
"non-self-signed leaf must have distinct subject and issuer DNs (regression: swap)"
);
}
#[test]
fn validated_path_outputs_for_self_signed_chain() {
let cert_bytes = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
let cert = load(cert_bytes);
let oracle = Certificate::from_der(cert_bytes).expect("oracle: cert must parse");
let anchors = [TrustAnchor::from_cert(cert.clone())];
let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
.expect("1-cert self-signed chain must validate");
assert_eq!(result.leaf_subject, oracle.tbs_certificate.subject);
assert_eq!(result.leaf_issuer, oracle.tbs_certificate.issuer);
assert_eq!(result.leaf_serial, oracle.tbs_certificate.serial_number);
assert_eq!(
result.leaf_spki,
oracle.tbs_certificate.subject_public_key_info
);
assert_eq!(
result.leaf_subject, result.leaf_issuer,
"self-signed cert must have equal subject and issuer DNs"
);
}
#[test]
fn correct_anchor_index_when_multiple_anchors() {
let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
let rsa = load(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"));
let anchors = [
TrustAnchor::from_cert(rsa),
TrustAnchor::from_cert(p256.clone()),
];
let result = validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
.expect("must find second anchor");
assert_eq!(result.anchor_index, 1);
assert_eq!(result.depth, 0);
}
#[test]
fn empty_chain_returns_error() {
let anchors = [TrustAnchor::from_cert(load(include_bytes!(
"../tests/fixtures/ec-p256-sha256.der"
)))];
assert!(
matches!(
validate_path(&[], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
Err(Error::NoTrustedPath)
),
"empty chain must fail"
);
}
#[test]
fn duplicate_cert_in_chain_returns_error() {
let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
let anchors = [TrustAnchor::from_cert(cert.clone())];
let result = validate_path(
&[cert.clone(), cert],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier,
);
assert!(
matches!(
result,
Err(Error::DuplicateCertificate {
first: 0,
second: 1
})
),
"duplicate cert must return DuplicateCertificate{{first:0, second:1}}, got {result:?}"
);
}
#[test]
fn path_too_long_returns_error() {
let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
let anchors = [TrustAnchor::from_cert(root)];
let policy = ValidationPolicy {
current_time_unix: GRY_NOW,
max_path_len: 0,
..Default::default()
};
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::PathTooLong)
),
"1 intermediate with max_path_len=0 must return PathTooLong"
);
}
#[test]
fn no_trusted_path_unrelated_anchor_returns_error() {
let gry_root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let vxf_int = load(include_bytes!("../tests/fixtures/vxf-int.der"));
let vxf_leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
let anchors = [TrustAnchor::from_cert(gry_root)];
assert!(
matches!(
validate_path(
&[vxf_leaf, vxf_int],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier
),
Err(Error::NoTrustedPath)
),
"vxf chain with gry anchor must return NoTrustedPath"
);
}
#[test]
fn oid_mismatch_outer_returns_malformed_certificate() {
let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
let oid_sha256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02];
let oid_sha384: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03];
let first = leaf_der
.windows(8)
.position(|w| w == oid_sha256)
.expect("inner SHA256 OID must be present in vxf-leaf.der");
let second = leaf_der[first + 8..]
.windows(8)
.position(|w| w == oid_sha256)
.map(|p| first + 8 + p)
.expect("outer SHA256 OID must be present in vxf-leaf.der");
leaf_der[second..second + 8].copy_from_slice(oid_sha384);
let leaf = Certificate::from_der(&leaf_der).expect("patched DER must parse");
assert_ne!(
leaf.signature_algorithm, leaf.tbs_certificate.signature,
"outer/inner OIDs must differ after patch"
);
let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(
&[leaf, int_cert],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier
),
Err(Error::MalformedCertificate { index: 0 })
),
"outer/inner OID mismatch must return MalformedCertificate {{ index: 0 }}"
);
}
#[test]
fn intermediate_not_ca_returns_not_ca() {
let root = load(include_bytes!("../tests/fixtures/nca-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/nca-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/nca-leaf.der"));
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(
&[leaf, int_cert],
&anchors,
&policy_at(GRY_NOW),
&EcdsaP256Verifier
),
Err(Error::NotCA { index: 1 })
),
"intermediate without BasicConstraints CA flag must return NotCA {{ index: 1 }}"
);
}
#[test]
fn key_usage_missing_cert_sign_returns_error() {
let root = load(include_bytes!("../tests/fixtures/kuf-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/kuf-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/kuf-leaf.der"));
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
Err(Error::KeyUsageMissing { index: 1 })
),
"intermediate with KeyUsage but no keyCertSign must return KeyUsageMissing {{ index: 1 }}"
);
}
#[test]
fn absent_key_usage_intermediate_accepted() {
let root = load(include_bytes!("../tests/fixtures/nku-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/nku-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/nku-leaf.der"));
let anchors = [TrustAnchor::from_cert(root)];
let now: u64 = 1_720_000_000; let policy = ValidationPolicy {
current_time_unix: now,
..Default::default()
};
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier).expect(
"intermediate with absent KeyUsage must be accepted when enforce_key_usage=true",
);
}
#[test]
fn critical_eku_accepted() {
let cert = load(include_bytes!(
"../tests/fixtures/eku-critical-self-signed.der"
));
let anchors = [TrustAnchor::from_cert(cert.clone())];
validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
.expect("cert with critical EKU must be accepted");
}
#[test]
fn forged_anchor_name_match_spki_mismatch_rejected() {
use der::Decode as _;
let p256 = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
.expect("parse P-256 cert");
let rsa =
Certificate::from_der(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"))
.expect("parse RSA cert");
let forged = TrustAnchor::new(
p256.tbs_certificate.subject.clone(),
rsa.tbs_certificate.subject_public_key_info,
);
let anchors = [forged];
assert!(
matches!(
validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
Err(Error::NoTrustedPath)
),
"anchor with matching name but wrong SPKI must return NoTrustedPath"
);
}
#[test]
fn large_cert_encoding_does_not_fail_with_der_error() {
let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
let anchors = [TrustAnchor::from_cert(cert.clone())];
let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier);
assert!(
!matches!(result, Err(Error::Der(_))),
"heap-backed encoding must not return Error::Der for a normal cert"
);
}
#[test]
fn cert_has_san_identity_false_for_normal_cert() {
let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
assert!(
!cert_has_san_identity(&cert),
"normal cert with non-empty Subject must not be SAN-identity"
);
}
}
#[cfg(all(test, feature = "p256"))]
mod tests_chain_walk {
use super::*;
use der::Decode;
const GRY_NOW: u64 = 1_780_272_000;
const GRY_EXPIRED: u64 = 1_830_384_000;
const GRY_NOTYET: u64 = 0;
fn load(bytes: &[u8]) -> Certificate {
Certificate::from_der(bytes).expect("parse cert")
}
fn policy_at(t: u64) -> ValidationPolicy {
ValidationPolicy {
current_time_unix: t,
..Default::default()
}
}
#[test]
fn single_cert_chain_ok() {
let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
let policy = policy_at(GRY_NOW);
let anchor = TrustAnchor::from_cert(p256.clone());
chain_walk(&[p256], &anchor, &policy, &EcdsaP256Verifier)
.expect("1-cert chain must pass chain_walk");
}
#[test]
fn two_cert_chain_ok() {
let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
let policy = policy_at(GRY_NOW);
let anchor = TrustAnchor::from_cert(root);
chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier)
.expect("2-cert chain must pass chain_walk");
}
#[test]
fn corrupted_signature_returns_signature_invalid() {
let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
*leaf_der.last_mut().unwrap() ^= 0xFF;
let leaf = Certificate::from_der(&leaf_der).expect("parse still succeeds after bit flip");
let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
let anchor = TrustAnchor::from_cert(load(include_bytes!("../tests/fixtures/vxf-root.der")));
let policy = policy_at(GRY_NOW);
assert!(
matches!(
chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
Err(Error::SignatureInvalid { index: 0 })
),
"corrupted leaf signature must return SignatureInvalid {{ index: 0 }}"
);
}
#[test]
fn wrong_issuer_name_returns_chain_broken() {
let root = load(include_bytes!("../tests/fixtures/chk-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/chk-int.der"));
let leaf_wrong = load(include_bytes!(
"../tests/fixtures/chk-leaf-wrong-issuer.der"
));
let policy = policy_at(GRY_NOW);
let anchor = TrustAnchor::from_cert(root);
assert!(
matches!(
chain_walk(
&[leaf_wrong, int_cert],
&anchor,
&policy,
&EcdsaP256Verifier
),
Err(Error::ChainBroken { index: 0 })
),
"leaf with wrong issuer must return ChainBroken {{ index: 0 }}"
);
}
#[test]
fn expired_leaf_returns_validity_period() {
let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
let policy = policy_at(GRY_EXPIRED);
let anchor = TrustAnchor::from_cert(root);
assert!(
matches!(
chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
Err(Error::ValidityPeriod { index: 0 })
),
"expired leaf must return ValidityPeriod {{ index: 0 }}"
);
}
#[test]
fn notyet_valid_intermediate_returns_validity_period() {
let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
let policy = policy_at(GRY_NOTYET);
let anchor = TrustAnchor::from_cert(root);
assert!(
matches!(
chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
Err(Error::ValidityPeriod { index: 1 })
),
"not-yet-valid intermediate must return ValidityPeriod {{ index: 1 }}"
);
}
#[test]
fn unknown_critical_extension_returns_unhandled() {
let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
let leaf_unk = load(include_bytes!(
"../tests/fixtures/gry-leaf-unknown-crit.der"
));
let policy = policy_at(GRY_NOW);
let anchor = TrustAnchor::from_cert(root);
assert!(
matches!(
chain_walk(&[leaf_unk, int_cert], &anchor, &policy, &EcdsaP256Verifier),
Err(Error::UnhandledCriticalExtension { index: 0 })
),
"unknown critical ext must return UnhandledCriticalExtension {{ index: 0 }}"
);
}
}
#[cfg(all(test, feature = "p256"))]
mod tests_policy_fields {
use super::*;
use der::Decode;
const PC_NOW: u64 = 1_780_272_000;
const ECDSA_SHA256_OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
const RSA_SHA256_OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const ID_KP_SERVER_AUTH: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
const ID_KP_EMAIL_PROTECTION: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.4");
fn load(bytes: &[u8]) -> Certificate {
Certificate::from_der(bytes).expect("valid DER fixture")
}
#[test]
fn max_validity_passes_when_cert_within_limit() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.max_validity_secs = Some(4_000 * 86_400);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("all certs within 4000-day cap should validate");
}
#[test]
fn max_validity_fails_when_cert_exceeds_limit() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.max_validity_secs = Some(400 * 86_400);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::ValidityPeriodExceedsMax { .. })
),
"certs with 3652-day validity over 400-day cap must return ValidityPeriodExceedsMax"
);
}
#[test]
fn max_validity_fails_at_leaf_index_zero() {
let cert = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
.expect("parse ec-p256-sha256.der");
let anchors = [TrustAnchor::from_cert(cert.clone())];
let mut policy = ValidationPolicy::new(1_780_272_000); policy.max_validity_secs = Some(86_400);
assert!(
matches!(
validate_path(&[cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::ValidityPeriodExceedsMax { index: 0 })
),
"1-cert chain: long-validity cert with 1-day cap must return ValidityPeriodExceedsMax {{ index: 0 }}"
);
}
#[test]
fn max_validity_none_is_unconstrained() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-400d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.max_validity_secs = None; let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("None cap must accept any validity length");
}
#[test]
fn alg_allowlist_passes_when_oid_in_list() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.allowed_signature_algs = Some(vec![ECDSA_SHA256_OID]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("ECDSA-SHA256 chain with ECDSA-SHA256 allowlist should pass");
}
#[test]
fn alg_allowlist_fails_when_oid_not_in_list() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.allowed_signature_algs = Some(vec![RSA_SHA256_OID]);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::AlgorithmNotAllowed { .. })
),
"ECDSA chain with RSA-only allowlist must return AlgorithmNotAllowed"
);
}
#[test]
fn alg_allowlist_none_is_unconstrained() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.allowed_signature_algs = None; let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("None allowlist must accept any algorithm");
}
#[test]
fn require_san_passes_when_san_present() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.require_subject_alt_name = true;
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("leaf with SAN must pass require_subject_alt_name=true");
}
#[test]
fn require_san_fails_when_san_absent() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-no-san.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.require_subject_alt_name = true;
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::MissingSan)
),
"leaf without SAN must return MissingSan when require_subject_alt_name=true"
);
}
#[test]
fn require_san_false_does_not_fail_on_missing_san() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-no-san.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.require_subject_alt_name = false; let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("require_subject_alt_name=false must not fail on missing SAN");
}
#[test]
fn require_san_only_checks_leaf_not_intermediates() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.require_subject_alt_name = true;
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("i==0 guard must ensure only the leaf is checked for SAN presence");
}
#[test]
fn required_eku_passes_when_all_oids_present() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("leaf with serverAuth EKU must pass required_leaf_eku=[serverAuth]");
}
#[test]
fn required_eku_fails_when_eku_extension_absent() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::MissingEku)
),
"leaf without EKU extension must return MissingEku when an EKU OID is required"
);
}
#[test]
fn required_eku_fails_when_required_oid_not_in_list() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-wrong-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::MissingEku)
),
"leaf with wrong EKU must return MissingEku when required OID is absent"
);
}
#[test]
fn required_eku_none_is_unconstrained() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = None; let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("None required_leaf_eku must accept leaf with no EKU");
}
#[test]
fn required_eku_empty_vec_is_unconstrained() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("Some([]) required_leaf_eku (empty) must accept any EKU configuration");
}
#[test]
fn required_eku_emailprotection_does_not_match_serverauth() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![ID_KP_EMAIL_PROTECTION]);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::MissingEku)
),
"OID comparison must be exact; emailProtection must not match serverAuth"
);
}
}
#[cfg(test)]
mod tests_dn_attr_rule {
use super::*;
use core::str::FromStr;
const OID_CN: der::asn1::ObjectIdentifier = der::asn1::ObjectIdentifier::new_unwrap("2.5.4.3");
const OID_SURNAME: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.4");
const OID_ORGANIZATION_NAME: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.10");
const OID_GIVEN_NAME: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.42");
const OID_PSEUDONYM: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.65");
fn dn(s: &str) -> x509_cert::name::Name {
x509_cert::name::Name::from_str(s).expect("valid RFC 4514 DN string")
}
#[test]
fn dn_contains_oid_finds_present_attribute() {
let subject = dn("CN=test,O=Test Org");
assert!(dn_contains_oid(&subject, &OID_ORGANIZATION_NAME));
assert!(dn_contains_oid(&subject, &OID_CN));
}
#[test]
fn dn_contains_oid_returns_false_for_absent_attribute() {
let subject = dn("CN=test");
assert!(!dn_contains_oid(&subject, &OID_ORGANIZATION_NAME));
assert!(!dn_contains_oid(&subject, &OID_SURNAME));
}
#[test]
fn dn_contains_oid_empty_subject_matches_nothing() {
let subject = x509_cert::name::RdnSequence(Vec::new());
assert!(!dn_contains_oid(&subject, &OID_CN));
}
#[test]
fn rule_field_matches_when_attribute_present() {
let subject = dn("CN=test,O=Test Org");
let rule = DnAttrRule::Field(OID_ORGANIZATION_NAME);
assert!(evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_field_rejects_when_attribute_absent() {
let subject = dn("CN=test");
let rule = DnAttrRule::Field(OID_ORGANIZATION_NAME);
assert!(!evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_allof_matches_when_every_branch_satisfied() {
let subject = dn("CN=test,givenName=Alice,surname=Smith");
let rule = DnAttrRule::AllOf(vec![
DnAttrRule::Field(OID_GIVEN_NAME),
DnAttrRule::Field(OID_SURNAME),
]);
assert!(evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_allof_rejects_when_any_branch_missing() {
let subject = dn("CN=test,givenName=Alice");
let rule = DnAttrRule::AllOf(vec![
DnAttrRule::Field(OID_GIVEN_NAME),
DnAttrRule::Field(OID_SURNAME),
]);
assert!(!evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_anyof_matches_when_at_least_one_branch_satisfied() {
let subject = dn("CN=test,pseudonym=BlueFox");
let rule = DnAttrRule::AnyOf(vec![
DnAttrRule::Field(OID_PSEUDONYM),
DnAttrRule::AllOf(vec![
DnAttrRule::Field(OID_GIVEN_NAME),
DnAttrRule::Field(OID_SURNAME),
]),
]);
assert!(evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_anyof_rejects_when_no_branch_satisfied() {
let subject = dn("CN=test");
let rule = DnAttrRule::AnyOf(vec![
DnAttrRule::Field(OID_PSEUDONYM),
DnAttrRule::AllOf(vec![
DnAttrRule::Field(OID_GIVEN_NAME),
DnAttrRule::Field(OID_SURNAME),
]),
]);
assert!(!evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_anyof_matches_when_inner_allof_branch_satisfied() {
let subject = dn("CN=test,givenName=Alice,surname=Smith");
let rule = DnAttrRule::AnyOf(vec![
DnAttrRule::Field(OID_PSEUDONYM),
DnAttrRule::AllOf(vec![
DnAttrRule::Field(OID_GIVEN_NAME),
DnAttrRule::Field(OID_SURNAME),
]),
]);
assert!(evaluate_dn_attr_rule(&subject, &rule));
}
#[test]
fn rule_allof_empty_accepts_everything() {
let empty = x509_cert::name::RdnSequence(Vec::new());
let cn_only = dn("CN=test");
let rule = DnAttrRule::AllOf(Vec::new());
assert!(evaluate_dn_attr_rule(&empty, &rule));
assert!(evaluate_dn_attr_rule(&cn_only, &rule));
}
#[test]
fn rule_anyof_empty_rejects_everything() {
let empty = x509_cert::name::RdnSequence(Vec::new());
let cn_only = dn("CN=test");
let rule = DnAttrRule::AnyOf(Vec::new());
assert!(!evaluate_dn_attr_rule(&empty, &rule));
assert!(!evaluate_dn_attr_rule(&cn_only, &rule));
}
}
#[cfg(all(test, feature = "p256"))]
mod tests_required_leaf_policy_dn {
use super::*;
use der::Decode;
const PC_NOW: u64 = 1_780_272_000;
const TEST_POLICY_OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.2.1.48.1");
const OID_ORGANIZATION_NAME: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.10");
fn load(bytes: &[u8]) -> Certificate {
Certificate::from_der(bytes).expect("valid DER fixture")
}
fn make_chain() -> (Certificate, Certificate, Certificate) {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
(root, int_cert, leaf)
}
#[test]
fn required_policy_oid_fails_when_extension_absent() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_policy_oids = Some(vec![TEST_POLICY_OID]);
let anchors = [TrustAnchor::from_cert(root)];
let err = validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect_err("leaf without CertificatePolicies must fail when an OID is required");
match err {
Error::MissingLeafPolicyOid { required } => {
assert_eq!(required, TEST_POLICY_OID, "reported OID must echo input");
}
other => panic!("expected MissingLeafPolicyOid, got {other:?}"),
}
}
#[test]
fn required_policy_oid_none_is_unconstrained() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_policy_oids = None;
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("None required_leaf_policy_oids must not constrain validation");
}
#[test]
fn required_policy_oid_empty_vec_is_unconstrained() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_policy_oids = Some(vec![]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("Some([]) required_leaf_policy_oids must accept any CertificatePolicies");
}
#[test]
fn required_dn_attr_field_fails_when_attribute_absent() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_subject_dn_attrs = Some(DnAttrRule::Field(OID_ORGANIZATION_NAME));
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::SubjectDnAttrRuleUnmet)
),
"leaf without organizationName must return SubjectDnAttrRuleUnmet"
);
}
#[test]
fn required_dn_attr_field_passes_when_attribute_present() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
let oid_cn: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.4.3");
policy.required_leaf_subject_dn_attrs = Some(DnAttrRule::Field(oid_cn));
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("leaf with CN must pass Field(CN) requirement");
}
#[test]
fn required_dn_attr_allof_empty_accepts_any_leaf() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_subject_dn_attrs = Some(DnAttrRule::AllOf(Vec::new()));
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("AllOf(vec![]) is vacuously true; validation must succeed");
}
#[test]
fn required_dn_attr_anyof_empty_rejects_any_leaf() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_subject_dn_attrs = Some(DnAttrRule::AnyOf(Vec::new()));
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
Err(Error::SubjectDnAttrRuleUnmet)
),
"AnyOf(vec![]) is vacuously false; validation must fail"
);
}
#[test]
fn required_dn_attr_none_is_unconstrained() {
let (root, int_cert, leaf) = make_chain();
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_subject_dn_attrs = None;
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("None required_leaf_subject_dn_attrs must not constrain validation");
}
}
#[cfg(all(test, feature = "p256", feature = "rsa"))]
mod tests_policy_fields_rsa {
use super::*;
use der::Decode;
const PC_NOW: u64 = 1_780_272_000;
const RSA_SHA256_OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const ECDSA_SHA256_OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
const ID_KP_SERVER_AUTH: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
fn load(bytes: &[u8]) -> Certificate {
Certificate::from_der(bytes).expect("valid DER fixture")
}
#[test]
fn rsa_key_bits_correct_for_2048_key() {
let cert = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
));
let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
assert_eq!(
result,
Some(2048),
"RSA-2048 key must return Some(2048) from rsa_public_key_bits"
);
}
#[test]
fn rsa_key_bits_correct_for_1024_key() {
let cert = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
));
let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
assert_eq!(
result,
Some(1024),
"RSA-1024 key must return Some(1024) from rsa_public_key_bits"
);
}
#[test]
fn rsa_key_bits_none_for_ec_key() {
let cert = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
assert_eq!(
result, None,
"EC key must return None from rsa_public_key_bits (not RSA)"
);
}
#[test]
fn min_rsa_key_bits_passes_when_key_meets_limit() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.min_rsa_key_bits = Some(2048);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier,
)
.expect("RSA-2048 leaf with min=2048 should pass");
}
#[test]
fn min_rsa_key_bits_fails_when_key_too_small() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.min_rsa_key_bits = Some(2048);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier
),
Err(Error::KeyTooSmall { index: 0 })
),
"RSA-1024 leaf with min=2048 must return KeyTooSmall {{ index: 0 }}"
);
}
#[test]
fn min_rsa_key_bits_none_is_unconstrained() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.min_rsa_key_bits = None; let anchors = [TrustAnchor::from_cert(root)];
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier,
)
.expect("None min_rsa_key_bits must accept RSA-1024 leaf");
}
#[test]
fn min_rsa_key_bits_ec_key_passes_unconditionally() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-p256.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-p256.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.min_rsa_key_bits = Some(16384);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
.expect("EC key must not be affected by min_rsa_key_bits");
}
#[test]
fn alg_allowlist_fails_on_rsa_chain_when_only_ecdsa_allowed() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.allowed_signature_algs = Some(vec![ECDSA_SHA256_OID]);
let anchors = [TrustAnchor::from_cert(root)];
assert!(
matches!(
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier
),
Err(Error::AlgorithmNotAllowed { .. })
),
"RSA chain with ECDSA-only allowlist must return AlgorithmNotAllowed"
);
}
#[test]
fn alg_allowlist_passes_for_rsa_chain() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.allowed_signature_algs = Some(vec![RSA_SHA256_OID]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier,
)
.expect("RSA chain with RSA-SHA256 in allowlist should pass");
}
#[test]
fn required_eku_passes_for_rsa_chain() {
let root = load(include_bytes!(
"../tests/fixtures/policy-checks/root-rsa2048.der"
));
let int_cert = load(include_bytes!(
"../tests/fixtures/policy-checks/int-rsa2048.der"
));
let leaf = load(include_bytes!(
"../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
));
let mut policy = ValidationPolicy::new(PC_NOW);
policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
let anchors = [TrustAnchor::from_cert(root)];
validate_path(
&[leaf, int_cert],
&anchors,
&policy,
&RsaPkcs1v15Sha256Verifier,
)
.expect("RSA leaf with serverAuth EKU must pass required_leaf_eku=[serverAuth]");
}
}