#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms)]
use der::Tagged;
use signature::Error as SignatureError;
use spki::{AlgorithmIdentifierRef, SubjectPublicKeyInfoRef};
use x509_cert::Certificate;
#[derive(Debug)]
#[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,
},
UnhandledCriticalExtension {
index: usize,
},
Der(der::Error),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::SignatureInvalid { index } => {
write!(f, "signature invalid at chain index {index}")
}
Error::ValidityPeriod { index } => {
write!(f, "validity period check failed at chain index {index}")
}
Error::MalformedCertificate { index } => {
write!(f, "malformed certificate at chain index {index}")
}
Error::ChainBroken { index } => {
write!(f, "issuer/subject linkage broken at chain index {index}")
}
Error::NoTrustedPath => write!(f, "no path to a trusted anchor"),
Error::PathTooLong => write!(f, "path length exceeds maximum"),
Error::NotCA { index } => write!(f, "certificate at index {index} is not a CA"),
Error::KeyUsageMissing { index } => {
write!(f, "keyCertSign missing at chain index {index}")
}
Error::UnhandledCriticalExtension { index } => {
write!(f, "unhandled critical extension at chain index {index}")
}
Error::Der(e) => write!(f, "DER error: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Der(e) => Some(e),
Error::SignatureInvalid { .. }
| Error::MalformedCertificate { .. }
| Error::ValidityPeriod { .. }
| Error::ChainBroken { .. }
| Error::NoTrustedPath
| Error::PathTooLong
| Error::NotCA { .. }
| Error::KeyUsageMissing { .. }
| Error::UnhandledCriticalExtension { .. } => None,
}
}
}
impl From<der::Error> for Error {
fn from(e: der::Error) -> Self {
Error::Der(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)]
pub struct TrustAnchor {
pub subject: x509_cert::name::Name,
pub subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
}
impl TrustAnchor {
pub fn new(
subject: x509_cert::name::Name,
subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
) -> Self {
Self {
subject,
subject_public_key_info,
}
}
pub fn from_cert(cert: Certificate) -> Self {
Self {
subject: cert.tbs_certificate.subject,
subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
}
}
}
#[derive(Clone, Debug)]
pub struct ValidationPolicy {
pub max_path_len: u8,
pub current_time_unix: u64,
pub enforce_key_usage: bool,
}
impl ValidationPolicy {
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,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ValidatedPath {
pub anchor_index: usize,
pub depth: usize,
}
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_intermediates = chain.len().saturating_sub(1);
if num_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
&& anchor.subject_public_key_info != last_cert.tbs_certificate.subject_public_key_info
{
continue;
}
match chain_walk(chain, anchor, policy, verifier) {
Ok(()) => {
return Ok(ValidatedPath {
anchor_index,
depth: chain.len().saturating_sub(1),
});
}
Err(e) => last_err = e,
}
}
Err(last_err)
}
fn check_inputs(chain: &[Certificate], anchors: &[TrustAnchor]) -> Result<()> {
if chain.is_empty() || anchors.is_empty() {
return Err(Error::NoTrustedPath);
}
Ok(())
}
fn check_oid_consistency(chain: &[Certificate]) -> Result<()> {
for (index, cert) in chain.iter().enumerate() {
if cert.signature_algorithm != cert.tbs_certificate.signature {
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 HANDLED_CRITICAL_OIDS: &[der::asn1::ObjectIdentifier] =
&[OID_KEY_USAGE, OID_BASIC_CONSTRAINTS, OID_SUBJECT_ALT_NAME];
fn check_critical_extensions(cert: &Certificate, index: usize) -> Result<()> {
if let Some(exts) = cert.tbs_certificate.extensions.as_ref() {
for ext in exts.iter() {
if ext.critical && !HANDLED_CRITICAL_OIDS.contains(&ext.extn_id) {
return Err(Error::UnhandledCriticalExtension { index });
}
}
}
Ok(())
}
fn has_key_cert_sign(cert: &Certificate) -> Option<bool> {
use der::Decode;
use x509_cert::ext::pkix::KeyUsage;
let exts = cert.tbs_certificate.extensions.as_ref()?;
for ext in exts.iter() {
if ext.extn_id == OID_KEY_USAGE {
let ku = KeyUsage::from_der(ext.extn_value.as_bytes()).ok()?;
return Some(ku.key_cert_sign());
}
}
None
}
fn cert_basic_constraints(cert: &Certificate) -> Option<x509_cert::ext::pkix::BasicConstraints> {
use der::Decode;
use x509_cert::ext::pkix::BasicConstraints;
let exts = cert.tbs_certificate.extensions.as_ref()?;
for ext in exts.iter() {
if ext.extn_id == OID_BASIC_CONSTRAINTS {
return BasicConstraints::from_der(ext.extn_value.as_bytes()).ok();
}
}
None
}
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 })
}
}
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.iter()) {
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.iter() {
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;
}
}
}
true
}
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, b_bytes),
(None, None) => a.value() == b.value(),
_ => false,
}
}
fn any_to_str_bytes(a: &der::Any) -> Option<&[u8]> {
use der::Tag;
match a.tag() {
Tag::Utf8String | Tag::PrintableString | Tag::Ia5String | Tag::VisibleString => {
Some(a.value())
}
_ => None,
}
}
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(|i| start + i + 1)
.unwrap_or(start);
Self {
bytes: &bytes[start..end],
pos: 0,
pending_space: false,
}
}
}
impl<'a> Iterator for NormalizedIter<'a> {
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())
}
}
}
#[cfg(feature = "p256")]
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> {
const OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
if algorithm.oid != OID {
return Err(SignatureError::new());
}
use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
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")]
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> {
const OID: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
if algorithm.oid != OID {
return Err(SignatureError::new());
}
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::signature::Verifier as _;
use sha2::Sha256;
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())
}
}
fn chain_walk<V: SignatureVerifier>(
chain: &[Certificate],
anchor: &TrustAnchor,
policy: &ValidationPolicy,
verifier: &V,
) -> Result<()> {
use der::Encode;
use spki::der::referenced::OwnedToRef as _;
let mut working_spki = &anchor.subject_public_key_info;
let mut working_issuer_name = &anchor.subject;
for i in (0..chain.len()).rev() {
let cert = &chain[i];
let mut tbs_buf = [0u8; 8192];
let tbs_bytes = cert
.tbs_certificate
.encode_to_slice(&mut tbs_buf)
.map_err(Error::Der)?;
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: i })?;
if !names_match(working_issuer_name, &cert.tbs_certificate.issuer) {
return Err(Error::ChainBroken { index: i });
}
check_validity(cert, policy.current_time_unix, i)?;
check_critical_extensions(cert, i)?;
if i > 0 {
let bc = cert_basic_constraints(cert);
if bc.as_ref().map(|b| b.ca) != Some(true) {
return Err(Error::NotCA { index: i });
}
if policy.enforce_key_usage {
match has_key_cert_sign(cert) {
Some(true) => {}
_ => return Err(Error::KeyUsageMissing { index: i }),
}
}
if let Some(path_len) = bc.and_then(|b| b.path_len_constraint) {
if (i - 1) > path_len as usize {
return Err(Error::PathTooLong);
}
}
}
working_spki = &cert.tbs_certificate.subject_public_key_info;
working_issuer_name = &cert.tbs_certificate.subject;
}
Ok(())
}
#[cfg(any(feature = "p256", feature = "rsa"))]
pub struct DefaultVerifier;
#[cfg(any(feature = "p256", 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 = "rsa")]
if oid == OID_SHA256_WITH_RSA {
return RsaPkcs1v15Sha256Verifier.verify_signature(
algorithm,
issuer_spki,
message,
signature,
);
}
Err(SignatureError::new())
}
}
#[cfg(any(feature = "p256", feature = "rsa"))]
const OID_ECDSA_P256_SHA256: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
#[cfg(any(feature = "p256", feature = "rsa"))]
const OID_SHA256_WITH_RSA: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
#[cfg(all(test, feature = "p256"))]
mod tests_ecdsa_p256 {
use super::*;
use der::Decode;
#[test]
fn verify_p256_self_signed() {
let der = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
let cert = Certificate::from_der(der).expect("parse cert");
use der::Encode as _;
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
use spki::der::referenced::OwnedToRef as _;
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 = "rsa"))]
mod tests_rsa {
use super::*;
use der::Decode;
#[test]
fn verify_rsa_pkcs1v15_sha256_self_signed() {
let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
let cert = Certificate::from_der(der).expect("parse cert");
use der::Encode as _;
let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
let sig_bytes = cert.signature.raw_bytes();
use spki::der::referenced::OwnedToRef as _;
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"
);
}
}
#[cfg(test)]
mod tests_normalized_iter {
use super::{normalized_eq, NormalizedIter};
#[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() {
let collected: Vec<u8> = NormalizedIter::new(b"ab ").collect();
assert_eq!(collected, b"ab");
let collected: Vec<u8> = NormalizedIter::new(b"ab cd ").collect();
assert_eq!(collected, b"ab cd");
}
}
#[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 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 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 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.clone(),
);
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"
);
}
}
#[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 }}"
);
}
}