Skip to main content

structured_ltv/pki/
closure.rs

1//! Iterative closure of the actual validation authorities used by path statuses.
2
3use super::{CheckedPath, CrlAuthorities, CrlAuthority, MemberStatus, PathStatuses, ResponderPath};
4use core::hash::{Hash, Hasher};
5use structured_asn1::memory::{HashWork, MemoryError, OwnedMap, OwnedVec, Work};
6use structured_pkix::{ocsp::AuthoritySource, temporal::Reading};
7
8/// Supplied original status census and the actual delegated-responder bindings
9/// selected for its OCSP inputs. Construction is not evidence of completeness;
10/// `close_authorities` derives and checks every reachable dependency.
11pub struct AuthorityNode<'view, 'certificate, 'document> {
12    /// Same actual path, exact evaluation label and complete per-member inputs.
13    pub statuses: &'view PathStatuses<'view, 'certificate, 'document>,
14    /// Original-root bindings for delegated OCSP inputs, without source copies.
15    pub responders: &'view [&'view ResponderPath<'view, 'view, 'certificate, 'document>],
16}
17
18/// Original obligation that required an additional authority path.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AuthorityOrigin {
21    /// Ordinal in the explicitly selected root-status inputs.
22    Root {
23        /// Original root ordinal.
24        index: usize,
25    },
26    /// One complete/delta contributor to an actual member's CRL reason union.
27    Crl {
28        /// Ordinal in the supplied authority-node catalogue.
29        node: usize,
30        /// Original non-anchor member ordinal.
31        member: usize,
32        /// Original reason contributor ordinal.
33        source: usize,
34        /// Whether this is the updating delta rather than the complete list.
35        delta: bool,
36    },
37    /// Actual directly delegated OCSP signer for this original member.
38    Responder {
39        /// Ordinal in the supplied authority-node catalogue.
40        node: usize,
41        /// Original non-anchor member ordinal.
42        member: usize,
43    },
44}
45
46/// Caller input or resource refusal, not a certificate/status rejection.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum AuthorityClosureError {
49    /// No original actor path was selected.
50    Roots,
51    /// Two nodes claim the same live path receipt and exact evaluation label.
52    DuplicateNode {
53        /// Second supplied node ordinal.
54        index: usize,
55    },
56    /// More than one responder binding was selected for the same OCSP target.
57    DuplicateResponder {
58        /// Supplied node containing the conflicting selection.
59        node: usize,
60    },
61    /// A required original path/label has no complete status census.
62    MissingPath {
63        /// Exact original dependency, not an invented byte offset.
64        origin: AuthorityOrigin,
65    },
66    /// A root was replaced by a different status-census receipt.
67    ForeignRoot {
68        /// Original root ordinal.
69        index: usize,
70    },
71    /// The original delegated response has no selected full-path binding.
72    MissingResponder {
73        /// Actual source member requiring the binding.
74        origin: AuthorityOrigin,
75    },
76    /// Current execution refused before traversal, hashing, growth or copying.
77    Resource(MemoryError),
78}
79
80impl From<MemoryError> for AuthorityClosureError {
81    fn from(value: MemoryError) -> Self {
82        Self::Resource(value)
83    }
84}
85
86/// Complete reachable authority-input set, not a baseline level or Good verdict.
87/// Every borrowed node retains the original status, freshness, nonce, configured
88/// trust and noCheck facts. Only reached nodes are retained; temporary indexes
89/// are released before return. No provider/parse/signature callback is replayed.
90pub struct AuthorityClosure<'view, 'certificate, 'document> {
91    roots: &'view [&'view PathStatuses<'view, 'certificate, 'document>],
92    nodes: OwnedVec<&'view AuthorityNode<'view, 'certificate, 'document>>,
93}
94
95impl<'view, 'certificate, 'document> AuthorityClosure<'view, 'certificate, 'document> {
96    /// Actual selected actor inputs; actor-to-signature association is separate.
97    #[must_use]
98    pub fn roots(&self) -> &'view [&'view PathStatuses<'view, 'certificate, 'document>] {
99        self.roots
100    }
101    /// All reached original paths, each once per exact evaluation label.
102    #[must_use]
103    pub fn nodes(&self) -> &[&'view AuthorityNode<'view, 'certificate, 'document>] {
104        &self.nodes
105    }
106}
107
108#[derive(Clone, Copy)]
109pub(super) struct Key<'view, 'document> {
110    pub(super) path: &'view CheckedPath<'document>,
111    pub(super) at: Reading,
112}
113
114impl PartialEq for Key<'_, '_> {
115    fn eq(&self, other: &Self) -> bool {
116        core::ptr::eq(self.path, other.path) && self.at == other.at
117    }
118}
119impl Eq for Key<'_, '_> {}
120impl Hash for Key<'_, '_> {
121    fn hash<H: Hasher>(&self, state: &mut H) {
122        // Only live, immovably borrowed, nonzero-sized receipts are indexed.
123        // This is operation-local receipt identity, never document identity.
124        core::ptr::from_ref(self.path).hash(state);
125        self.at.start().seconds().hash(state);
126        self.at.start().nanos().hash(state);
127        self.at.start().is_leap_second().hash(state);
128        self.at.has_subnanosecond_remainder().hash(state);
129    }
130}
131impl HashWork for Key<'_, '_> {
132    fn hash_work(&self) -> Result<u64, MemoryError> {
133        Ok(6)
134    }
135}
136struct State {
137    index: usize,
138    queued: bool,
139}
140
141struct Pending<'view, 'certificate, 'document, W: Work> {
142    supplied: &'view [AuthorityNode<'view, 'certificate, 'document>],
143    index: OwnedMap<Key<'view, 'document>, State, W>,
144    queue: OwnedVec<&'view AuthorityNode<'view, 'certificate, 'document>, W>,
145}
146
147impl<'view, 'document, W: Work> Pending<'view, '_, 'document, W> {
148    fn need(
149        &mut self,
150        path: &'view CheckedPath<'document>,
151        at: Reading,
152        origin: AuthorityOrigin,
153        work: &W,
154    ) -> Result<usize, AuthorityClosureError> {
155        let Some(state) = self.index.get_mut(&Key { path, at }, work)? else {
156            return Err(AuthorityClosureError::MissingPath { origin });
157        };
158        work.charge_work(1)?;
159        if !state.queued {
160            self.queue.try_push(&self.supplied[state.index])?;
161            state.queued = true;
162        }
163        Ok(state.index)
164    }
165}
166
167/// Derive all actual status-authority paths from selected original actor inputs.
168/// EN319132-1 V1.3.1 ยง6.3(aa) requires the validation data actually needed for
169/// the actors, including status authorities, rather than an arbitrary nonempty
170/// collection. Every complete/delta authority is followed. Delegated responses
171/// need their actual full-path receipt; direct issuer/configured branches remain
172/// explicit in the original input. A valid noCheck exempts only its responder's
173/// status, as already enforced by the shared status census.
174///
175/// The queue is iterative and deduplicates exact live path/Reading pairs;
176/// repeated dependencies and cycles neither recurse nor replay verification.
177/// Missing inputs and resource refusal drop all partial output and scratch.
178/// This proves input closure, not status usability, application trust, or a
179/// complete signer/counter/credential/timestamp actor census.
180/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
181///
182/// # Panics
183/// Only if the private queue/index invariant is violated. Entries are queued
184/// exclusively after insertion and the index is never removed during traversal.
185pub fn close_authorities<'view, 'certificate, 'document, W: Work>(
186    roots: &'view [&'view PathStatuses<'view, 'certificate, 'document>],
187    supplied: &'view [AuthorityNode<'view, 'certificate, 'document>],
188    work: &W,
189) -> Result<AuthorityClosure<'view, 'certificate, 'document>, AuthorityClosureError> {
190    work.charge_work(1)?;
191    if roots.is_empty() {
192        return Err(AuthorityClosureError::Roots);
193    }
194    let mut pending = Pending {
195        supplied,
196        index: work.map(),
197        queue: work.vector(),
198    };
199    let mut responders = work.map();
200    for (index, node) in supplied.iter().enumerate() {
201        work.charge_work(1)?;
202        let key = Key {
203            path: node.statuses.path(),
204            at: node.statuses.evaluated_at(),
205        };
206        if pending
207            .index
208            .try_insert(
209                key,
210                State {
211                    index,
212                    queued: false,
213                },
214            )?
215            .is_some()
216        {
217            return Err(AuthorityClosureError::DuplicateNode { index });
218        }
219        for responder in node.responders {
220            work.charge_work(1)?;
221            let key = core::ptr::from_ref(responder.target()).addr();
222            if responders.try_insert(key, *responder)?.is_some() {
223                return Err(AuthorityClosureError::DuplicateResponder { node: index });
224            }
225        }
226    }
227    for (index, root) in roots.iter().enumerate() {
228        work.charge_work(1)?;
229        let node = pending.need(
230            root.path(),
231            root.evaluated_at(),
232            AuthorityOrigin::Root { index },
233            work,
234        )?;
235        if !core::ptr::eq(supplied[node].statuses, *root) {
236            return Err(AuthorityClosureError::ForeignRoot { index });
237        }
238    }
239    let mut cursor = 0;
240    while cursor < pending.queue.len() {
241        work.charge_work(1)?;
242        let inputs = pending.queue[cursor].statuses;
243        let key = Key {
244            path: inputs.path(),
245            at: inputs.evaluated_at(),
246        };
247        let index = pending
248            .index
249            .get(&key, work)?
250            .expect("queued node has its index")
251            .index;
252        follow(
253            inputs,
254            index,
255            &responders,
256            &mut |path, at, origin, work| pending.need(path, at, origin, work).map(|_| ()),
257            work,
258        )?;
259        cursor += 1;
260    }
261    Ok(AuthorityClosure {
262        roots,
263        nodes: pending.queue.into_owned(),
264    })
265}
266
267pub(super) fn follow<'view, 'certificate, 'document, W: Work>(
268    inputs: &'view PathStatuses<'view, 'certificate, 'document>,
269    index: usize,
270    responders: &OwnedMap<usize, &'view ResponderPath<'view, 'view, 'certificate, 'document>, W>,
271    need: &mut impl FnMut(
272        &'view CheckedPath<'document>,
273        Reading,
274        AuthorityOrigin,
275        &W,
276    ) -> Result<(), AuthorityClosureError>,
277    work: &W,
278) -> Result<(), AuthorityClosureError> {
279    for (member, input) in inputs.entries().iter().enumerate() {
280        work.charge_work(1)?;
281        match input {
282            MemberStatus::Crl(bound) => {
283                for (source, authority) in bound.authorities().iter().enumerate() {
284                    work.charge_work(1)?;
285                    follow_crl(
286                        (index, member, source),
287                        *authority,
288                        inputs.evaluated_at(),
289                        need,
290                        work,
291                    )?;
292                }
293            }
294            MemberStatus::CrlReading(bound) => follow_crl(
295                (index, member, 0),
296                bound.authorities(),
297                inputs.evaluated_at(),
298                need,
299                work,
300            )?,
301            MemberStatus::Ocsp(target) => {
302                if matches!(target.authority(), AuthoritySource::Delegate { .. }) {
303                    let origin = AuthorityOrigin::Responder {
304                        node: index,
305                        member,
306                    };
307                    let key = core::ptr::from_ref(*target).addr();
308                    let responder = responders
309                        .get(&key, work)?
310                        .ok_or(AuthorityClosureError::MissingResponder { origin })?;
311                    need(responder.path(), inputs.evaluated_at(), origin, work)?;
312                }
313            }
314            MemberStatus::ResponderNoCheck(_) => (),
315        }
316    }
317    Ok(())
318}
319
320fn follow_crl<'view, 'document, W: Work>(
321    position: (usize, usize, usize),
322    authorities: CrlAuthorities<'view, 'document>,
323    at: Reading,
324    visit: &mut impl FnMut(
325        &'view CheckedPath<'document>,
326        Reading,
327        AuthorityOrigin,
328        &W,
329    ) -> Result<(), AuthorityClosureError>,
330    work: &W,
331) -> Result<(), AuthorityClosureError> {
332    let (node, member, source) = position;
333    let mut need = |authority, delta| {
334        if let CrlAuthority::Path(path) = authority {
335            visit(
336                path,
337                at,
338                AuthorityOrigin::Crl {
339                    node,
340                    member,
341                    source,
342                    delta,
343                },
344                work,
345            )?;
346        }
347        Ok::<_, AuthorityClosureError>(())
348    };
349    match authorities {
350        CrlAuthorities::Complete(authority) => need(authority, false),
351        CrlAuthorities::Delta { base, delta } => {
352            need(base, false)?;
353            need(delta, true)
354        }
355    }
356}