Skip to main content

structured_cades/
credential.rs

1//! Complete source-derived independently authenticated credential census.
2
3use crate::{
4    Error,
5    basic::SignerBinding,
6    counters::Paths,
7    prepare::Preparation,
8    properties::{OpenRole, OpenValue, Properties},
9    source::{DocumentId, Node, Original},
10};
11use structured_asn1::{
12    Reader,
13    memory::{OwnedVec, Work},
14};
15use structured_ltv::source::Snapshot;
16use structured_ltv::{
17    credential::Authentication,
18    pki::{self, Authorities, CounterReceipts, CredentialGroup, CredentialReceipts},
19};
20use structured_pkix::Certificate;
21
22/// Actual protected credential delivered to its selected format owner. The
23/// private source/owner comes from the complete original property reading.
24pub struct Context<'scope, 'model, 'document> {
25    source: Original<'model, 'document>,
26    signature: Node,
27    value: OpenValue<'document>,
28    signer: &'scope Certificate<'document>,
29    operation: &'scope Reader<'document>,
30}
31impl<'m, 'd> Context<'_, 'm, 'd> {
32    /// Original protected credential source and immutable document identity.
33    #[must_use]
34    pub fn source(&self) -> Original<'m, 'd> {
35        self.source
36    }
37    /// Actual root/counter signature protecting this value.
38    #[must_use]
39    pub fn signature(&self) -> Node {
40        self.signature
41    }
42    /// Actual independently bound signing certificate, not an arbitrary hint.
43    #[must_use]
44    pub fn signer(&self) -> &Certificate<'d> {
45        self.signer
46    }
47    /// Original open type, explicit OPTIONAL absence, and source context.
48    #[must_use]
49    pub fn value(&self) -> OpenValue<'d> {
50        self.value
51    }
52    /// Current operation; use the value's saved reader context for ASN.1
53    /// inspection, preserving original source/depth and both work ceilings.
54    #[must_use]
55    pub fn operation(&self) -> &Reader<'d> {
56        self.operation
57    }
58}
59
60/// Actual credential-format authentication, without a parallel crypto engine.
61///
62/// EN319122-1 ยง5.2.6.1 leaves non-X.509 certificate/assertion formats to their
63/// own specifications. Authenticate THIS original value and its binding to the
64/// actual signer, returning the concrete format evidence and every actual
65/// authority path/direct anchor or explicit certificate-free mechanism used.
66/// Admit parsing, arithmetic, copies, output and retained evidence through the
67/// current execution before work; preserve original resource/processing causes.
68/// No network/file acquisition or asserted-role authorization occurs here.
69/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf>
70pub trait Provider<'path, 'document> {
71    /// Actual retained format evidence, without Clone/Send/Sync bounds.
72    type Evidence;
73    /// Original provider/source/resource refusal, distinct from rejection.
74    type Error;
75    /// Authenticate one original independently signed credential exactly once.
76    fn authenticate(
77        &self,
78        context: Context<'_, '_, 'document>,
79    ) -> Result<Authentication<'path, 'document, Self::Evidence>, Self::Error>;
80}
81
82/// Completed credential answer retained even when no complete census exists.
83pub enum Outcome<'path, 'document, E> {
84    /// Concrete original authentication outcome (including negative answers).
85    Authentication(Authentication<'path, 'document, E>),
86    /// The open schema carries no payload; this cannot become authenticated.
87    Absent,
88    /// Empty Paths is incomplete handler evidence, not certificate-free.
89    MissingAuthorities(E),
90}
91/// One original credential and its completed original-format answer.
92pub struct Checked<'model, 'document, 'path, E> {
93    source: Original<'model, 'document>,
94    outcome: Outcome<'path, 'document, E>,
95}
96impl<'m, 'd, 'p, E> Checked<'m, 'd, 'p, E> {
97    /// Exact original credential enclosure and protecting document.
98    #[must_use]
99    pub fn source(&self) -> Original<'m, 'd> {
100        self.source
101    }
102    /// Actual format evidence or explicit negative outcome, without copying.
103    #[must_use]
104    pub fn outcome(&self) -> &Outcome<'p, 'd, E> {
105        &self.outcome
106    }
107}
108
109/// Complete original group, exposed as an LTV group only through [`Complete`].
110pub struct Group<'model, 'document, 'path, E> {
111    signature: Node,
112    entries: OwnedVec<Checked<'model, 'document, 'path, E>>,
113}
114impl<'d, 'p, E> CredentialGroup<'p, 'd> for Group<'_, 'd, 'p, E> {
115    type Id = Node;
116    fn signature(&self) -> Node {
117        self.signature
118    }
119    fn len(&self) -> usize {
120        self.entries.len()
121    }
122    fn credential(&self, ordinal: usize) -> pki::Credential<'p, 'd, Node> {
123        let entry = &self.entries[ordinal];
124        let Outcome::Authentication(Authentication::Authenticated { authorities, .. }) =
125            &entry.outcome
126        else {
127            unreachable!("only complete groups are exposed as credential receipts");
128        };
129        pki::Credential {
130            signature: self.signature,
131            node: entry.source.node,
132            authorities: *authorities,
133        }
134    }
135}
136
137/// Complete original root/counter results. A negative later credential does
138/// not disappear behind the successful prefix, and no complete receipt exists.
139pub struct Report<'model, 'document, 'path, E> {
140    signature: Original<'model, 'document>,
141    root: Group<'model, 'document, 'path, E>,
142    counters: OwnedVec<Group<'model, 'document, 'path, E>>,
143    complete: bool,
144}
145/// One original borrowed counter group, without copying retained evidence.
146pub type CounterGroup<'view, 'm, 'd, 'p, E> = (Node, &'view [Checked<'m, 'd, 'p, E>]);
147impl<'m, 'd, 'p, E> Report<'m, 'd, 'p, E> {
148    /// Every original root credential, including absent/rejected/unsupported.
149    #[must_use]
150    pub fn root(&self) -> &[Checked<'m, 'd, 'p, E>] {
151        &self.root.entries
152    }
153    /// Number of counter groups with original credentials.
154    #[must_use]
155    pub fn counters(&self) -> usize {
156        self.counters.len()
157    }
158    /// One original counter group after current metadata admission. Absence is
159    /// distinct from refusal; the caller cannot traverse attacker-sized groups
160    /// through an infallible iterator that bypasses current Work.
161    pub fn counter<W: Work>(
162        &self,
163        ordinal: usize,
164        work: &W,
165    ) -> Result<Option<CounterGroup<'_, 'm, 'd, 'p, E>>, structured_asn1::memory::MemoryError> {
166        work.charge_work(1)?;
167        Ok(self
168            .counters
169            .get(ordinal)
170            .map(|group| (group.signature, &*group.entries)))
171    }
172    /// Complete actual authority-input census, only after ALL original values
173    /// authenticated with nonempty/explicit authority facts.
174    #[must_use]
175    pub fn complete(&self) -> Option<Complete<'_, 'm, 'd, 'p, E>> {
176        if self.complete {
177            Some(Complete { report: self })
178        } else {
179            None
180        }
181    }
182}
183
184/// Borrowed full credential proof. Its construction cannot select a successful
185/// subset or replace the original protecting signature.
186pub struct Complete<'receipt, 'model, 'document, 'path, E> {
187    pub(crate) report: &'receipt Report<'model, 'document, 'path, E>,
188}
189impl<'m, 'd, 'p, E> CredentialReceipts<'p, 'd> for Complete<'_, 'm, 'd, 'p, E> {
190    type Id = Node;
191    type Document = DocumentId<'m, 'd>;
192    type Generation = ();
193    type Group = Group<'m, 'd, 'p, E>;
194    fn snapshot(&self) -> Snapshot<DocumentId<'m, 'd>, ()> {
195        self.report.signature.snapshot
196    }
197    fn signature(&self) -> Node {
198        self.report.signature.node
199    }
200    fn root(&self) -> Option<&Self::Group> {
201        if self.report.root.entries.is_empty() {
202            None
203        } else {
204            Some(&self.report.root)
205        }
206    }
207    fn counters(&self) -> usize {
208        self.report.counters.len()
209    }
210    fn counter(&self, ordinal: usize) -> (Node, &Self::Group) {
211        let group = &self.report.counters[ordinal];
212        (group.signature, group)
213    }
214}
215
216/// Processing refuses without returning a successful partial authentication.
217#[derive(Debug)]
218pub enum CheckError<E> {
219    /// Source, original association or capacity/work refused before use.
220    Processing(Error),
221    /// Actual root/counter has no bound signing certificate for this operation.
222    Unbound {
223        /// Original protecting signer.
224        signature: Node,
225    },
226    /// Original format/schema/placement findings prevent a complete protected
227    /// credential census. The preparation retains every original cause; no
228    /// format handler has been called and no successful empty set is issued.
229    Nonconforming {
230        /// Original selected root.
231        signature: Node,
232    },
233    /// Original opaque format refusal and its physical enclosing credential.
234    Provider {
235        /// Original protected credential.
236        node: Node,
237        /// Unchanged provider/resource error.
238        cause: E,
239    },
240}
241
242/// Authenticate every actual root/counter non-claimed credential using the
243/// existing format-owner contract. All original selections are checked before
244/// authentication begins; a caller cannot supply only successful groups.
245pub fn check<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
246    prepared: &Preparation<'m, 'd>,
247    signer: &SignerBinding<'_, 'm, 'd, 'p>,
248    counters: &Paths<'_, 'm, 'd, 'p>,
249    provider: &P,
250    operation: &Reader<'d>,
251) -> Result<Report<'m, 'd, 'p, E>, CheckError<P::Error>> {
252    let processing = CheckError::Processing;
253    prepared
254        .document()
255        .message()
256        .require_session(operation, "cades.credentials")
257        .map_err(|cause| processing(Error::Cms(cause)))?;
258    let selected = prepared.document().locate(prepared.signer().span());
259    operation
260        .charge(1)
261        .map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
262    if !core::ptr::eq(prepared, signer.prepared())
263        || selected.snapshot != counters.snapshot()
264        || selected.node != counters.signature()
265        || counters.len() != prepared.counters().len()
266    {
267        return Err(processing(Error::Foreign {
268            at: selected.node.span().start,
269        }));
270    }
271    if !signer.is_bound() {
272        return Err(CheckError::Unbound {
273            signature: selected.node,
274        });
275    }
276    // Both value syntax and signed placement were established by the original
277    // preparation. A partial open-value prefix or an unsigned role value must
278    // never be promoted to a complete PROTECTED credential census.
279    if !prepared.basic_format_requirements_met() {
280        return Err(CheckError::Nonconforming {
281            signature: selected.node,
282        });
283    }
284    // Finish whole-source/counter membership preflight BEFORE the first handler.
285    for (ordinal, expected) in prepared.counters().iter().enumerate() {
286        operation
287            .charge(1)
288            .map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
289        if counters.counter(ordinal).signature != expected.node() {
290            return Err(processing(Error::Foreign {
291                at: expected.node().span().start,
292            }));
293        }
294    }
295    let mut complete = true;
296    let root = group(
297        prepared,
298        selected.node,
299        prepared.properties(),
300        signer.certificate(),
301        provider,
302        operation,
303        &mut complete,
304    )?;
305    let work = operation.execution();
306    let mut groups = work.vector();
307    for (ordinal, counter) in prepared.counters().iter().enumerate() {
308        operation
309            .charge(1)
310            .map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
311        let Some(properties) = counter.properties() else {
312            return Err(CheckError::Unbound {
313                signature: counter.node(),
314            });
315        };
316        let path = counters.counter(ordinal).path;
317        let target = path.members().last().ok_or(CheckError::Unbound {
318            signature: counter.node(),
319        })?;
320        let group = group(
321            prepared,
322            counter.node(),
323            properties,
324            target.certificate(),
325            provider,
326            operation,
327            &mut complete,
328        )?;
329        if !group.entries.is_empty() {
330            groups.try_push(group).map_err(|cause| {
331                processing(Error::resource(
332                    cause,
333                    "cades.credentials",
334                    counter.node().span().start,
335                ))
336            })?;
337        }
338    }
339    Ok(Report {
340        signature: selected,
341        root,
342        counters: groups.into_owned(),
343        complete,
344    })
345}
346
347fn group<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
348    prepared: &Preparation<'m, 'd>,
349    signature: Node,
350    properties: &Properties<'d>,
351    signer: &Certificate<'d>,
352    provider: &P,
353    operation: &Reader<'d>,
354    complete: &mut bool,
355) -> Result<Group<'m, 'd, 'p, E>, CheckError<P::Error>> {
356    let work = operation.execution();
357    let mut entries = work.vector();
358    for value in properties.open_values() {
359        work.charge_work(1).map_err(|cause| {
360            CheckError::Processing(Error::resource(
361                cause,
362                "cades.credentials",
363                signature.span().start,
364            ))
365        })?;
366        if !matches!(
367            value.role,
368            OpenRole::AttributeCertificate
369                | OpenRole::OtherAttributeCertificate
370                | OpenRole::SignedAssertion
371        ) {
372            continue;
373        }
374        let source = prepared.document().locate(value.enclosure.span());
375        let outcome = if value.value.is_none() {
376            *complete = false;
377            Outcome::Absent
378        } else {
379            match provider
380                .authenticate(Context {
381                    source,
382                    signature,
383                    value: *value,
384                    signer,
385                    operation,
386                })
387                .map_err(|cause| CheckError::Provider {
388                    node: source.node,
389                    cause,
390                })? {
391                Authentication::Authenticated {
392                    evidence,
393                    authorities: Authorities::Paths([]),
394                } => {
395                    *complete = false;
396                    Outcome::MissingAuthorities(evidence)
397                }
398                Authentication::Unsupported => {
399                    *complete = false;
400                    Outcome::Authentication(Authentication::Unsupported)
401                }
402                Authentication::Rejected => {
403                    *complete = false;
404                    Outcome::Authentication(Authentication::Rejected)
405                }
406                authenticated @ Authentication::Authenticated { .. } => {
407                    Outcome::Authentication(authenticated)
408                }
409            }
410        };
411        entries
412            .try_push(Checked { source, outcome })
413            .map_err(|cause| {
414                CheckError::Processing(Error::resource(
415                    cause,
416                    "cades.credentials",
417                    source.node.span().start,
418                ))
419            })?;
420    }
421    Ok(Group {
422        signature,
423        entries: entries.into_owned(),
424    })
425}