pkix_path/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![warn(missing_docs, rust_2018_idioms)]
5
6//! RFC 5280 X.509 certificate path validation — pure Rust, `no_std`.
7//!
8//! Implements certificate path building and validation per
9//! [RFC 5280 §6](https://www.rfc-editor.org/rfc/rfc5280#section-6).
10//!
11//! # Architecture
12//!
13//! Cryptographic signature verification is pluggable via [`SignatureVerifier`].
14//! The default feature set (`rustcrypto`) wires in RustCrypto backends for
15//! RSA-PKCS1v15-SHA-256 (`rsa` feature) and ECDSA-P-256-SHA-256 (`p256` feature).
16//! P-384 and Ed25519 are planned for v0.2.
17//! For FIPS-validated crypto, implement [`SignatureVerifier`] against
18//! `wolfcrypt-rustcrypto` and disable the `rustcrypto` feature.
19//!
20//! Revocation checking is handled by `pkix-revocation`. This crate never
21//! touches the network — use `pkix_chain::verify_chain` for the combined API.
22//!
23//! # Limitations
24//!
25//! v0.1 does **not** implement:
26//! - NameConstraints (RFC 5280 §4.2.1.10)
27//! - PolicyConstraints / certificate policy validation (§4.2.1.9, §6.1.5)
28//! - Revocation (use `pkix-revocation`)
29//! - Cross-certificate path building (RFC 4158)
30//!
31//! These are tracked for v0.2+.
32
33use der::Tagged;
34use signature::Error as SignatureError;
35use spki::{AlgorithmIdentifierRef, SubjectPublicKeyInfoRef};
36use x509_cert::Certificate;
37
38/// Errors returned by path validation.
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum Error {
42 /// Certificate signature verification failed at the given chain index.
43 SignatureInvalid {
44 /// Zero-based index into the `chain` slice of the failing certificate.
45 index: usize,
46 },
47 /// A structural encoding error was found in a certificate.
48 ///
49 /// Currently returned when the outer `signatureAlgorithm` field differs from
50 /// the inner `TBSCertificate.signature` field (RFC 5280 §4.1.1.2).
51 MalformedCertificate {
52 /// Zero-based index into the `chain` slice of the malformed certificate.
53 index: usize,
54 },
55 /// Certificate validity period check failed (expired or not yet valid).
56 ValidityPeriod {
57 /// Zero-based index into the `chain` slice of the failing certificate.
58 index: usize,
59 },
60 /// Issuer/subject name linkage is broken at the given chain index.
61 ChainBroken {
62 /// Zero-based index into the `chain` slice where the break was found.
63 index: usize,
64 },
65 /// No path from the subject certificate to any trust anchor was found.
66 NoTrustedPath,
67 /// Path length exceeds [`ValidationPolicy::max_path_len`].
68 PathTooLong,
69 /// An intermediate certificate is missing BasicConstraints cA=TRUE.
70 NotCA {
71 /// Zero-based index into the `chain` slice of the failing certificate.
72 index: usize,
73 },
74 /// An intermediate certificate is missing KeyUsage keyCertSign.
75 KeyUsageMissing {
76 /// Zero-based index into the `chain` slice of the failing certificate.
77 index: usize,
78 },
79 /// A critical extension is present that this implementation does not handle.
80 UnhandledCriticalExtension {
81 /// Zero-based index into the `chain` slice of the failing certificate.
82 index: usize,
83 },
84 /// ASN.1 / DER decoding error.
85 ///
86 /// Returned when DER encoding of a TBS structure fails inside `chain_walk`
87 /// (e.g. the TBS is too large for the internal stack buffer). The inner
88 /// `der::Error` is exposed for diagnostic purposes; callers that want a
89 /// stable match target should check for `Error::Der(_)` without inspecting
90 /// the inner value, as the specific `der::Error` variants are not part of
91 /// the stable API contract.
92 Der(der::Error),
93}
94
95impl core::fmt::Display for Error {
96 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97 match self {
98 Error::SignatureInvalid { index } => {
99 write!(f, "signature invalid at chain index {index}")
100 }
101 Error::ValidityPeriod { index } => {
102 write!(f, "validity period check failed at chain index {index}")
103 }
104 Error::MalformedCertificate { index } => {
105 write!(f, "malformed certificate at chain index {index}")
106 }
107 Error::ChainBroken { index } => {
108 write!(f, "issuer/subject linkage broken at chain index {index}")
109 }
110 Error::NoTrustedPath => write!(f, "no path to a trusted anchor"),
111 Error::PathTooLong => write!(f, "path length exceeds maximum"),
112 Error::NotCA { index } => write!(f, "certificate at index {index} is not a CA"),
113 Error::KeyUsageMissing { index } => {
114 write!(f, "keyCertSign missing at chain index {index}")
115 }
116 Error::UnhandledCriticalExtension { index } => {
117 write!(f, "unhandled critical extension at chain index {index}")
118 }
119 Error::Der(e) => write!(f, "DER error: {e}"),
120 }
121 }
122}
123
124#[cfg(feature = "std")]
125impl std::error::Error for Error {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 match self {
128 Error::Der(e) => Some(e),
129 Error::SignatureInvalid { .. }
130 | Error::MalformedCertificate { .. }
131 | Error::ValidityPeriod { .. }
132 | Error::ChainBroken { .. }
133 | Error::NoTrustedPath
134 | Error::PathTooLong
135 | Error::NotCA { .. }
136 | Error::KeyUsageMissing { .. }
137 | Error::UnhandledCriticalExtension { .. } => None,
138 }
139 }
140}
141
142impl From<der::Error> for Error {
143 fn from(e: der::Error) -> Self {
144 Error::Der(e)
145 }
146}
147
148/// Result alias for this crate.
149pub type Result<T> = core::result::Result<T, Error>;
150
151/// Pluggable signature verification backend.
152///
153/// Implement this trait to provide algorithm-specific signature verification.
154/// The trait is OID-dispatched: the `algorithm` argument carries the OID and
155/// any parameters from the certificate's `signatureAlgorithm` field.
156///
157/// # Implementing a custom backend
158///
159/// ```rust,ignore
160/// struct MyVerifier;
161///
162/// impl pkix_path::SignatureVerifier for MyVerifier {
163/// fn verify_signature(
164/// &self,
165/// algorithm: spki::AlgorithmIdentifierRef<'_>,
166/// issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
167/// message: &[u8],
168/// signature: &[u8],
169/// ) -> core::result::Result<(), signature::Error> {
170/// match algorithm.oid {
171/// MY_RSA_OID => { /* ... */ }
172/// MY_ECDSA_OID => { /* ... */ }
173/// _ => Err(signature::Error::new()),
174/// }
175/// }
176/// }
177/// ```
178pub trait SignatureVerifier {
179 /// Verify `signature` over `message`.
180 ///
181 /// - `algorithm` — from the subject cert's `signatureAlgorithm` field
182 /// - `issuer_spki` — SPKI extracted from the issuer or trust anchor cert
183 /// - `message` — DER-encoded TBSCertificate (the bytes that were signed)
184 /// - `signature` — raw signature bytes (BitString content, not the wrapper)
185 ///
186 /// Returns `Ok(())` on success or `Err(signature::Error)` on failure.
187 /// The caller ([`validate_path`]) maps the error to [`Error::SignatureInvalid`]
188 /// with the correct chain index — the verifier does not need to know it.
189 fn verify_signature(
190 &self,
191 algorithm: AlgorithmIdentifierRef<'_>,
192 issuer_spki: SubjectPublicKeyInfoRef<'_>,
193 message: &[u8],
194 signature: &[u8],
195 ) -> core::result::Result<(), SignatureError>;
196}
197
198/// A trust anchor used to terminate path validation.
199///
200/// A trust anchor is typically either a self-signed root CA certificate
201/// or a raw (name, SPKI) pair extracted from a platform trust store.
202/// The trust anchor itself is **not** signature-verified — it is trusted
203/// by definition.
204#[derive(Clone, Debug)]
205pub struct TrustAnchor {
206 /// The subject distinguished name of the trust anchor.
207 pub subject: x509_cert::name::Name,
208 /// The subject public key info of the trust anchor.
209 ///
210 /// Must be a valid SPKI for the chosen signature algorithm. An empty or
211 /// malformed SPKI will cause signature verification to fail with
212 /// `Error::NoTrustedPath` (no anchor matched), not a panic.
213 pub subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
214}
215
216impl TrustAnchor {
217 /// Create a trust anchor from raw subject name and SPKI.
218 pub fn new(
219 subject: x509_cert::name::Name,
220 subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
221 ) -> Self {
222 Self {
223 subject,
224 subject_public_key_info,
225 }
226 }
227
228 /// Extract subject name and SPKI from a certificate to create a trust anchor.
229 ///
230 /// This is the typical constructor when your trust store contains full
231 /// self-signed root CA certificates.
232 pub fn from_cert(cert: Certificate) -> Self {
233 Self {
234 subject: cert.tbs_certificate.subject,
235 subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
236 }
237 }
238}
239
240/// Policy parameters controlling path validation.
241///
242/// # Limitations
243///
244/// v0.1 does not enforce NameConstraints, CertificatePolicies, or
245/// PolicyMappings. Fields for these will be added in v0.2.
246#[derive(Clone, Debug)]
247pub struct ValidationPolicy {
248 /// Maximum chain depth, not counting the trust anchor. Default: 10.
249 ///
250 /// A chain of `[leaf]` is depth 0. `[leaf, intermediate, root]` is depth 1
251 /// (one intermediate). Validation fails if depth exceeds this value.
252 pub max_path_len: u8,
253
254 /// Current time as seconds since the Unix epoch (1970-01-01T00:00:00Z).
255 ///
256 /// Used to check `notBefore` ≤ `now` ≤ `notAfter` on every certificate.
257 /// **Must be set by the caller** — there is no platform clock in `no_std`.
258 ///
259 /// **Warning — the default is 0 (1970-01-01):** Any certificate issued
260 /// after 1970 has `notBefore > 0` and will fail the validity check with
261 /// [`Error::ValidityPeriod`]. If you see unexpected `ValidityPeriod`
262 /// errors, check that `current_time_unix` is set to the current time.
263 ///
264 /// **Warning**: passing `u64::MAX` causes all `notAfter` checks to pass.
265 /// This effectively disables expiry checking — only use it in contexts
266 /// where you explicitly want permissive (clock-free) validation.
267 pub current_time_unix: u64,
268
269 /// Enforce the KeyUsage extension when present. Default: `true`.
270 ///
271 /// When `true`, an intermediate certificate missing `keyCertSign` in its
272 /// KeyUsage will be rejected even if BasicConstraints cA=TRUE.
273 pub enforce_key_usage: bool,
274}
275
276impl ValidationPolicy {
277 /// Construct a policy with the given time and sensible defaults.
278 ///
279 /// Equivalent to `ValidationPolicy { current_time_unix: now_unix, ..Default::default() }`.
280 /// This is the preferred constructor: it forces the caller to supply a timestamp,
281 /// preventing the silent validity failures caused by `Default`'s `current_time_unix = 0`.
282 pub fn new(now_unix: u64) -> Self {
283 Self {
284 current_time_unix: now_unix,
285 ..Default::default()
286 }
287 }
288}
289
290impl Default for ValidationPolicy {
291 fn default() -> Self {
292 Self {
293 max_path_len: 10,
294 current_time_unix: 0, // caller must set to avoid silent clock skew
295 enforce_key_usage: true,
296 }
297 }
298}
299
300/// The result of a successful certificate path validation.
301///
302/// Fields are `pub` for direct read access. `#[non_exhaustive]` prevents external
303/// code from constructing `ValidatedPath` directly and from pattern-matching
304/// exhaustively, preserving the ability to add fields in future minor versions
305/// without a breaking change.
306#[derive(Clone, Debug, PartialEq, Eq)]
307#[non_exhaustive]
308pub struct ValidatedPath {
309 /// Index into the `anchors` slice of the trust anchor that terminated the path.
310 pub anchor_index: usize,
311 /// Depth of the validated chain (number of intermediates, excluding trust anchor).
312 pub depth: usize,
313}
314
315/// Validate a certificate chain from subject to a trust anchor.
316///
317/// `chain` must be ordered leaf-first:
318/// - `chain[0]` is the subject (end-entity) certificate
319/// - `chain[1..]` are intermediates in issuer order
320/// - The last element of `chain` must be issued by one of `anchors`
321///
322/// Validation follows RFC 5280 §6.1. Each certificate's signature is verified
323/// using `verifier`, with the signing key taken from the next certificate in
324/// the chain (or the matching trust anchor for the last cert).
325///
326/// # Errors
327///
328/// Returns `Err` on the first RFC 5280 §6.1 check failure. The error variant
329/// includes the chain index of the failing certificate where applicable.
330///
331/// # Limitations
332///
333/// See crate-level documentation for v0.1 scope limits.
334///
335/// Duplicate certificates in `chain` (same cert appearing at two indices) are
336/// not detected. They will fail signature verification or name linkage with a
337/// `SignatureInvalid` or `ChainBroken` error rather than a dedicated diagnostic.
338pub fn validate_path<V>(
339 chain: &[Certificate],
340 anchors: &[TrustAnchor],
341 policy: &ValidationPolicy,
342 verifier: &V,
343) -> Result<ValidatedPath>
344where
345 V: SignatureVerifier,
346{
347 // (1) Input guards: reject empty chain or anchors, check OID consistency.
348 check_inputs(chain, anchors)?;
349 check_oid_consistency(chain)?;
350
351 // (2) Path-length check (anchor-independent).
352 let num_intermediates = chain.len().saturating_sub(1);
353 if num_intermediates > policy.max_path_len as usize {
354 return Err(Error::PathTooLong);
355 }
356
357 // (3) Try each name-matching anchor. Iterating all candidates handles key
358 // rollover: multiple anchors may share a DN but have different keys
359 // (e.g., during a root CA rotation). The first anchor that passes the
360 // full chain walk is used; the last error is returned if none succeed.
361 //
362 // Complexity: O(A × N) where A = number of anchors, N = chain length.
363 // For the common case of O(1) matching anchors this is effectively O(N).
364 let last_cert = chain.last().ok_or(Error::NoTrustedPath)?;
365 let is_self_issued = names_match(
366 &last_cert.tbs_certificate.issuer,
367 &last_cert.tbs_certificate.subject,
368 );
369 let mut last_err = Error::NoTrustedPath;
370 for (anchor_index, anchor) in anchors.iter().enumerate() {
371 if !names_match(&anchor.subject, &last_cert.tbs_certificate.issuer) {
372 continue;
373 }
374 // For self-issued certs the cert and anchor are the same entity; their
375 // SPKIs must match (RFC 5280 §3.2 name-collision guard).
376 if is_self_issued
377 && anchor.subject_public_key_info != last_cert.tbs_certificate.subject_public_key_info
378 {
379 continue;
380 }
381 match chain_walk(chain, anchor, policy, verifier) {
382 Ok(()) => {
383 return Ok(ValidatedPath {
384 anchor_index,
385 depth: chain.len().saturating_sub(1),
386 });
387 }
388 Err(e) => last_err = e,
389 }
390 }
391 Err(last_err)
392}
393
394// ---------------------------------------------------------------------------
395// validate_path helpers — input guards and OID consistency (PKIX-6vu)
396// ---------------------------------------------------------------------------
397
398fn check_inputs(chain: &[Certificate], anchors: &[TrustAnchor]) -> Result<()> {
399 if chain.is_empty() || anchors.is_empty() {
400 return Err(Error::NoTrustedPath);
401 }
402 Ok(())
403}
404
405/// RFC 5280 §4.1.1.2: outer signatureAlgorithm must equal inner TBSCertificate.signature.
406fn check_oid_consistency(chain: &[Certificate]) -> Result<()> {
407 for (index, cert) in chain.iter().enumerate() {
408 if cert.signature_algorithm != cert.tbs_certificate.signature {
409 return Err(Error::MalformedCertificate { index });
410 }
411 }
412 Ok(())
413}
414
415// ---------------------------------------------------------------------------
416// Critical extension guard (PKIX-ad6)
417// ---------------------------------------------------------------------------
418
419const OID_KEY_USAGE: der::asn1::ObjectIdentifier =
420 der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
421
422const OID_BASIC_CONSTRAINTS: der::asn1::ObjectIdentifier =
423 der::asn1::ObjectIdentifier::new_unwrap("2.5.29.19");
424
425const OID_SUBJECT_ALT_NAME: der::asn1::ObjectIdentifier =
426 der::asn1::ObjectIdentifier::new_unwrap("2.5.29.17");
427
428/// OIDs of extensions that this implementation handles; all others, if critical, cause rejection.
429///
430/// `OID_SUBJECT_ALT_NAME` is listed here so that certs with critical SAN extensions
431/// (e.g. TLS server certs) do not fail with `UnhandledCriticalExtension`. However,
432/// the SAN *value* is not inspected by path validation — name matching still uses the
433/// Subject DN. **v0.1 limitation**: a cert with an empty Subject and critical SAN
434/// will pass this check but fail name linkage since `names_match` compares against
435/// the empty Subject. This is tracked for v0.2 (RFC 5280 §4.2.1.6).
436const HANDLED_CRITICAL_OIDS: &[der::asn1::ObjectIdentifier] =
437 &[OID_KEY_USAGE, OID_BASIC_CONSTRAINTS, OID_SUBJECT_ALT_NAME];
438
439/// RFC 5280 §6.1.3(a)(3): reject any critical extension not in the handled set.
440fn check_critical_extensions(cert: &Certificate, index: usize) -> Result<()> {
441 if let Some(exts) = cert.tbs_certificate.extensions.as_ref() {
442 for ext in exts.iter() {
443 if ext.critical && !HANDLED_CRITICAL_OIDS.contains(&ext.extn_id) {
444 return Err(Error::UnhandledCriticalExtension { index });
445 }
446 }
447 }
448 Ok(())
449}
450
451// ---------------------------------------------------------------------------
452// KeyUsage extraction (PKIX-8ae)
453// ---------------------------------------------------------------------------
454
455/// Returns whether the `keyCertSign` bit is set in the KeyUsage extension.
456///
457/// - `None` — KeyUsage extension absent (no constraint)
458/// - `Some(true)` — keyCertSign is set
459/// - `Some(false)` — KeyUsage present, keyCertSign NOT set
460fn has_key_cert_sign(cert: &Certificate) -> Option<bool> {
461 use der::Decode;
462 use x509_cert::ext::pkix::KeyUsage;
463
464 let exts = cert.tbs_certificate.extensions.as_ref()?;
465 for ext in exts.iter() {
466 if ext.extn_id == OID_KEY_USAGE {
467 let ku = KeyUsage::from_der(ext.extn_value.as_bytes()).ok()?;
468 return Some(ku.key_cert_sign());
469 }
470 }
471 None
472}
473
474// ---------------------------------------------------------------------------
475// BasicConstraints extraction (PKIX-0q5)
476// ---------------------------------------------------------------------------
477
478/// Decode the `BasicConstraints` extension from a certificate, if present.
479///
480/// Returns `None` if the extension is absent; decoding errors are silently
481/// treated as absent (the caller will then fail the cA=TRUE check).
482fn cert_basic_constraints(cert: &Certificate) -> Option<x509_cert::ext::pkix::BasicConstraints> {
483 use der::Decode;
484 use x509_cert::ext::pkix::BasicConstraints;
485
486 let exts = cert.tbs_certificate.extensions.as_ref()?;
487 for ext in exts.iter() {
488 if ext.extn_id == OID_BASIC_CONSTRAINTS {
489 return BasicConstraints::from_der(ext.extn_value.as_bytes()).ok();
490 }
491 }
492 None
493}
494
495// ---------------------------------------------------------------------------
496// Validity period checker (PKIX-047)
497// ---------------------------------------------------------------------------
498
499/// Convert an `x509_cert::time::Time` to seconds since the Unix epoch.
500fn time_to_unix_secs(t: &x509_cert::time::Time) -> u64 {
501 t.to_unix_duration().as_secs()
502}
503
504/// RFC 5280 §6.1.3(a)(2): check notBefore ≤ now ≤ notAfter.
505fn check_validity(cert: &Certificate, now_unix: u64, index: usize) -> Result<()> {
506 let not_before = time_to_unix_secs(&cert.tbs_certificate.validity.not_before);
507 let not_after = time_to_unix_secs(&cert.tbs_certificate.validity.not_after);
508 if now_unix >= not_before && now_unix <= not_after {
509 Ok(())
510 } else {
511 Err(Error::ValidityPeriod { index })
512 }
513}
514
515// ---------------------------------------------------------------------------
516// Name comparison — RFC 4518 string prep (PKIX-drv)
517// ---------------------------------------------------------------------------
518
519/// Compare two distinguished names per RFC 4518 string prep rules.
520///
521/// For v0.1: implements case-fold and whitespace normalization for ASCII
522/// characters. Full Unicode NFKD normalization is deferred to v0.2.
523///
524/// Returns `true` if the names are equivalent.
525///
526/// # Ordering
527///
528/// RFC 5280 §4.1.2.4 defines `Name` as `SEQUENCE OF RDN`, so RDNs are
529/// compared positionally (index 0 with index 0, etc.). Within each RDN —
530/// which is a `SET OF AttributeTypeAndValue` — comparison is order-independent:
531/// each AVA in one RDN is matched against any AVA in the other.
532pub fn names_match(a: &x509_cert::name::Name, b: &x509_cert::name::Name) -> bool {
533 let a_rdns = a.0.as_slice();
534 let b_rdns = b.0.as_slice();
535
536 if a_rdns.len() != b_rdns.len() {
537 return false;
538 }
539
540 for (a_rdn, b_rdn) in a_rdns.iter().zip(b_rdns.iter()) {
541 let a_avas = a_rdn.0.as_slice();
542 let b_avas = b_rdn.0.as_slice();
543 if a_avas.len() != b_avas.len() {
544 return false;
545 }
546 // For each AVA in a_rdn, find matching AVA in b_rdn (same OID, equal normalized value).
547 for a_ava in a_avas.iter() {
548 let found = b_avas.iter().any(|b_ava| {
549 b_ava.oid == a_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
550 });
551 if !found {
552 return false;
553 }
554 }
555 }
556 true
557}
558
559/// Compare two AttributeTypeAndValue values after RFC 4518 normalization.
560fn ava_values_match(a: &der::Any, b: &der::Any) -> bool {
561 let a_str = any_to_str_bytes(a);
562 let b_str = any_to_str_bytes(b);
563
564 match (a_str, b_str) {
565 (Some(a_bytes), Some(b_bytes)) => normalized_eq(a_bytes, b_bytes),
566 // Fall back to raw DER byte comparison if we can't decode as a string type.
567 (None, None) => a.value() == b.value(),
568 _ => false,
569 }
570}
571
572/// Extract the string content bytes from a DirectoryString Any value.
573/// Returns None if the tag is not a string type we handle.
574///
575/// **v0.1 limitation**: `TeletexString` (T61String) and `BMPString` (used in
576/// some legacy CA certificates) are not handled here and fall back to raw DER
577/// byte comparison in `ava_values_match`. Name matching against these string
578/// types may fail even when the names are semantically equivalent. Tracked
579/// for v0.2 (RFC 5280 §7.1 / RFC 4518 §2.6 legacy encoding support).
580fn any_to_str_bytes(a: &der::Any) -> Option<&[u8]> {
581 use der::Tag;
582 match a.tag() {
583 Tag::Utf8String | Tag::PrintableString | Tag::Ia5String | Tag::VisibleString => {
584 Some(a.value())
585 }
586 _ => None,
587 }
588}
589
590/// Compare two ASCII byte slices after RFC 4518 whitespace normalization and case-folding.
591///
592/// Rules applied:
593/// 1. ASCII letters: case-fold to lowercase
594/// 2. Leading/trailing spaces: ignored
595/// 3. Internal multiple spaces: collapsed to single space
596fn normalized_eq(a: &[u8], b: &[u8]) -> bool {
597 NormalizedIter::new(a).eq(NormalizedIter::new(b))
598}
599
600/// Iterator that yields bytes after ASCII case-fold and whitespace normalization.
601struct NormalizedIter<'a> {
602 bytes: &'a [u8],
603 pos: usize,
604 pending_space: bool,
605}
606
607impl<'a> NormalizedIter<'a> {
608 fn new(bytes: &'a [u8]) -> Self {
609 // Skip leading spaces.
610 let start = bytes.iter().position(|&b| b != b' ').unwrap_or(bytes.len());
611 // Find end (skip trailing spaces).
612 let end = bytes[start..]
613 .iter()
614 .rposition(|&b| b != b' ')
615 .map(|i| start + i + 1)
616 .unwrap_or(start);
617 Self {
618 bytes: &bytes[start..end],
619 pos: 0,
620 pending_space: false,
621 }
622 }
623}
624
625impl<'a> Iterator for NormalizedIter<'a> {
626 type Item = u8;
627 fn next(&mut self) -> Option<u8> {
628 // A space was already emitted on the previous call; skip any additional
629 // consecutive spaces now without emitting another space character.
630 if self.pending_space {
631 self.pending_space = false;
632 while self.pos < self.bytes.len() && self.bytes[self.pos] == b' ' {
633 self.pos += 1;
634 }
635 // Fall through: process the next non-space byte (or return None if at end).
636 }
637 if self.pos >= self.bytes.len() {
638 return None;
639 }
640 let b = self.bytes[self.pos];
641 self.pos += 1;
642 if b == b' ' {
643 // Emit one space; next call will skip any further consecutive spaces.
644 self.pending_space = true;
645 Some(b' ')
646 } else {
647 Some(b.to_ascii_lowercase())
648 }
649 }
650}
651
652// ---------------------------------------------------------------------------
653// ECDSA P-256 SHA-256 backend (PKIX-evy)
654// ---------------------------------------------------------------------------
655
656/// ECDSA P-256 with SHA-256 signature verifier.
657///
658/// Handles OID `ecdsa-with-SHA256` (1.2.840.10045.4.3.2).
659/// Feature-gated behind `p256`.
660#[cfg(feature = "p256")]
661pub struct EcdsaP256Verifier;
662
663#[cfg(feature = "p256")]
664impl SignatureVerifier for EcdsaP256Verifier {
665 fn verify_signature(
666 &self,
667 algorithm: spki::AlgorithmIdentifierRef<'_>,
668 issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
669 message: &[u8],
670 signature: &[u8],
671 ) -> core::result::Result<(), SignatureError> {
672 // Reject any OID other than ecdsa-with-SHA256.
673 const OID: der::asn1::ObjectIdentifier =
674 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
675 if algorithm.oid != OID {
676 return Err(SignatureError::new());
677 }
678
679 use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
680
681 let vk = VerifyingKey::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
682
683 let sig = DerSignature::try_from(signature).map_err(|_| SignatureError::new())?;
684
685 vk.verify(message, &sig).map_err(|_| SignatureError::new())
686 }
687}
688
689// ---------------------------------------------------------------------------
690// RSA PKCS#1 v1.5 SHA-256 backend (PKIX-gmv)
691// ---------------------------------------------------------------------------
692
693/// RSA with PKCS#1 v1.5 padding and SHA-256 signature verifier.
694///
695/// Handles OID `sha256WithRSAEncryption` (1.2.840.113549.1.1.11).
696/// Feature-gated behind `rsa`.
697#[cfg(feature = "rsa")]
698pub struct RsaPkcs1v15Sha256Verifier;
699
700#[cfg(feature = "rsa")]
701impl SignatureVerifier for RsaPkcs1v15Sha256Verifier {
702 fn verify_signature(
703 &self,
704 algorithm: spki::AlgorithmIdentifierRef<'_>,
705 issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
706 message: &[u8],
707 signature: &[u8],
708 ) -> core::result::Result<(), SignatureError> {
709 // Reject any OID other than sha256WithRSAEncryption.
710 const OID: der::asn1::ObjectIdentifier =
711 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
712 if algorithm.oid != OID {
713 return Err(SignatureError::new());
714 }
715
716 use rsa::pkcs1v15::{Signature, VerifyingKey};
717 use rsa::signature::Verifier as _;
718 use sha2::Sha256;
719
720 let vk =
721 VerifyingKey::<Sha256>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
722
723 let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
724
725 vk.verify(message, &sig).map_err(|_| SignatureError::new())
726 }
727}
728
729// ---------------------------------------------------------------------------
730// Chain walk loop — signature verification and name linkage (PKIX-vxf)
731// ---------------------------------------------------------------------------
732
733/// Walk the chain from issuer to leaf, applying all RFC 5280 §6.1 per-cert checks.
734///
735/// Path-length and anchor-matching are handled by the caller (`validate_path`).
736/// This function walks `chain` in reverse (issuer-to-leaf) against `anchor`:
737///
738/// a. Verify signature with the current issuer's SPKI.
739/// b. Verify issuer/subject name linkage.
740/// c. Check validity period against `policy.current_time_unix`.
741/// d. Reject any unhandled critical extensions.
742/// e. For all certs except the leaf (i > 0): require `BasicConstraints` cA=TRUE.
743/// f. For all certs except the leaf (i > 0): if `policy.enforce_key_usage`, require `keyCertSign`.
744/// g. For all certs except the leaf (i > 0): enforce `pathLenConstraint` if present.
745///
746/// RFC 5280 §4.2.1.9 note on pathLenConstraint: for the cert at position `i`
747/// (leaf at 0, root-adjacent at chain.len()-1), there are exactly `i-1`
748/// intermediate certs below it. The constraint requires `i-1 ≤ pathLenConstraint`.
749fn chain_walk<V: SignatureVerifier>(
750 chain: &[Certificate],
751 anchor: &TrustAnchor,
752 policy: &ValidationPolicy,
753 verifier: &V,
754) -> Result<()> {
755 use der::Encode;
756 use spki::der::referenced::OwnedToRef as _;
757
758 let mut working_spki = &anchor.subject_public_key_info;
759 let mut working_issuer_name = &anchor.subject;
760
761 for i in (0..chain.len()).rev() {
762 let cert = &chain[i];
763
764 // (a) Verify signature with the current issuer's SPKI.
765 // 8 KiB covers every well-formed certificate encountered in practice
766 // (typical TLS certs are 1–3 KiB). Certificates exceeding this limit
767 // return Error::Der; tracked for v0.2 with heap-backed encoding.
768 let mut tbs_buf = [0u8; 8192];
769 let tbs_bytes = cert
770 .tbs_certificate
771 .encode_to_slice(&mut tbs_buf)
772 .map_err(Error::Der)?;
773 verifier
774 .verify_signature(
775 cert.signature_algorithm.owned_to_ref(),
776 working_spki.owned_to_ref(),
777 tbs_bytes,
778 cert.signature.raw_bytes(),
779 )
780 .map_err(|_| Error::SignatureInvalid { index: i })?;
781
782 // (b) Issuer/subject name linkage.
783 if !names_match(working_issuer_name, &cert.tbs_certificate.issuer) {
784 return Err(Error::ChainBroken { index: i });
785 }
786
787 // (c) Validity period.
788 check_validity(cert, policy.current_time_unix, i)?;
789
790 // (d) Critical extension guard.
791 check_critical_extensions(cert, i)?;
792
793 // (e–g) CA-only checks: apply to every cert except the leaf (chain[0]).
794 // This includes any intermediate CAs and the root CA cert if it
795 // is included in the chain rather than supplied only as an anchor.
796 if i > 0 {
797 // (e) BasicConstraints cA=TRUE required; (g) pathLenConstraint.
798 // Decode BasicConstraints once for both checks.
799 let bc = cert_basic_constraints(cert);
800 if bc.as_ref().map(|b| b.ca) != Some(true) {
801 return Err(Error::NotCA { index: i });
802 }
803
804 // (f) KeyUsage keyCertSign required (when policy demands it).
805 if policy.enforce_key_usage {
806 match has_key_cert_sign(cert) {
807 Some(true) => {}
808 _ => return Err(Error::KeyUsageMissing { index: i }),
809 }
810 }
811
812 // (g) pathLenConstraint: the cert at position i has i-1 intermediates
813 // below it in the chain. Enforce the constraint.
814 if let Some(path_len) = bc.and_then(|b| b.path_len_constraint) {
815 if (i - 1) > path_len as usize {
816 return Err(Error::PathTooLong);
817 }
818 }
819 }
820
821 // Update state for next iteration.
822 working_spki = &cert.tbs_certificate.subject_public_key_info;
823 working_issuer_name = &cert.tbs_certificate.subject;
824 }
825
826 Ok(())
827}
828
829// ---------------------------------------------------------------------------
830// DefaultVerifier — OID-dispatching RustCrypto backend (PKIX-8wg)
831// ---------------------------------------------------------------------------
832
833/// A [`SignatureVerifier`] that dispatches to available RustCrypto backends by OID.
834///
835/// This is the recommended out-of-the-box verifier for applications that use
836/// the default RustCrypto feature set. It supports:
837///
838/// - `ecdsa-with-SHA256` (1.2.840.10045.4.3.2) — via the `p256` feature
839/// - `sha256WithRSAEncryption` (1.2.840.113549.1.1.11) — via the `rsa` feature
840///
841/// Any OID not in the above set returns `Err(signature::Error::new())`.
842///
843/// To support additional algorithms, implement [`SignatureVerifier`] directly
844/// and dispatch your own OID table.
845#[cfg(any(feature = "p256", feature = "rsa"))]
846pub struct DefaultVerifier;
847
848#[cfg(any(feature = "p256", feature = "rsa"))]
849impl SignatureVerifier for DefaultVerifier {
850 fn verify_signature(
851 &self,
852 algorithm: AlgorithmIdentifierRef<'_>,
853 issuer_spki: SubjectPublicKeyInfoRef<'_>,
854 message: &[u8],
855 signature: &[u8],
856 ) -> core::result::Result<(), SignatureError> {
857 let oid = algorithm.oid;
858 #[cfg(feature = "p256")]
859 if oid == OID_ECDSA_P256_SHA256 {
860 return EcdsaP256Verifier.verify_signature(algorithm, issuer_spki, message, signature);
861 }
862 #[cfg(feature = "rsa")]
863 if oid == OID_SHA256_WITH_RSA {
864 return RsaPkcs1v15Sha256Verifier.verify_signature(
865 algorithm,
866 issuer_spki,
867 message,
868 signature,
869 );
870 }
871 Err(SignatureError::new())
872 }
873}
874
875/// OID for `ecdsa-with-SHA256` — used by `DefaultVerifier` dispatch.
876#[cfg(any(feature = "p256", feature = "rsa"))]
877const OID_ECDSA_P256_SHA256: der::asn1::ObjectIdentifier =
878 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
879
880/// OID for `sha256WithRSAEncryption` — used by `DefaultVerifier` dispatch.
881#[cfg(any(feature = "p256", feature = "rsa"))]
882const OID_SHA256_WITH_RSA: der::asn1::ObjectIdentifier =
883 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
884
885// ---------------------------------------------------------------------------
886// Tests
887// ---------------------------------------------------------------------------
888
889#[cfg(all(test, feature = "p256"))]
890mod tests_ecdsa_p256 {
891 use super::*;
892 use der::Decode;
893
894 /// Test vector: a real P-256/SHA-256 self-signed cert generated by OpenSSL.
895 /// Oracle: `openssl verify -CAfile ec.pem ec.pem` returns OK.
896 #[test]
897 fn verify_p256_self_signed() {
898 let der = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
899 let cert = Certificate::from_der(der).expect("parse cert");
900
901 use der::Encode as _;
902 let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
903 let sig_bytes = cert.signature.raw_bytes();
904
905 // Self-signed cert: signer SPKI is the cert's own SPKI.
906 use spki::der::referenced::OwnedToRef as _;
907 let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
908
909 let verifier = EcdsaP256Verifier;
910 assert!(
911 verifier
912 .verify_signature(
913 cert.signature_algorithm.owned_to_ref(),
914 spki_ref,
915 &tbs_der,
916 sig_bytes,
917 )
918 .is_ok(),
919 "self-signed P-256 cert should verify"
920 );
921 }
922}
923
924#[cfg(all(test, feature = "rsa"))]
925mod tests_rsa {
926 use super::*;
927 use der::Decode;
928
929 /// Test vector: a real RSA-2048/SHA-256 self-signed cert generated by OpenSSL.
930 /// Oracle: `openssl verify -CAfile rsa.pem rsa.pem` returns OK.
931 #[test]
932 fn verify_rsa_pkcs1v15_sha256_self_signed() {
933 let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
934 let cert = Certificate::from_der(der).expect("parse cert");
935
936 use der::Encode as _;
937 let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
938 let sig_bytes = cert.signature.raw_bytes();
939
940 // Self-signed cert: signer SPKI is the cert's own SPKI.
941 use spki::der::referenced::OwnedToRef as _;
942 let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
943
944 let verifier = RsaPkcs1v15Sha256Verifier;
945 assert!(
946 verifier
947 .verify_signature(
948 cert.signature_algorithm.owned_to_ref(),
949 spki_ref,
950 &tbs_der,
951 sig_bytes,
952 )
953 .is_ok(),
954 "self-signed RSA cert should verify"
955 );
956 }
957}
958
959// ---------------------------------------------------------------------------
960// NormalizedIter / names_match unit tests
961// ---------------------------------------------------------------------------
962#[cfg(test)]
963mod tests_normalized_iter {
964 use super::{normalized_eq, NormalizedIter};
965
966 /// Identical ASCII strings must compare equal.
967 #[test]
968 fn identical_strings_equal() {
969 assert!(normalized_eq(b"hello", b"hello"));
970 }
971
972 /// Case is folded to lowercase.
973 #[test]
974 fn case_folding() {
975 assert!(normalized_eq(b"Hello", b"hello"));
976 assert!(normalized_eq(b"HELLO WORLD", b"hello world"));
977 }
978
979 /// Leading spaces are stripped.
980 #[test]
981 fn leading_spaces_stripped() {
982 assert!(normalized_eq(b" hello", b"hello"));
983 }
984
985 /// Trailing spaces are stripped.
986 ///
987 /// Regression test: NormalizedIter must not emit a trailing space for
988 /// input that ends with a space sequence.
989 #[test]
990 fn trailing_spaces_stripped() {
991 assert!(normalized_eq(b"hello ", b"hello"));
992 assert!(normalized_eq(b"hello ", b"hello"));
993 }
994
995 /// Multiple consecutive internal spaces are collapsed to a single space.
996 ///
997 /// Regression test for the double-space bug: `pending_space` must not
998 /// cause two spaces to be emitted for a single space in the input.
999 #[test]
1000 fn internal_spaces_collapsed() {
1001 assert!(normalized_eq(b"hello world", b"hello world"));
1002 assert!(normalized_eq(b"hello world", b"hello world"));
1003 }
1004
1005 /// Combined: leading + trailing + internal spaces, case folding.
1006 #[test]
1007 fn combined_normalization() {
1008 assert!(normalized_eq(b" Hello World ", b"hello world"));
1009 }
1010
1011 /// Empty string and all-spaces string must both yield zero bytes.
1012 #[test]
1013 fn empty_and_whitespace_only() {
1014 assert!(normalized_eq(b"", b""));
1015 assert!(normalized_eq(b" ", b""));
1016 assert!(normalized_eq(b" ", b" "));
1017 }
1018
1019 /// Different strings must NOT compare equal after normalization.
1020 #[test]
1021 fn different_strings_not_equal() {
1022 assert!(!normalized_eq(b"hello", b"world"));
1023 assert!(!normalized_eq(b"ab", b"abc"));
1024 }
1025
1026 /// NormalizedIter: input ending with an internal space sequence followed by
1027 /// trailing spaces must emit the space and then stop (no double space, no
1028 /// trailing space).
1029 #[test]
1030 fn internal_then_trailing_space_no_trailing_emit() {
1031 // "ab " → normalized → "ab" (one word, no trailing space)
1032 let collected: Vec<u8> = NormalizedIter::new(b"ab ").collect();
1033 assert_eq!(collected, b"ab");
1034
1035 // "ab cd " → normalized → "ab cd" (one internal space, no trailing space)
1036 let collected: Vec<u8> = NormalizedIter::new(b"ab cd ").collect();
1037 assert_eq!(collected, b"ab cd");
1038 }
1039}
1040
1041// PKIX-h6z: validate_path public API tests.
1042#[cfg(all(test, feature = "p256"))]
1043mod tests_validate_path {
1044 use super::*;
1045 use der::Decode;
1046
1047 // Fixtures and time constants reused from tests_chain_walk.
1048 const GRY_NOW: u64 = 1_780_272_000; // 2026-06-01
1049
1050 fn load(bytes: &[u8]) -> Certificate {
1051 Certificate::from_der(bytes).expect("parse cert")
1052 }
1053
1054 fn policy_at(t: u64) -> ValidationPolicy {
1055 ValidationPolicy {
1056 current_time_unix: t,
1057 ..Default::default()
1058 }
1059 }
1060
1061 /// Happy-path 1-cert chain: self-signed cert is both chain and anchor.
1062 ///
1063 /// Expected: Ok(ValidatedPath { anchor_index: 0, depth: 0 })
1064 #[test]
1065 fn one_cert_chain_ok() {
1066 let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1067 let anchors = [TrustAnchor::from_cert(cert.clone())];
1068 let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
1069 .expect("1-cert chain must validate");
1070 assert_eq!(result.anchor_index, 0);
1071 assert_eq!(result.depth, 0);
1072 }
1073
1074 /// Happy-path 2-cert chain: leaf + intermediate, with root anchor.
1075 ///
1076 /// Oracle: openssl verify -CAfile gry-root.pem -untrusted gry-int.pem gry-leaf.pem → OK
1077 /// Expected: Ok(ValidatedPath { anchor_index: 0, depth: 1 })
1078 #[test]
1079 fn two_cert_chain_ok() {
1080 let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1081 let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1082 let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1083 let anchors = [TrustAnchor::from_cert(root)];
1084 let result = validate_path(
1085 &[leaf, int_cert],
1086 &anchors,
1087 &policy_at(GRY_NOW),
1088 &EcdsaP256Verifier,
1089 )
1090 .expect("2-cert chain must validate");
1091 assert_eq!(result.anchor_index, 0);
1092 assert_eq!(result.depth, 1);
1093 }
1094
1095 /// Multiple anchors: correct anchor is second in the slice.
1096 ///
1097 /// Expected: Ok(ValidatedPath { anchor_index: 1, depth: 0 })
1098 #[test]
1099 fn correct_anchor_index_when_multiple_anchors() {
1100 let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1101 let rsa = load(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"));
1102 // First anchor is the RSA cert (wrong name and SPKI for the P-256 chain).
1103 // Second anchor matches.
1104 let anchors = [
1105 TrustAnchor::from_cert(rsa),
1106 TrustAnchor::from_cert(p256.clone()),
1107 ];
1108 let result = validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
1109 .expect("must find second anchor");
1110 assert_eq!(result.anchor_index, 1);
1111 assert_eq!(result.depth, 0);
1112 }
1113
1114 /// Empty chain returns NoTrustedPath.
1115 #[test]
1116 fn empty_chain_returns_error() {
1117 let anchors = [TrustAnchor::from_cert(load(include_bytes!(
1118 "../tests/fixtures/ec-p256-sha256.der"
1119 )))];
1120 assert!(
1121 matches!(
1122 validate_path(&[], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1123 Err(Error::NoTrustedPath)
1124 ),
1125 "empty chain must fail"
1126 );
1127 }
1128
1129 /// path_too_long: vxf chain [leaf, int] with max_path_len = 0.
1130 ///
1131 /// chain.len()=2 → 1 intermediate. 1 > max_path_len(0) → PathTooLong.
1132 #[test]
1133 fn path_too_long_returns_error() {
1134 let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1135 let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1136 let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1137 let anchors = [TrustAnchor::from_cert(root)];
1138 let policy = ValidationPolicy {
1139 current_time_unix: GRY_NOW,
1140 max_path_len: 0,
1141 ..Default::default()
1142 };
1143 assert!(
1144 matches!(
1145 validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
1146 Err(Error::PathTooLong)
1147 ),
1148 "1 intermediate with max_path_len=0 must return PathTooLong"
1149 );
1150 }
1151
1152 /// no_trusted_path: vxf chain presented to an unrelated anchor (gry-root).
1153 ///
1154 /// vxf's last cert issuer name does not match gry-root's subject name.
1155 #[test]
1156 fn no_trusted_path_unrelated_anchor_returns_error() {
1157 let gry_root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1158 let vxf_int = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1159 let vxf_leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1160 let anchors = [TrustAnchor::from_cert(gry_root)];
1161 assert!(
1162 matches!(
1163 validate_path(
1164 &[vxf_leaf, vxf_int],
1165 &anchors,
1166 &policy_at(GRY_NOW),
1167 &EcdsaP256Verifier
1168 ),
1169 Err(Error::NoTrustedPath)
1170 ),
1171 "vxf chain with gry anchor must return NoTrustedPath"
1172 );
1173 }
1174
1175 /// oid_mismatch: outer signatureAlgorithm OID differs from inner TBS signature OID.
1176 ///
1177 /// Patch the SECOND occurrence of the ECDSA-with-SHA256 OID bytes in vxf-leaf.der
1178 /// to ECDSA-with-SHA384. The inner TBS.signature remains SHA256.
1179 /// check_oid_consistency detects this → MalformedCertificate { index: 0 }.
1180 ///
1181 /// Oracle: RFC 5280 §4.1.1.2 requires outer and inner AlgorithmIdentifiers to be identical.
1182 #[test]
1183 fn oid_mismatch_outer_returns_malformed_certificate() {
1184 let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
1185 // ECDSA-with-SHA256 OID content bytes: 1.2.840.10045.4.3.2
1186 let oid_sha256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02];
1187 // ECDSA-with-SHA384 OID content bytes: 1.2.840.10045.4.3.3 (same length, last byte differs)
1188 let oid_sha384: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03];
1189 // In Certificate DER the inner TBS.signature OID appears FIRST (inside TBSCertificate)
1190 // and the outer signatureAlgorithm OID appears SECOND (after TBSCertificate). Patching
1191 // only the second occurrence changes the outer OID while leaving the inner intact.
1192 let first = leaf_der
1193 .windows(8)
1194 .position(|w| w == oid_sha256)
1195 .expect("inner SHA256 OID must be present in vxf-leaf.der");
1196 let second = leaf_der[first + 8..]
1197 .windows(8)
1198 .position(|w| w == oid_sha256)
1199 .map(|p| first + 8 + p)
1200 .expect("outer SHA256 OID must be present in vxf-leaf.der");
1201 leaf_der[second..second + 8].copy_from_slice(oid_sha384);
1202 let leaf = Certificate::from_der(&leaf_der).expect("patched DER must parse");
1203 assert_ne!(
1204 leaf.signature_algorithm, leaf.tbs_certificate.signature,
1205 "outer/inner OIDs must differ after patch"
1206 );
1207 let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1208 let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1209 let anchors = [TrustAnchor::from_cert(root)];
1210 assert!(
1211 matches!(
1212 validate_path(
1213 &[leaf, int_cert],
1214 &anchors,
1215 &policy_at(GRY_NOW),
1216 &EcdsaP256Verifier
1217 ),
1218 Err(Error::MalformedCertificate { index: 0 })
1219 ),
1220 "outer/inner OID mismatch must return MalformedCertificate {{ index: 0 }}"
1221 );
1222 }
1223
1224 /// intermediate_not_ca: nca-int has no BasicConstraints extension.
1225 ///
1226 /// Oracle: pyca/cryptography — nca-int built without any extensions.
1227 /// cert_is_ca(nca-int) returns None → NotCA { index: 1 }.
1228 #[test]
1229 fn intermediate_not_ca_returns_not_ca() {
1230 let root = load(include_bytes!("../tests/fixtures/nca-root.der"));
1231 let int_cert = load(include_bytes!("../tests/fixtures/nca-int.der"));
1232 let leaf = load(include_bytes!("../tests/fixtures/nca-leaf.der"));
1233 let anchors = [TrustAnchor::from_cert(root)];
1234 assert!(
1235 matches!(
1236 validate_path(
1237 &[leaf, int_cert],
1238 &anchors,
1239 &policy_at(GRY_NOW),
1240 &EcdsaP256Verifier
1241 ),
1242 Err(Error::NotCA { index: 1 })
1243 ),
1244 "intermediate without BasicConstraints CA flag must return NotCA {{ index: 1 }}"
1245 );
1246 }
1247
1248 /// key_usage_missing_cert_sign: kuf-int has KeyUsage with digitalSignature only.
1249 ///
1250 /// Oracle: pyca/cryptography — kuf-int KeyUsage.keyCertSign = False.
1251 /// Default policy has enforce_key_usage = true; chain_walk checks at i=1.
1252 #[test]
1253 fn key_usage_missing_cert_sign_returns_error() {
1254 let root = load(include_bytes!("../tests/fixtures/kuf-root.der"));
1255 let int_cert = load(include_bytes!("../tests/fixtures/kuf-int.der"));
1256 let leaf = load(include_bytes!("../tests/fixtures/kuf-leaf.der"));
1257 let anchors = [TrustAnchor::from_cert(root)];
1258 assert!(
1259 matches!(
1260 validate_path(&[leaf, int_cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1261 Err(Error::KeyUsageMissing { index: 1 })
1262 ),
1263 "intermediate with KeyUsage but no keyCertSign must return KeyUsageMissing {{ index: 1 }}"
1264 );
1265 }
1266
1267 /// Security test: anchor with matching name but wrong SPKI must be rejected.
1268 ///
1269 /// Guards against a name-collision attack: an attacker who creates a root cert
1270 /// with the same DN as a trusted anchor but a different key must not be accepted.
1271 /// The self-issued SPKI guard in validate_path catches this.
1272 #[test]
1273 fn forged_anchor_name_match_spki_mismatch_rejected() {
1274 use der::Decode as _;
1275 let p256 = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
1276 .expect("parse P-256 cert");
1277 let rsa =
1278 Certificate::from_der(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"))
1279 .expect("parse RSA cert");
1280 // Forged anchor: P-256 cert's subject name + RSA cert's SPKI.
1281 let forged = TrustAnchor::new(
1282 p256.tbs_certificate.subject.clone(),
1283 rsa.tbs_certificate.subject_public_key_info.clone(),
1284 );
1285 let anchors = [forged];
1286 assert!(
1287 matches!(
1288 validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1289 Err(Error::NoTrustedPath)
1290 ),
1291 "anchor with matching name but wrong SPKI must return NoTrustedPath"
1292 );
1293 }
1294}
1295
1296// PKIX-vxf + PKIX-gry: chain_walk tests require the p256 feature.
1297#[cfg(all(test, feature = "p256"))]
1298mod tests_chain_walk {
1299 use super::*;
1300 use der::Decode;
1301
1302 // Fixtures (PKIX-vxf):
1303 // vxf-root.der — self-signed root CA, CN=PKIX-vxf-root (P-256)
1304 // vxf-int.der — intermediate CA, CN=PKIX-vxf-int, signed by vxf-root
1305 // vxf-leaf.der — leaf cert, CN=PKIX-vxf-leaf, signed by vxf-int
1306 // chk-root.der / chk-int.der / chk-leaf-wrong-issuer.der — ChainBroken test chain
1307 //
1308 // Fixtures (PKIX-gry):
1309 // gry-root.der — root CA, CN=PKIX-gry-root (P-256)
1310 // gry-int.der — intermediate CA, CN=PKIX-gry-int, valid 2026-2036
1311 // gry-leaf.der — leaf, CN=PKIX-gry-leaf, valid 2026-2027 (short-lived)
1312 // gry-leaf-unknown-crit.der — leaf with unknown critical extension
1313 //
1314 // Unix timestamp constants for gry validity tests:
1315 // GRY_NOW = 1780272000 (2026-06-01, all gry certs valid)
1316 // GRY_EXPIRED = 1830384000 (2028-01-02, gry-leaf expired; gry-int still valid)
1317 // GRY_NOTYET = 0 (1970-01-01, all gry certs not-yet-valid)
1318 //
1319 // Oracle:
1320 // vxf chain: openssl verify -CAfile vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
1321 // gry chain: pyca/cryptography; chain verifies at GRY_NOW
1322 // chk-leaf-wrong-issuer: signature valid under chk-int key (pyca); issuer = PKIX-WRONG-ISSUER by design
1323
1324 const GRY_NOW: u64 = 1_780_272_000;
1325 const GRY_EXPIRED: u64 = 1_830_384_000;
1326 const GRY_NOTYET: u64 = 0;
1327
1328 fn load(bytes: &[u8]) -> Certificate {
1329 Certificate::from_der(bytes).expect("parse cert")
1330 }
1331
1332 fn policy_at(t: u64) -> ValidationPolicy {
1333 ValidationPolicy {
1334 current_time_unix: t,
1335 ..Default::default()
1336 }
1337 }
1338
1339 /// 1-cert chain: self-signed P-256 cert as both chain and anchor.
1340 #[test]
1341 fn single_cert_chain_ok() {
1342 let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1343 let policy = policy_at(GRY_NOW);
1344 let anchor = TrustAnchor::from_cert(p256.clone());
1345 chain_walk(&[p256], &anchor, &policy, &EcdsaP256Verifier)
1346 .expect("1-cert chain must pass chain_walk");
1347 }
1348
1349 /// 2-cert chain (leaf + intermediate) with root as anchor.
1350 ///
1351 /// Oracle: openssl verify -CAfile vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
1352 #[test]
1353 fn two_cert_chain_ok() {
1354 let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1355 let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1356 let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1357 let policy = policy_at(GRY_NOW);
1358 let anchor = TrustAnchor::from_cert(root);
1359 chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier)
1360 .expect("2-cert chain must pass chain_walk");
1361 }
1362
1363 /// Leaf with corrupted signature — last byte flipped.
1364 ///
1365 /// The DER structure remains valid; only the BIT STRING content is wrong.
1366 /// Expect SignatureInvalid at chain index 0.
1367 #[test]
1368 fn corrupted_signature_returns_signature_invalid() {
1369 let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
1370 *leaf_der.last_mut().unwrap() ^= 0xFF;
1371 let leaf = Certificate::from_der(&leaf_der).expect("parse still succeeds after bit flip");
1372 let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1373 let anchor = TrustAnchor::from_cert(load(include_bytes!("../tests/fixtures/vxf-root.der")));
1374 let policy = policy_at(GRY_NOW);
1375 assert!(
1376 matches!(
1377 chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1378 Err(Error::SignatureInvalid { index: 0 })
1379 ),
1380 "corrupted leaf signature must return SignatureInvalid {{ index: 0 }}"
1381 );
1382 }
1383
1384 /// Chain where the leaf's issuer field does not match the intermediate's subject.
1385 ///
1386 /// Oracle: chk-leaf-wrong-issuer was signed by chk-int's private key
1387 /// (signature IS valid), but its issuer field = "PKIX-WRONG-ISSUER" by design.
1388 #[test]
1389 fn wrong_issuer_name_returns_chain_broken() {
1390 let root = load(include_bytes!("../tests/fixtures/chk-root.der"));
1391 let int_cert = load(include_bytes!("../tests/fixtures/chk-int.der"));
1392 let leaf_wrong = load(include_bytes!(
1393 "../tests/fixtures/chk-leaf-wrong-issuer.der"
1394 ));
1395 let policy = policy_at(GRY_NOW);
1396 let anchor = TrustAnchor::from_cert(root);
1397 assert!(
1398 matches!(
1399 chain_walk(
1400 &[leaf_wrong, int_cert],
1401 &anchor,
1402 &policy,
1403 &EcdsaP256Verifier
1404 ),
1405 Err(Error::ChainBroken { index: 0 })
1406 ),
1407 "leaf with wrong issuer must return ChainBroken {{ index: 0 }}"
1408 );
1409 }
1410
1411 // --- PKIX-gry per-cert check tests ---
1412
1413 /// Expired leaf cert → ValidityPeriod at index 0.
1414 ///
1415 /// Oracle: gry-leaf.der has notAfter=2027-01-01; GRY_EXPIRED=2028-01-02.
1416 /// gry-int.der has notAfter=2036-01-01, which is still valid at GRY_EXPIRED.
1417 /// Reverse walk: i=1 (gry-int) passes validity, then i=0 (gry-leaf) fails.
1418 #[test]
1419 fn expired_leaf_returns_validity_period() {
1420 let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1421 let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1422 let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1423 let policy = policy_at(GRY_EXPIRED);
1424 let anchor = TrustAnchor::from_cert(root);
1425 assert!(
1426 matches!(
1427 chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1428 Err(Error::ValidityPeriod { index: 0 })
1429 ),
1430 "expired leaf must return ValidityPeriod {{ index: 0 }}"
1431 );
1432 }
1433
1434 /// Not-yet-valid intermediate → ValidityPeriod at index 1.
1435 ///
1436 /// Oracle: gry-int.der has notBefore=2026-01-01; GRY_NOTYET=0 (1970-01-01).
1437 /// Reverse walk processes chain[1] (gry-int) first; it is not yet valid at time 0.
1438 #[test]
1439 fn notyet_valid_intermediate_returns_validity_period() {
1440 let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1441 let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1442 let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1443 let policy = policy_at(GRY_NOTYET);
1444 let anchor = TrustAnchor::from_cert(root);
1445 assert!(
1446 matches!(
1447 chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1448 Err(Error::ValidityPeriod { index: 1 })
1449 ),
1450 "not-yet-valid intermediate must return ValidityPeriod {{ index: 1 }}"
1451 );
1452 }
1453
1454 /// Leaf with unknown critical extension → UnhandledCriticalExtension at index 0.
1455 ///
1456 /// Oracle: gry-leaf-unknown-crit.der was generated with OID 1.3.6.1.5.5.7.99.99 critical=true
1457 /// (not in HANDLED_CRITICAL_OIDS) using pyca/cryptography.
1458 #[test]
1459 fn unknown_critical_extension_returns_unhandled() {
1460 let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1461 let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1462 let leaf_unk = load(include_bytes!(
1463 "../tests/fixtures/gry-leaf-unknown-crit.der"
1464 ));
1465 let policy = policy_at(GRY_NOW);
1466 let anchor = TrustAnchor::from_cert(root);
1467 assert!(
1468 matches!(
1469 chain_walk(&[leaf_unk, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1470 Err(Error::UnhandledCriticalExtension { index: 0 })
1471 ),
1472 "unknown critical ext must return UnhandledCriticalExtension {{ index: 0 }}"
1473 );
1474 }
1475}