Skip to main content

structured_pkix/path/
validation.rs

1//! Static certificate-path processing shared by construction and supplied paths.
2//! RFC5280 §6.1 and RFC9618 govern the common core. Revocation, application
3//! purpose, algorithm permissions and trust-anchor selection remain external.
4
5use super::{
6    Anchor, BuildError, BuildResult, Interval, Meter, Policy, Truncation, Verified, Verify, Watch,
7    WorkingKey, cost, sorting_rounds,
8};
9use crate::constraints::BasicConstraints;
10use crate::extension::{Extension, Recognizer};
11use crate::{Certificate, ErrorCode, Name, oid};
12use structured_asn1::Oid;
13use structured_asn1::memory::{Memory, MemoryError, OwnedVec};
14
15mod processing;
16
17/// Explicit local trust anchor with an effective, possibly inherited key.
18/// Binding proves original SPKI identity, not trust or a continuation of an
19/// earlier path's accumulated constraints. The caller selects this boundary.
20#[derive(Debug, Clone, Copy)]
21pub struct EffectiveAnchor<'a> {
22    anchor: Anchor<'a>,
23    key: WorkingKey<'a>,
24}
25
26impl<'a> EffectiveAnchor<'a> {
27    /// Bind a previously projected effective key to this anchor's original
28    /// SPKI. `None` denotes completed source mismatch; resource errors remain
29    /// errors. Inherited parameter bytes continue borrowing their source.
30    pub fn bind(
31        anchor: Anchor<'a>,
32        key: WorkingKey<'a>,
33        execution: &Memory,
34    ) -> crate::Result<Option<Self>> {
35        let left = anchor.key().as_bytes();
36        let right = key.key().as_bytes();
37        let work = u64::try_from(left.len().max(right.len()))
38            .ok()
39            .and_then(|n| n.checked_mul(2))
40            .and_then(|n| n.checked_add(1))
41            .ok_or_else(|| anchor.name().resource(MemoryError::WorkExceeded))?;
42        execution
43            .charge_work(work)
44            .map_err(|reason| anchor.name().resource(reason))?;
45        if left != right {
46            return Ok(None);
47        }
48        Ok(Some(Self { anchor, key }))
49    }
50
51    /// Caller-selected local anchor, retaining its certificate constraints.
52    #[must_use]
53    pub fn anchor(self) -> Anchor<'a> {
54        self.anchor
55    }
56
57    /// Effective key bound to the anchor's original SPKI.
58    #[must_use]
59    pub fn key(self) -> WorkingKey<'a> {
60        self.key
61    }
62
63    fn standing(anchor: Anchor<'a>) -> Self {
64        Self {
65            anchor,
66            key: WorkingKey::standing(anchor.key()),
67        }
68    }
69}
70
71/// The source of a path fact; certificate positions are leaf-first.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ConstraintSource {
74    /// The explicitly supplied trust anchor.
75    Anchor,
76    /// A certificate at this position in the supplied path.
77    Certificate(usize),
78}
79
80/// How a presented name fails an accumulated name constraint.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum NameConstraintFailure {
83    /// An authority explicitly excluded the name.
84    Excluded,
85    /// No permitted subtree of the relevant authority contains the name.
86    NotPermitted,
87    /// A relevant constraint cannot be enforced by this implementation/provider.
88    Unenforceable,
89}
90
91/// A completed semantic rejection, distinct from inability to execute a check.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum RejectionReason<'a> {
95    /// A certification path must contain at least one certificate.
96    EmptyPath,
97    /// The same encoded certificate already occurs at the named position.
98    DuplicateCertificate {
99        /// The earlier leaf-first position.
100        earlier: usize,
101    },
102    /// The trust anchor's own certificate is not part of a prospective path.
103    AnchorIncluded,
104    /// The certificate does not cover the requested validation interval.
105    Validity,
106    /// The certificate's issuer name differs from the expected issuer.
107    Issuer,
108    /// An intermediate does not assert certificate-authority status.
109    NotAuthority,
110    /// An intermediate's `KeyUsage` forbids signing certificates.
111    KeyCertSign,
112    /// An asserted authority has an empty subject name.
113    EmptyAuthorityName,
114    /// The path exceeds an authority's pathLenConstraint.
115    PathLength {
116        /// The authority whose retained limit was exhausted.
117        source: ConstraintSource,
118        /// The original pathLenConstraint, before counter decrements.
119        limit: u32,
120    },
121    /// The provider completed the signature check with a negative answer.
122    Signature,
123    /// The provider cannot verify this algorithm/key/parameter combination.
124    UnsupportedSignature,
125    /// A critical extension is not understood.
126    CriticalExtension(Oid<'a>),
127    /// A relevant extension could not be decoded semantically.
128    MalformedExtension {
129        /// The extension's type identifier.
130        extension: Oid<'a>,
131        /// The original decoding code.
132        code: ErrorCode,
133        /// The original schema field.
134        field: &'static str,
135        /// The original document offset.
136        offset: u64,
137    },
138    /// A name constraint is carried by a certificate that is not a CA.
139    NameConstraintsOnNonAuthority,
140    /// A name violates a constraint originating above this certificate.
141    NameConstraints {
142        /// The authority or anchor that supplied the decisive constraint.
143        source: ConstraintSource,
144        /// The completed constraint result.
145        failure: NameConstraintFailure,
146    },
147    /// The path does not satisfy the requested/required certificate policies.
148    Policies,
149}
150
151/// A semantic failure at its source; `None` denotes the whole supplied input.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct Rejection<'a> {
154    /// Leaf-first certificate position, anchor, or whole-input failure.
155    pub at: Option<ConstraintSource>,
156    /// The completed rejection reason.
157    pub reason: RejectionReason<'a>,
158}
159
160/// Accepted static path facts, not a complete application trust verdict.
161/// The policy container owns and accounts its allocation; its OID values and
162/// the effective key still borrow the original certificate/anchor input.
163#[derive(Debug)]
164pub struct ValidationFacts<'a> {
165    /// The target's effective key, including inherited algorithm parameters.
166    pub key: WorkingKey<'a>,
167    /// Intermediate CAs that are not self-issued, excluding target and anchor.
168    /// This scalar lets caller anchor-policy checks reuse the completed census.
169    pub non_self_issued_intermediates: u32,
170    policies: OwnedVec<Oid<'a>>,
171}
172
173impl<'a> ValidationFacts<'a> {
174    /// Accepted authority/user policy identifiers in deterministic encoded order.
175    #[must_use]
176    pub fn policies(&self) -> &[Oid<'a>] {
177        &self.policies
178    }
179
180    /// Transfers owned policy-set storage with its retained capacity charge.
181    #[must_use]
182    pub fn into_policies(self) -> OwnedVec<Oid<'a>> {
183        self.policies
184    }
185}
186
187/// The completed outcome of checking exactly one supplied path.
188#[derive(Debug)]
189pub enum Validation<'a> {
190    /// Static certificate-path checks completed successfully.
191    Accepted(ValidationFacts<'a>),
192    /// Static checks completed with a semantic rejection.
193    Rejected(Rejection<'a>),
194    /// A local processing limit stopped the operation; no accepted facts exist.
195    Incomplete(Truncation),
196}
197
198/// Local limits for checking a supplied path, with no alternative-path search.
199#[derive(Debug, Clone, Copy)]
200pub struct ValidationBounds {
201    /// Maximum number of supplied certificates, excluding the anchor.
202    pub depth: usize,
203    /// Maximum cumulative work units for this check.
204    pub work: u64,
205    /// Retained policy graph nodes, parent edges and expected OIDs together.
206    pub policies: usize,
207}
208
209impl Default for ValidationBounds {
210    fn default() -> Self {
211        let bounds = super::Bounds::default();
212        Self {
213            depth: bounds.depth,
214            work: bounds.work,
215            policies: bounds.policies,
216        }
217    }
218}
219
220/// Explicit execution inputs for static checking of a borrowed supplied path.
221/// Callers supply trusted anchors, cryptographic capability and application
222/// policy separately; no clock, network or trust discovery is performed here.
223pub struct ValidationContext<'a, 'p, V: Verify, W: Watch, R: Recognizer> {
224    /// The current operation's shared work and storage account.
225    pub execution: &'p Memory,
226    /// The interval every certificate must cover.
227    pub interval: Interval,
228    /// Certificate-policy processing inputs.
229    pub policy: Policy<'a>,
230    /// Local bounds that only tighten the shared execution budget.
231    pub bounds: ValidationBounds,
232    /// Injected signature verifier; it accounts its own internal work/storage.
233    pub verifier: &'p V,
234    /// Caller-provided deadline check; `()` means no clock, with work still bounded.
235    pub watch: &'p W,
236    /// Injected recognition of scheme-specific critical extensions/names.
237    pub recognizer: &'p R,
238}
239
240pub(super) struct Settings<'a, 'p, R: Recognizer> {
241    pub(super) execution: &'p Memory,
242    pub(super) interval: Interval,
243    pub(super) policy: Policy<'a>,
244    pub(super) policies: usize,
245    pub(super) recognizer: &'p R,
246}
247
248/// Cached by search, computed directly for a supplied path without cache storage.
249#[derive(Debug, Clone, Copy)]
250pub(super) struct CertificateFacts<'a> {
251    pub(super) self_issued: bool,
252    pub(super) basic: Option<BasicConstraints>,
253    pub(super) issuing: Option<RejectionReason<'a>>,
254    pub(super) rejected: Option<RejectionReason<'a>>,
255}
256
257pub(super) trait Driver<'a, R: Recognizer>: Meter {
258    type VerificationError;
259    fn verified(
260        &mut self,
261        certificate: &'a Certificate<'a>,
262        key: WorkingKey<'a>,
263    ) -> BuildResult<Option<Verified>, Self::VerificationError, R::Error>;
264    fn facts(
265        &mut self,
266        certificate: &'a Certificate<'a>,
267        settings: &Settings<'a, '_, R>,
268    ) -> BuildResult<Option<CertificateFacts<'a>>, Self::VerificationError, R::Error>;
269}
270
271/// Search constructs this witness only after all selected issuer-name bindings
272/// and the terminal anchor binding were checked. The supplied variant performs
273/// those same Name comparisons here, without repeating search's proven work.
274#[derive(Clone, Copy)]
275pub(super) enum Chain<'a, 'p> {
276    Supplied(&'p [&'a Certificate<'a>]),
277    Constructed(&'p [&'a Certificate<'a>]),
278}
279
280enum Decoded<T> {
281    Value(Option<T>),
282    Rejected(RejectionReason<'static>),
283    Incomplete,
284}
285
286fn decode<T>(
287    meter: &mut impl Meter,
288    kind: Oid<'static>,
289    action: impl FnOnce() -> crate::Result<Option<T>>,
290) -> crate::Result<Decoded<T>> {
291    if !meter.spend(cost::EXTENSION)? {
292        return Ok(Decoded::Incomplete);
293    }
294    Ok(match decoded(kind, action())? {
295        Ok(value) => Decoded::Value(value),
296        Err(reason) => Decoded::Rejected(reason),
297    })
298}
299
300fn decoded<T>(
301    kind: Oid<'static>,
302    result: crate::Result<T>,
303) -> crate::Result<core::result::Result<T, RejectionReason<'static>>> {
304    match result {
305        Ok(value) => Ok(Ok(value)),
306        Err(error) if error.is_resource() => Err(error),
307        Err(error) => Ok(Err(RejectionReason::MalformedExtension {
308            extension: kind,
309            code: error.code(),
310            field: error.field(),
311            offset: error.offset(),
312        })),
313    }
314}
315
316pub(super) fn spend_names(
317    meter: &mut impl Meter,
318    left: &Name<'_>,
319    right: &Name<'_>,
320) -> crate::Result<bool> {
321    let parts = left.parts().len() as u64 + right.parts().len() as u64;
322    if !meter.spend(parts)? {
323        return Ok(false);
324    }
325    let bytes = left.as_bytes().len() as u64 + right.as_bytes().len() as u64;
326    let rounds = sorting_rounds(left).max(sorting_rounds(right));
327    let work = rounds
328        .checked_mul(bytes / cost::COMPARED)
329        .and_then(|work| work.checked_add(cost::NAME))
330        .ok_or_else(|| left.resource(MemoryError::WorkExceeded))?;
331    meter.spend(work)
332}
333
334fn critical_policy(
335    item: &Extension<'_>,
336    execution: &Memory,
337) -> Option<(Oid<'static>, crate::Result<bool>)> {
338    // Known path extensions retain decoder failures even when critical.
339    let kind = item.kind();
340    if kind == oid::CERTIFICATE_POLICIES {
341        Some((
342            oid::CERTIFICATE_POLICIES,
343            // RFC5280 §4.2.1.4 requires interpretation of all qualifiers
344            // when critical. Reuse the ordinary qualifier semantic core on
345            // this decoded value rather than treating shape as understanding.
346            crate::policy::CertificatePolicies::read(item, execution)
347                .and_then(|policies| policies.understood(item.parser(), execution)),
348        ))
349    } else if kind == oid::POLICY_CONSTRAINTS {
350        Some((
351            oid::POLICY_CONSTRAINTS,
352            crate::constraints::PolicyConstraints::read(item, execution).map(|_| true),
353        ))
354    } else if kind == oid::POLICY_MAPPINGS {
355        Some((
356            oid::POLICY_MAPPINGS,
357            crate::policy::PolicyMappings::read(item, execution).map(|_| true),
358        ))
359    } else if kind == oid::INHIBIT_ANY_POLICY {
360        Some((
361            oid::INHIBIT_ANY_POLICY,
362            crate::constraints::InhibitAnyPolicy::read(item, execution).map(|_| true),
363        ))
364    } else {
365        None
366    }
367}
368
369pub(super) fn certificate_facts<'a, E, R: Recognizer, M: Meter>(
370    certificate: &'a Certificate<'a>,
371    settings: &Settings<'a, '_, R>,
372    meter: &mut M,
373) -> BuildResult<Option<CertificateFacts<'a>>, E, R::Error> {
374    let execution = settings.execution;
375    let extensions = certificate.tbs().extensions();
376    if !meter.spend(cost::CHECK)? {
377        return Ok(None);
378    }
379    let mut facts = CertificateFacts {
380        self_issued: false,
381        basic: None,
382        issuing: Some(RejectionReason::NotAuthority),
383        rejected: None,
384    };
385    if !settings.interval.inside(certificate.tbs().validity()) {
386        facts.rejected = Some(RejectionReason::Validity);
387        return Ok(Some(facts));
388    }
389    if !spend_names(
390        meter,
391        certificate.tbs().subject(),
392        certificate.tbs().issuer(),
393    )? {
394        return Ok(None);
395    }
396    facts.self_issued = certificate
397        .tbs()
398        .subject()
399        .matches(certificate.tbs().issuer(), execution)?;
400    match decode(meter, oid::BASIC_CONSTRAINTS, || {
401        extensions.basic_constraints(execution)
402    })? {
403        Decoded::Value(basic) => facts.basic = basic,
404        Decoded::Rejected(reason) => {
405            facts.rejected = Some(reason);
406            return Ok(Some(facts));
407        }
408        Decoded::Incomplete => return Ok(None),
409    }
410    if facts.basic.is_some_and(BasicConstraints::is_authority) {
411        facts.issuing = None;
412        if certificate.tbs().subject().is_empty() {
413            facts.rejected = Some(RejectionReason::EmptyAuthorityName);
414            return Ok(Some(facts));
415        }
416        match decode(meter, oid::KEY_USAGE, || extensions.key_usage(execution))? {
417            Decoded::Value(Some(usage)) if !usage.key_cert_sign() => {
418                facts.issuing = Some(RejectionReason::KeyCertSign);
419            }
420            Decoded::Rejected(reason) => facts.issuing = Some(reason),
421            Decoded::Incomplete => return Ok(None),
422            Decoded::Value(_) => {}
423        }
424    }
425    for demand in extensions.criticals(execution) {
426        let demand = demand?;
427        if !meter.spend(cost::EXTENSION)? {
428            return Ok(None);
429        }
430        // The common processor decodes and retains nameConstraints before
431        // verification, then enforces it on subordinate names. Decoding here
432        // as well would allocate the same constraint payload twice.
433        if demand.kind() == oid::NAME_CONSTRAINTS || demand.kind() == oid::BASIC_CONSTRAINTS {
434            continue;
435        }
436        if demand.kind() == oid::KEY_USAGE
437            && facts.basic.is_some_and(BasicConstraints::is_authority)
438        {
439            if matches!(
440                facts.issuing,
441                Some(RejectionReason::MalformedExtension { .. })
442            ) {
443                facts.rejected = facts.issuing;
444                return Ok(Some(facts));
445            }
446            continue;
447        }
448        // RFC5280 §6.1.3/§6.1.4/§6.1.5: these are known path-processing
449        // extensions. A malformed value keeps its decoder details even if
450        // critical; it is not an unrecognized extension. Critical mappings
451        // and inhibitAnyPolicy must decode even on the final certificate,
452        // where their intermediate-only state transitions do not apply.
453        if let Some((kind, result)) = critical_policy(demand, execution) {
454            let reason = match decoded(kind, result)? {
455                Ok(true) => continue,
456                Ok(false) => RejectionReason::CriticalExtension(kind),
457                Err(reason) => reason,
458            };
459            facts.rejected = Some(reason);
460            return Ok(Some(facts));
461        }
462        if extensions.understands(demand, settings.recognizer, execution)? {
463            continue;
464        }
465        facts.rejected = Some(RejectionReason::CriticalExtension(demand.kind()));
466        return Ok(Some(facts));
467    }
468    Ok(Some(facts))
469}
470
471fn rejection(at: ConstraintSource, reason: RejectionReason<'_>) -> Validation<'_> {
472    Validation::Rejected(Rejection {
473        at: Some(at),
474        reason,
475    })
476}
477
478pub(super) fn process<'a, R: Recognizer, D: Driver<'a, R>>(
479    input: Chain<'a, '_>,
480    anchor: Anchor<'a>,
481    settings: &Settings<'a, '_, R>,
482    driver: &mut D,
483) -> BuildResult<Validation<'a>, D::VerificationError, R::Error> {
484    processing::process(input, &EffectiveAnchor::standing(anchor), settings, driver)
485}
486
487struct Supplied<'a, 'p, V: Verify, W: Watch, R: Recognizer> {
488    context: &'p ValidationContext<'a, 'p, V, W, R>,
489    origin: &'a Certificate<'a>,
490    spent: u64,
491    halted: Option<Truncation>,
492}
493
494impl<V: Verify, W: Watch, R: Recognizer> Supplied<'_, '_, V, W, R> {
495    fn admit(&mut self, work: u64) -> crate::Result<Option<Truncation>> {
496        if work > self.context.bounds.work - self.spent {
497            return Ok(Some(Truncation::Work));
498        }
499        if self.context.watch.out_of_time() {
500            return Ok(Some(Truncation::Time));
501        }
502        self.context
503            .execution
504            .charge_work(work)
505            .map_err(|reason| self.origin.tbs().subject().resource(reason))?;
506        self.spent += work;
507        Ok(None)
508    }
509}
510
511impl<V: Verify, W: Watch, R: Recognizer> Meter for Supplied<'_, '_, V, W, R> {
512    fn spend(&mut self, work: u64) -> crate::Result<bool> {
513        if let Some(halted) = self.admit(work)? {
514            self.halted = Some(halted);
515            return Ok(false);
516        }
517        Ok(true)
518    }
519}
520
521impl<'a, V: Verify, W: Watch, R: Recognizer> Driver<'a, R> for Supplied<'a, '_, V, W, R> {
522    type VerificationError = V::Error;
523    fn verified(
524        &mut self,
525        certificate: &'a Certificate<'a>,
526        key: WorkingKey<'a>,
527    ) -> BuildResult<Option<Verified>, V::Error, R::Error> {
528        if !self.spend(cost::SIGNATURE)? {
529            return Ok(None);
530        }
531        self.context
532            .verifier
533            .verify(
534                &certificate.signature_algorithm(),
535                key,
536                certificate.signed_bytes(),
537                certificate.signature(),
538                self.context.execution,
539            )
540            .map(Some)
541            .map_err(BuildError::Verification)
542    }
543    fn facts(
544        &mut self,
545        certificate: &'a Certificate<'a>,
546        settings: &Settings<'a, '_, R>,
547    ) -> BuildResult<Option<CertificateFacts<'a>>, V::Error, R::Error> {
548        certificate_facts(certificate, settings, self)
549    }
550}
551
552/// Checks exactly the supplied leaf-first path to one explicit anchor.
553///
554/// The anchor is excluded from `chain`. This returns static path facts only;
555/// revocation, purpose, key strength and application permissions remain caller
556/// decisions. It neither searches alternative issuers nor re-encodes signed data.
557///
558/// A completed rejection is a value. Processing/resource and injected callback
559/// errors remain `BuildError`, while local bounds yield `Validation::Incomplete`.
560///
561/// ```
562/// use structured_asn1::memory::Memory;
563/// use structured_pkix::{Certificate, path};
564///
565/// fn check<'a, V: path::Verify>(
566///     chain: &[&'a Certificate<'a>],
567///     anchor: path::Anchor<'a>,
568///     execution: &Memory,
569///     interval: path::Interval,
570///     verifier: &V,
571/// ) -> path::BuildResult<path::Validation<'a>, V::Error, core::convert::Infallible> {
572///     let context = path::ValidationContext {
573///         execution,
574///         interval,
575///         policy: path::Policy::default(),
576///         bounds: path::ValidationBounds::default(),
577///         verifier,
578///         watch: &(),
579///         recognizer: &(),
580///     };
581///     path::validate(chain, anchor, &context)
582/// }
583/// ```
584pub fn validate<'a, V: Verify, W: Watch, R: Recognizer>(
585    chain: &[&'a Certificate<'a>],
586    anchor: Anchor<'a>,
587    context: &ValidationContext<'a, '_, V, W, R>,
588) -> BuildResult<Validation<'a>, V::Error, R::Error> {
589    validate_effective(chain, EffectiveAnchor::standing(anchor), context)
590}
591
592/// Check the supplied local path with an explicitly bound effective anchor key.
593/// This shares every static check with [`validate`], including the local anchor
594/// certificate's name constraints. It does not resume constraints or policy
595/// state inherited from a previously validated ancestor path. For original-root
596/// validation, pass that complete path and its original anchor instead.
597pub fn validate_effective<'a, V: Verify, W: Watch, R: Recognizer>(
598    chain: &[&'a Certificate<'a>],
599    effective: EffectiveAnchor<'a>,
600    context: &ValidationContext<'a, '_, V, W, R>,
601) -> BuildResult<Validation<'a>, V::Error, R::Error> {
602    let anchor = effective.anchor;
603    let Some(target) = chain.first() else {
604        return Ok(Validation::Rejected(Rejection {
605            at: None,
606            reason: RejectionReason::EmptyPath,
607        }));
608    };
609    if chain.len() > context.bounds.depth {
610        return Ok(Validation::Incomplete(Truncation::Depth));
611    }
612    let length = u32::try_from(chain.len()).map_err(|_| {
613        target
614            .tbs()
615            .subject()
616            .resource(MemoryError::CapacityOverflow)
617    })?;
618    length.checked_add(1).ok_or_else(|| {
619        target
620            .tbs()
621            .subject()
622            .resource(MemoryError::CapacityOverflow)
623    })?;
624    let mut driver = Supplied {
625        context,
626        origin: target,
627        spent: 0,
628        halted: None,
629    };
630    let mut seen = context.execution.map();
631    for (index, certificate) in chain.iter().enumerate() {
632        if let Some(halted) =
633            driver.admit(cost::ENTRY + certificate.as_bytes().len() as u64 / cost::HASHED)?
634        {
635            return Ok(Validation::Incomplete(halted));
636        }
637        if let Some(earlier) = seen
638            .try_insert(certificate.as_bytes(), index)
639            .map_err(|reason| certificate.tbs().subject().resource(reason))?
640        {
641            return Ok(rejection(
642                ConstraintSource::Certificate(index),
643                RejectionReason::DuplicateCertificate { earlier },
644            ));
645        }
646        if let Some(published) = anchor.certificate() {
647            let bytes = certificate.as_bytes().len().min(published.as_bytes().len()) as u64;
648            if let Some(halted) = driver.admit(bytes / cost::COMPARED)? {
649                return Ok(Validation::Incomplete(halted));
650            }
651            context
652                .execution
653                .charge_work(bytes)
654                .map_err(|reason| certificate.tbs().subject().resource(reason))?;
655            if super::same(certificate, published) {
656                return Ok(rejection(
657                    ConstraintSource::Certificate(index),
658                    RejectionReason::AnchorIncluded,
659                ));
660            }
661        }
662    }
663    drop(seen);
664    let settings = Settings {
665        execution: context.execution,
666        interval: context.interval,
667        policy: context.policy,
668        policies: context.bounds.policies,
669        recognizer: context.recognizer,
670    };
671    let outcome = processing::process(Chain::Supplied(chain), &effective, &settings, &mut driver)?;
672    if let Some(halted) = driver.halted {
673        return Ok(Validation::Incomplete(halted));
674    }
675    Ok(outcome)
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681    use crate::fixtures::{CertificateParts, oid as encoded_oid, printable, seq, set};
682    use core::cell::Cell;
683    use structured_asn1::{BitString, Parser};
684
685    struct Answer {
686        result: Verified,
687        calls: Cell<usize>,
688    }
689    impl Verify for Answer {
690        type Error = core::convert::Infallible;
691        fn verify(
692            &self,
693            _algorithm: &crate::AlgorithmIdentifier<'_>,
694            _key: WorkingKey<'_>,
695            _signed: &[u8],
696            _signature: BitString<'_>,
697            _execution: &Memory,
698        ) -> core::result::Result<Verified, Self::Error> {
699            self.calls.set(self.calls.get() + 1);
700            Ok(self.result)
701        }
702    }
703
704    fn target() -> alloc::vec::Vec<u8> {
705        CertificateParts {
706            subject: seq(&[set(&[seq(&[
707                encoded_oid(oid::COMMON_NAME),
708                printable("target"),
709            ])])]),
710            ..CertificateParts::default()
711        }
712        .build()
713    }
714
715    fn root() -> alloc::vec::Vec<u8> {
716        let parts = CertificateParts::default();
717        CertificateParts {
718            subject: parts.issuer.clone(),
719            ..parts
720        }
721        .build()
722    }
723
724    #[test]
725    fn supplied_path_separates_success_no_and_unsupported() {
726        // The same public operation preserves all three completed provider answers.
727        let root_der = root();
728        let leaf_der = target();
729        let parser = Parser::der(crate::fixtures::memory());
730        let root = Certificate::read(&parser, &root_der, 0).unwrap();
731        let leaf = Certificate::read(&parser, &leaf_der, 100).unwrap();
732        let anchor = Anchor::new(root.tbs().subject(), root.tbs().public_key());
733        for result in [Verified::Yes, Verified::No, Verified::Unsupported] {
734            let execution = crate::fixtures::memory();
735            let verifier = Answer {
736                result,
737                calls: Cell::new(0),
738            };
739            let context = ValidationContext {
740                execution: &execution,
741                interval: Interval::at(leaf.tbs().validity().not_before().seconds()),
742                policy: Policy::default(),
743                bounds: ValidationBounds::default(),
744                verifier: &verifier,
745                watch: &(),
746                recognizer: &(),
747            };
748            let outcome = crate::path::validate(&[&leaf], anchor, &context).unwrap();
749            match (result, outcome) {
750                (Verified::Yes, Validation::Accepted(facts)) => {
751                    assert_eq!(
752                        facts.key.key().as_bytes(),
753                        leaf.tbs().public_key().as_bytes()
754                    );
755                    assert_eq!(facts.policies(), []);
756                }
757                (Verified::No, Validation::Rejected(rejection)) => {
758                    assert_eq!(rejection.at, Some(ConstraintSource::Certificate(0)));
759                    assert_eq!(rejection.reason, RejectionReason::Signature);
760                }
761                (Verified::Unsupported, Validation::Rejected(rejection)) => {
762                    assert_eq!(rejection.at, Some(ConstraintSource::Certificate(0)));
763                    assert_eq!(rejection.reason, RejectionReason::UnsupportedSignature);
764                }
765                other => panic!("unexpected public path result: {other:?}"),
766            }
767            assert_eq!(verifier.calls.get(), 1);
768            assert_eq!(execution.used(), 0);
769        }
770    }
771
772    #[test]
773    fn supplied_local_work_refusal_never_calls_the_provider() {
774        // Local work exhaustion is incomplete; it is not a signature rejection.
775        let root_der = root();
776        let leaf_der = target();
777        let parser = Parser::der(crate::fixtures::memory());
778        let root = Certificate::read(&parser, &root_der, 0).unwrap();
779        let leaf = Certificate::read(&parser, &leaf_der, 100).unwrap();
780        let execution = crate::fixtures::memory();
781        let verifier = Answer {
782            result: Verified::Yes,
783            calls: Cell::new(0),
784        };
785        let context = ValidationContext {
786            execution: &execution,
787            interval: Interval::at(leaf.tbs().validity().not_before().seconds()),
788            policy: Policy::default(),
789            bounds: ValidationBounds {
790                work: 0,
791                ..ValidationBounds::default()
792            },
793            verifier: &verifier,
794            watch: &(),
795            recognizer: &(),
796        };
797        assert!(matches!(
798            crate::path::validate(
799                &[&leaf],
800                Anchor::new(root.tbs().subject(), root.tbs().public_key()),
801                &context
802            )
803            .unwrap(),
804            Validation::Incomplete(Truncation::Work)
805        ));
806        assert_eq!(verifier.calls.get(), 0);
807        assert_eq!(execution.used(), 0);
808        assert_eq!(execution.work_used(), 0);
809    }
810}