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