Skip to main content

structured_ltv/
pki.rs

1//! Original certificate-path facts for validation-material accounting.
2//!
3//! Caller trust selection and signature/credential association stay explicit.
4//! This module executes the common PKIX processor, rather than accepting a
5//! caller-written assertion that unrelated certificates formed a valid path.
6
7use crate::timestamp::{CheckedActor, Interval as GenerationInterval, TemporalError};
8use structured_asn1::memory::{MemoryError, OwnedVec, Work};
9use structured_pkix::Certificate;
10use structured_pkix::extension::Recognizer;
11use structured_pkix::path::{
12    self, Anchor, BuildResult, Interval, Rejection, Truncation, Validation, ValidationContext,
13    ValidationFacts, Verify, Watch, WorkingKey,
14};
15use structured_pkix::temporal::LeapTable;
16
17mod authority;
18mod census;
19mod closure;
20mod root_visits;
21pub use closure::{
22    AuthorityClosure, AuthorityClosureError, AuthorityNode, AuthorityOrigin, close_authorities,
23};
24pub use root_visits::AuthorityVisitError;
25mod crl;
26pub use census::{MemberStatus, PathStatuses, StatusCensusError};
27mod origin;
28mod participants;
29mod purpose;
30pub use purpose::{AuthorityUse, AuthorityUses, Uses, UsesError, UsesResult, authority_uses};
31mod reading;
32pub use participants::{
33    Authorities, ClosureResult, Counter, CounterReceipts, Credential, CredentialGroup,
34    CredentialReceipts, Participant, ParticipantAuthority, ParticipantClosure,
35    ParticipantClosureError, ParticipantError, ParticipantRequirement, ParticipantRequirements,
36    ParticipantResult, SignerReceipt, participants,
37};
38mod responder;
39pub use reading::{CrlReading, CrlReadingBinding, CrlReadingTarget};
40mod status;
41pub use crl::{
42    CrlAuthorities, CrlAuthority, CrlBinding, CrlBindingError, CrlBindingFailure, CrlCoverage,
43    CrlCoverageBinding, CrlCoverageError, CrlTarget,
44};
45pub use responder::{ResponderBinding, ResponderError, ResponderFailure, ResponderPath};
46pub use status::{OcspBinding, OcspBindingError, OcspBindingFailure, OcspTarget};
47
48/// One original non-anchor certificate and its projected effective key.
49#[derive(Debug, Clone, Copy)]
50pub struct Member<'a> {
51    certificate: &'a Certificate<'a>,
52    key: WorkingKey<'a>,
53}
54
55impl<'a> Member<'a> {
56    /// Original certificate, without a rendered or re-encoded identity.
57    #[must_use]
58    pub fn certificate(self) -> &'a Certificate<'a> {
59        self.certificate
60    }
61
62    /// Effective key at this certificate, including inherited parameters.
63    #[must_use]
64    pub fn key(self) -> WorkingKey<'a> {
65        self.key
66    }
67}
68
69/// Static checked chain used by the common material census.
70///
71/// This does not authenticate its association with a signature, credential
72/// or timestamp. Nor does it establish revocation, purpose, historical trust or
73/// application algorithm permission. The root operation must establish those
74/// separate facts against the actual selected source.
75#[derive(Debug)]
76pub struct CheckedPath<'a> {
77    anchor: Anchor<'a>,
78    interval: Interval,
79    members: OwnedVec<Member<'a>>,
80    facts: ValidationFacts<'a>,
81}
82
83impl<'a> CheckedPath<'a> {
84    /// Caller-selected anchor, outside the prospective certification path.
85    /// An anchor certificate, when supplied, is available for material census
86    /// but never becomes a non-anchor revocation obligation.
87    #[must_use]
88    pub fn anchor(&self) -> Anchor<'a> {
89        self.anchor
90    }
91
92    /// Exact lossless interval checked by the shared path processor.
93    #[must_use]
94    pub fn interval(&self) -> Interval {
95        self.interval
96    }
97
98    /// Original non-anchor members in issuer-to-subject order. The final entry
99    /// is the supplied target. Each needs its own status-processing outcome;
100    /// the path's existence alone never supplies a Good assertion.
101    #[must_use]
102    pub fn members(&self) -> &[Member<'a>] {
103        &self.members
104    }
105
106    /// Accepted static policies/effective target key from the same invocation.
107    #[must_use]
108    pub fn facts(&self) -> &ValidationFacts<'a> {
109        &self.facts
110    }
111
112    /// Binds this actual path to the signer/key used by the complete token check.
113    /// The checked path interval must contain the entire timestamp-generation
114    /// interval, not just its bare genTime. This is static TSA path binding;
115    /// unresolved revocation and application trust acceptance remain separate.
116    ///
117    /// # Panics
118    /// Only if the private construction invariant is violated: an accepted
119    /// common path always has its original nonempty target chain.
120    pub fn bind_timestamp<'view, 'certificate, N: Copy, D: Copy, G: Copy, I, W: Work>(
121        &'view self,
122        actor: &'view CheckedActor<'certificate, 'a, N, D, G, I>,
123        utc: &LeapTable<'_>,
124        execution: &W,
125    ) -> TimestampBindingResult<'view, 'certificate, 'a, N, D, G, I> {
126        execution
127            .charge_work(1)
128            .map_err(TimestampBindingError::Resource)?;
129        // Construction follows a successful common path with a nonempty chain.
130        let target = self.members.last().expect("a checked path has a target");
131        if !exact(
132            target.certificate.as_bytes(),
133            actor.certificate().as_bytes(),
134            execution,
135        )
136        .map_err(TimestampBindingError::Resource)?
137        {
138            return Ok(TimestampBinding::Rejected(
139                TimestampBindingFailure::Certificate,
140            ));
141        }
142        // The common token checker already bound the original SPKI. Inherited
143        // parameter state must additionally equal the state actually projected
144        // by this validated path; never replace it after checking a signature.
145        let key = match (self.facts.key.parameters(), actor.key().parameters()) {
146            (None, None) => true,
147            (Some(left), Some(right)) => {
148                exact(left, right, execution).map_err(TimestampBindingError::Resource)?
149            }
150            _ => false,
151        };
152        if !key {
153            return Ok(TimestampBinding::Rejected(
154                TimestampBindingFailure::EffectiveKey,
155            ));
156        }
157        let generation = actor
158            .verified()
159            .interval(utc, execution)
160            .map_err(TimestampBindingError::Temporal)?;
161        if self.interval.from_instant() > generation.earliest
162            || self.interval.to_instant() < generation.latest
163        {
164            return Ok(TimestampBinding::Rejected(
165                TimestampBindingFailure::Interval,
166            ));
167        }
168        Ok(TimestampBinding::Bound(TimestampPath {
169            path: self,
170            actor,
171            generation,
172        }))
173    }
174}
175
176fn exact(left: &[u8], right: &[u8], execution: &impl Work) -> Result<bool, MemoryError> {
177    execution.charge_work(1)?;
178    if core::ptr::eq(left, right) {
179        return Ok(true);
180    }
181    if left.len() != right.len() {
182        return Ok(false);
183    }
184    let work = u64::try_from(left.len())
185        .ok()
186        .and_then(|n| n.checked_mul(2))
187        .ok_or(MemoryError::WorkExceeded)?;
188    execution.charge_work(work)?;
189    Ok(left == right)
190}
191
192/// Completed mismatch between an actual token actor and a checked static path.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum TimestampBindingFailure {
195    /// Target is a different original certificate, even if a DN/key hint agrees.
196    Certificate,
197    /// Signature was checked under a different inherited parameter state.
198    EffectiveKey,
199    /// The path was not checked over the complete generation interval.
200    Interval,
201}
202
203/// Inability to bind, separately from a completed source/key/interval mismatch.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum TimestampBindingError {
206    /// Current work refusal before source comparison or metadata traversal.
207    Resource(MemoryError),
208    /// Existing accuracy/UTC kernel could not establish the generation interval.
209    Temporal(TemporalError),
210}
211
212/// Privately bound mathematical timestamp and static path for its actual signer.
213/// Revocation, required material and application trust still need completion.
214pub struct TimestampPath<'view, 'certificate, 'document, N, D, G, I> {
215    path: &'view CheckedPath<'document>,
216    actor: &'view CheckedActor<'certificate, 'document, N, D, G, I>,
217    generation: GenerationInterval,
218}
219
220/// Binding of a checked path to a token actor, or the inability to bind.
221pub type TimestampBindingResult<'view, 'certificate, 'document, N, D, G, I> =
222    Result<TimestampBinding<'view, 'certificate, 'document, N, D, G, I>, TimestampBindingError>;
223
224impl<'view, 'certificate, 'document, N, D, G, I>
225    TimestampPath<'view, 'certificate, 'document, N, D, G, I>
226{
227    /// Same static path that was bound to this token's actual selected signer.
228    #[must_use]
229    pub fn path(&self) -> &'view CheckedPath<'document> {
230        self.path
231    }
232    /// Same complete mathematical token/input receipt and selected key.
233    #[must_use]
234    pub fn actor(&self) -> &'view CheckedActor<'certificate, 'document, N, D, G, I> {
235        self.actor
236    }
237    /// Complete generation interval covered by this static path's check.
238    #[must_use]
239    pub fn generation(&self) -> GenerationInterval {
240        self.generation
241    }
242}
243
244/// Completed binding or explicit mismatch; errors never become a false match.
245pub enum TimestampBinding<'view, 'certificate, 'document, N, D, G, I> {
246    /// Same original signer and parameters, with full generation-time coverage.
247    Bound(TimestampPath<'view, 'certificate, 'document, N, D, G, I>),
248    /// Completed source/key/interval mismatch.
249    Rejected(TimestampBindingFailure),
250}
251
252/// Completed static processing, without converting incomplete work to rejection.
253#[derive(Debug)]
254#[expect(
255    clippy::large_enum_variant,
256    reason = "return the checked path and its existing accounted storage without a second heap allocation for enum layout"
257)]
258pub enum PathOutcome<'a> {
259    /// Original chain passed the common static checks.
260    Accepted(CheckedPath<'a>),
261    /// Common processor completed with its original semantic rejection.
262    Rejected(Rejection<'a>),
263    /// Explicit local path bounds or deadline stopped processing.
264    Incomplete(Truncation),
265}
266
267/// Checks an original leaf-first chain and records precisely its member keys.
268///
269/// The caller selects the trusted anchor, interval, policy and backend through
270/// the shared context. Original certificate bytes are neither copied nor
271/// reparsed. The common RFC5280/RFC9618 validator runs once; effective-key
272/// projection then reuses its ordinary transition kernel, without verifying a
273/// signature again. All metadata growth and transitions retain current Work.
274///
275/// EN319132-1 V1.3.1 §§5.4.2–5.4.5/6.3(aa) make incorporated material depend
276/// on the actual validated actors and paths, not the number of carried values.
277/// The resulting path supplies original inputs for that census, not a B level.
278/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
279pub fn check_path<'a, V: Verify<E>, T: Watch, R: Recognizer<E>, E: Work>(
280    chain: &[&'a Certificate<'a>],
281    anchor: Anchor<'a>,
282    context: &ValidationContext<'a, '_, V, T, R, E>,
283) -> BuildResult<PathOutcome<'a>, V::Error, R::Error> {
284    let facts = match path::validate(chain, anchor, context)? {
285        Validation::Accepted(facts) => facts,
286        Validation::Rejected(reason) => return Ok(PathOutcome::Rejected(reason)),
287        Validation::Incomplete(reason) => return Ok(PathOutcome::Incomplete(reason)),
288    };
289    let execution = context.execution;
290    let mut members = execution.vector();
291    let fail = |reason| {
292        structured_pkix::Error::resource(reason, "ltv.validationPath", chain[0].span().start)
293    };
294    members.try_reserve_exact(chain.len()).map_err(fail)?;
295    let mut key = WorkingKey::standing(anchor.key());
296    for certificate in chain.iter().rev() {
297        key = key.subject(certificate, execution)?;
298        members
299            .try_push(Member { certificate, key })
300            .map_err(fail)?;
301    }
302    Ok(PathOutcome::Accepted(CheckedPath {
303        anchor,
304        interval: context.interval,
305        members: members.into_owned(),
306        facts,
307    }))
308}