Skip to main content

structured_pkix/path/
validation.rs

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