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 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<E>, W: Watch, R: Recognizer<E>, E: Work = Memory> {
224    /// The current operation's shared work and storage account.
225    pub execution: &'p E,
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<E>, E: Work = Memory> {
241    pub(super) execution: &'p E,
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<E>, E: Work = Memory>: 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, E>,
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<E: Work>(
335    item: &Extension<'_>,
336    execution: &E,
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<Q>, M: Meter, Q: Work>(
370    certificate: &'a Certificate<'a>,
371    settings: &Settings<'a, '_, R, Q>,
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<E>, D: Driver<'a, R, E>, E: Work>(
479    input: Chain<'a, '_>,
480    anchor: Anchor<'a>,
481    settings: &Settings<'a, '_, R, E>,
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<E>, W: Watch, R: Recognizer<E>, E: Work> {
488    context: &'p ValidationContext<'a, 'p, V, W, R, E>,
489    origin: &'a Certificate<'a>,
490    spent: u64,
491    halted: Option<Truncation>,
492}
493
494impl<V: Verify<E>, W: Watch, R: Recognizer<E>, E: Work> Supplied<'_, '_, V, W, R, E> {
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<E>, W: Watch, R: Recognizer<E>, E: Work> Meter for Supplied<'_, '_, V, W, R, E> {
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<E>, W: Watch, R: Recognizer<E>, E: Work> Driver<'a, R, E>
522    for Supplied<'a, '_, V, W, R, E>
523{
524    type VerificationError = V::Error;
525    fn verified(
526        &mut self,
527        certificate: &'a Certificate<'a>,
528        key: WorkingKey<'a>,
529    ) -> BuildResult<Option<Verified>, V::Error, R::Error> {
530        if !self.spend(cost::SIGNATURE)? {
531            return Ok(None);
532        }
533        self.context
534            .verifier
535            .verify(
536                &certificate.signature_algorithm(),
537                key,
538                certificate.signed_bytes(),
539                certificate.signature(),
540                self.context.execution,
541            )
542            .map(Some)
543            .map_err(BuildError::Verification)
544    }
545    fn facts(
546        &mut self,
547        certificate: &'a Certificate<'a>,
548        settings: &Settings<'a, '_, R, E>,
549    ) -> BuildResult<Option<CertificateFacts<'a>>, V::Error, R::Error> {
550        certificate_facts(certificate, settings, self)
551    }
552}
553
554/// Checks exactly the supplied leaf-first path to one explicit anchor.
555///
556/// The anchor is excluded from `chain`. This returns static path facts only;
557/// revocation, purpose, key strength and application permissions remain caller
558/// decisions. It neither searches alternative issuers nor re-encodes signed data.
559///
560/// A completed rejection is a value. Processing/resource and injected callback
561/// errors remain `BuildError`, while local bounds yield `Validation::Incomplete`.
562///
563/// ```
564/// use structured_asn1::memory::Memory;
565/// use structured_pkix::{Certificate, path};
566///
567/// fn check<'a, V: path::Verify>(
568///     chain: &[&'a Certificate<'a>],
569///     anchor: path::Anchor<'a>,
570///     execution: &Memory,
571///     interval: path::Interval,
572///     verifier: &V,
573/// ) -> path::BuildResult<path::Validation<'a>, V::Error, core::convert::Infallible> {
574///     let context = path::ValidationContext {
575///         execution,
576///         interval,
577///         policy: path::Policy::default(),
578///         bounds: path::ValidationBounds::default(),
579///         verifier,
580///         watch: &(),
581///         recognizer: &(),
582///     };
583///     path::validate(chain, anchor, &context)
584/// }
585/// ```
586pub fn validate<'a, V: Verify<E>, W: Watch, R: Recognizer<E>, E: Work>(
587    chain: &[&'a Certificate<'a>],
588    anchor: Anchor<'a>,
589    context: &ValidationContext<'a, '_, V, W, R, E>,
590) -> BuildResult<Validation<'a>, V::Error, R::Error> {
591    validate_effective(chain, EffectiveAnchor::standing(anchor), context)
592}
593
594/// Check the supplied local path with an explicitly bound effective anchor key.
595/// This shares every static check with [`validate`], including the local anchor
596/// certificate's name constraints. It does not resume constraints or policy
597/// state inherited from a previously validated ancestor path. For original-root
598/// validation, pass that complete path and its original anchor instead.
599pub fn validate_effective<'a, V: Verify<E>, W: Watch, R: Recognizer<E>, E: Work>(
600    chain: &[&'a Certificate<'a>],
601    effective: EffectiveAnchor<'a>,
602    context: &ValidationContext<'a, '_, V, W, R, E>,
603) -> BuildResult<Validation<'a>, V::Error, R::Error> {
604    let anchor = effective.anchor;
605    let Some(target) = chain.first() else {
606        return Ok(Validation::Rejected(Rejection {
607            at: None,
608            reason: RejectionReason::EmptyPath,
609        }));
610    };
611    if chain.len() > context.bounds.depth {
612        return Ok(Validation::Incomplete(Truncation::Depth));
613    }
614    let length = u32::try_from(chain.len()).map_err(|_| {
615        target
616            .tbs()
617            .subject()
618            .resource(MemoryError::CapacityOverflow)
619    })?;
620    length.checked_add(1).ok_or_else(|| {
621        target
622            .tbs()
623            .subject()
624            .resource(MemoryError::CapacityOverflow)
625    })?;
626    let mut driver = Supplied {
627        context,
628        origin: target,
629        spent: 0,
630        halted: None,
631    };
632    let mut seen = context.execution.map();
633    for (index, certificate) in chain.iter().enumerate() {
634        if let Some(halted) =
635            driver.admit(cost::ENTRY + certificate.as_bytes().len() as u64 / cost::HASHED)?
636        {
637            return Ok(Validation::Incomplete(halted));
638        }
639        if let Some(earlier) = seen
640            .try_insert(certificate.as_bytes(), index)
641            .map_err(|reason| certificate.tbs().subject().resource(reason))?
642        {
643            return Ok(rejection(
644                ConstraintSource::Certificate(index),
645                RejectionReason::DuplicateCertificate { earlier },
646            ));
647        }
648        if let Some(published) = anchor.certificate() {
649            let bytes = certificate.as_bytes().len().min(published.as_bytes().len()) as u64;
650            if let Some(halted) = driver.admit(bytes / cost::COMPARED)? {
651                return Ok(Validation::Incomplete(halted));
652            }
653            context
654                .execution
655                .charge_work(bytes)
656                .map_err(|reason| certificate.tbs().subject().resource(reason))?;
657            if super::same(certificate, published) {
658                return Ok(rejection(
659                    ConstraintSource::Certificate(index),
660                    RejectionReason::AnchorIncluded,
661                ));
662            }
663        }
664    }
665    drop(seen);
666    let settings = Settings {
667        execution: context.execution,
668        interval: context.interval,
669        policy: context.policy,
670        policies: context.bounds.policies,
671        recognizer: context.recognizer,
672    };
673    let outcome = processing::process(Chain::Supplied(chain), &effective, &settings, &mut driver)?;
674    if let Some(halted) = driver.halted {
675        return Ok(Validation::Incomplete(halted));
676    }
677    Ok(outcome)
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use crate::fixtures::{CertificateParts, oid as encoded_oid, printable, seq, set};
684    use core::cell::Cell;
685    use structured_asn1::{BitString, Parser};
686
687    struct Answer {
688        result: Verified,
689        calls: Cell<usize>,
690    }
691    impl Verify for Answer {
692        type Error = core::convert::Infallible;
693        fn verify(
694            &self,
695            _algorithm: &crate::AlgorithmIdentifier<'_>,
696            _key: WorkingKey<'_>,
697            _signed: &[u8],
698            _signature: BitString<'_>,
699            _execution: &Memory,
700        ) -> core::result::Result<Verified, Self::Error> {
701            self.calls.set(self.calls.get() + 1);
702            Ok(self.result)
703        }
704    }
705
706    fn target() -> alloc::vec::Vec<u8> {
707        CertificateParts {
708            subject: seq(&[set(&[seq(&[
709                encoded_oid(oid::COMMON_NAME),
710                printable("target"),
711            ])])]),
712            ..CertificateParts::default()
713        }
714        .build()
715    }
716
717    fn root() -> alloc::vec::Vec<u8> {
718        let parts = CertificateParts::default();
719        CertificateParts {
720            subject: parts.issuer.clone(),
721            ..parts
722        }
723        .build()
724    }
725
726    #[test]
727    fn supplied_path_separates_success_no_and_unsupported() {
728        // The same public operation preserves all three completed provider answers.
729        let root_der = root();
730        let leaf_der = target();
731        let parser = Parser::der(crate::fixtures::memory());
732        let root = Certificate::read(&parser, &root_der, 0).unwrap();
733        let leaf = Certificate::read(&parser, &leaf_der, 100).unwrap();
734        let anchor = Anchor::new(root.tbs().subject(), root.tbs().public_key());
735        for result in [Verified::Yes, Verified::No, Verified::Unsupported] {
736            let execution = crate::fixtures::memory();
737            let verifier = Answer {
738                result,
739                calls: Cell::new(0),
740            };
741            let context = ValidationContext {
742                execution: &execution,
743                interval: Interval::at(leaf.tbs().validity().not_before().seconds()),
744                policy: Policy::default(),
745                bounds: ValidationBounds::default(),
746                verifier: &verifier,
747                watch: &(),
748                recognizer: &(),
749            };
750            let outcome = crate::path::validate(&[&leaf], anchor, &context).unwrap();
751            match (result, outcome) {
752                (Verified::Yes, Validation::Accepted(facts)) => {
753                    assert_eq!(
754                        facts.key.key().as_bytes(),
755                        leaf.tbs().public_key().as_bytes()
756                    );
757                    assert_eq!(facts.policies(), []);
758                }
759                (Verified::No, Validation::Rejected(rejection)) => {
760                    assert_eq!(rejection.at, Some(ConstraintSource::Certificate(0)));
761                    assert_eq!(rejection.reason, RejectionReason::Signature);
762                }
763                (Verified::Unsupported, Validation::Rejected(rejection)) => {
764                    assert_eq!(rejection.at, Some(ConstraintSource::Certificate(0)));
765                    assert_eq!(rejection.reason, RejectionReason::UnsupportedSignature);
766                }
767                other => panic!("unexpected public path result: {other:?}"),
768            }
769            assert_eq!(verifier.calls.get(), 1);
770            assert_eq!(execution.used(), 0);
771        }
772    }
773
774    #[test]
775    fn supplied_local_work_refusal_never_calls_the_provider() {
776        // Local work exhaustion is incomplete; it is not a signature rejection.
777        let root_der = root();
778        let leaf_der = target();
779        let parser = Parser::der(crate::fixtures::memory());
780        let root = Certificate::read(&parser, &root_der, 0).unwrap();
781        let leaf = Certificate::read(&parser, &leaf_der, 100).unwrap();
782        let execution = crate::fixtures::memory();
783        let verifier = Answer {
784            result: Verified::Yes,
785            calls: Cell::new(0),
786        };
787        let context = ValidationContext {
788            execution: &execution,
789            interval: Interval::at(leaf.tbs().validity().not_before().seconds()),
790            policy: Policy::default(),
791            bounds: ValidationBounds {
792                work: 0,
793                ..ValidationBounds::default()
794            },
795            verifier: &verifier,
796            watch: &(),
797            recognizer: &(),
798        };
799        assert!(matches!(
800            crate::path::validate(
801                &[&leaf],
802                Anchor::new(root.tbs().subject(), root.tbs().public_key()),
803                &context
804            )
805            .unwrap(),
806            Validation::Incomplete(Truncation::Work)
807        ));
808        assert_eq!(verifier.calls.get(), 0);
809        assert_eq!(execution.used(), 0);
810        assert_eq!(execution.work_used(), 0);
811    }
812}