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