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, ScopedReader,
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 ScopedReader<'document, 'scope>,
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) -> &ScopedReader<'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<'m, 'd, 'p, E> Group<'m, 'd, 'p, E> {
115    /// Actual retained authentication results in their original source order.
116    #[must_use]
117    pub fn entries(&self) -> &[Checked<'m, 'd, 'p, E>] {
118        &self.entries
119    }
120}
121impl<'d, 'p, E> CredentialGroup<'p, 'd> for Group<'_, 'd, 'p, E> {
122    type Id = Node;
123    fn signature(&self) -> Node {
124        self.signature
125    }
126    fn len(&self) -> usize {
127        self.entries.len()
128    }
129    fn credential(&self, ordinal: usize) -> pki::Credential<'p, 'd, Node> {
130        let entry = &self.entries[ordinal];
131        let Outcome::Authentication(Authentication::Authenticated { authorities, .. }) =
132            &entry.outcome
133        else {
134            unreachable!("only complete groups are exposed as credential receipts");
135        };
136        pki::Credential {
137            signature: self.signature,
138            node: entry.source.node,
139            authorities: *authorities,
140        }
141    }
142}
143
144/// Complete original root/counter results. A negative later credential does
145/// not disappear behind the successful prefix, and no complete receipt exists.
146pub struct Report<'model, 'document, 'path, E> {
147    signature: Original<'model, 'document>,
148    root: Group<'model, 'document, 'path, E>,
149    counters: OwnedVec<Group<'model, 'document, 'path, E>>,
150    complete: bool,
151}
152/// One original borrowed counter group, without copying retained evidence.
153pub type CounterGroup<'view, 'm, 'd, 'p, E> = (Node, &'view [Checked<'m, 'd, 'p, E>]);
154impl<'m, 'd, 'p, E> Report<'m, 'd, 'p, E> {
155    /// Every original root credential, including absent/rejected/unsupported.
156    #[must_use]
157    pub fn root(&self) -> &[Checked<'m, 'd, 'p, E>] {
158        &self.root.entries
159    }
160    /// Number of counter groups with original credentials.
161    #[must_use]
162    pub fn counters(&self) -> usize {
163        self.counters.len()
164    }
165    /// One original counter group after current metadata admission. Absence is
166    /// distinct from refusal; the caller cannot traverse attacker-sized groups
167    /// through an infallible iterator that bypasses current Work.
168    pub fn counter<W: Work>(
169        &self,
170        ordinal: usize,
171        work: &W,
172    ) -> Result<Option<CounterGroup<'_, 'm, 'd, 'p, E>>, structured_asn1::memory::MemoryError> {
173        work.charge_work(1)?;
174        Ok(self
175            .counters
176            .get(ordinal)
177            .map(|group| (group.signature, &*group.entries)))
178    }
179    /// Complete actual authority-input census, only after ALL original values
180    /// authenticated with nonempty/explicit authority facts.
181    #[must_use]
182    pub fn complete(&self) -> Option<Complete<'_, 'm, 'd, 'p, E>> {
183        if self.complete {
184            Some(Complete { report: self })
185        } else {
186            None
187        }
188    }
189}
190
191/// Borrowed full credential proof. Its construction cannot select a successful
192/// subset or replace the original protecting signature.
193pub struct Complete<'receipt, 'model, 'document, 'path, E> {
194    pub(crate) report: &'receipt Report<'model, 'document, 'path, E>,
195}
196impl<'r, 'm, 'd, 'p, E> Complete<'r, 'm, 'd, 'p, E> {
197    /// Borrows the actual retained root group, without tying its lifetime to
198    /// a temporary complete-census descriptor or copying authentication.
199    #[must_use]
200    pub fn root_group(&self) -> Option<&'r Group<'m, 'd, 'p, E>> {
201        if self.report.root.entries.is_empty() {
202            None
203        } else {
204            Some(&self.report.root)
205        }
206    }
207    /// Original counter group after current metadata admission. Absence is
208    /// distinct from refusal; the group remains owned by the source report.
209    pub fn counter_group(
210        &self,
211        ordinal: usize,
212        work: &impl Work,
213    ) -> Result<Option<&'r Group<'m, 'd, 'p, E>>, structured_asn1::memory::MemoryError> {
214        work.charge_work(1)?;
215        Ok(self.report.counters.get(ordinal))
216    }
217}
218impl<'m, 'd, 'p, E> CredentialReceipts<'p, 'd> for Complete<'_, 'm, 'd, 'p, E> {
219    type Id = Node;
220    type Document = DocumentId<'m, 'd>;
221    type Generation = ();
222    type Group = Group<'m, 'd, 'p, E>;
223    fn snapshot(&self) -> Snapshot<DocumentId<'m, 'd>, ()> {
224        self.report.signature.snapshot
225    }
226    fn signature(&self) -> Node {
227        self.report.signature.node
228    }
229    fn root(&self) -> Option<&Self::Group> {
230        if self.report.root.entries.is_empty() {
231            None
232        } else {
233            Some(&self.report.root)
234        }
235    }
236    fn counters(&self) -> usize {
237        self.report.counters.len()
238    }
239    fn counter(&self, ordinal: usize) -> (Node, &Self::Group) {
240        let group = &self.report.counters[ordinal];
241        (group.signature, group)
242    }
243}
244
245/// Processing refuses without returning a successful partial authentication.
246#[derive(Debug)]
247pub enum CheckError<E> {
248    /// Source, original association or capacity/work refused before use.
249    Processing(Error),
250    /// Actual root/counter has no bound signing certificate for this operation.
251    Unbound {
252        /// Original protecting signer.
253        signature: Node,
254    },
255    /// Original format/schema/placement findings prevent a complete protected
256    /// credential census. The preparation retains every original cause; no
257    /// format handler has been called and no successful empty set is issued.
258    Nonconforming {
259        /// Original selected root.
260        signature: Node,
261    },
262    /// Original opaque format refusal and its physical enclosing credential.
263    Provider {
264        /// Original protected credential.
265        node: Node,
266        /// Unchanged provider/resource error.
267        cause: E,
268    },
269}
270
271/// Authenticate every actual root/counter non-claimed credential using the
272/// existing format-owner contract. All original selections are checked before
273/// authentication begins; a caller cannot supply only successful groups.
274pub fn check<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
275    prepared: &Preparation<'m, 'd>,
276    signer: &SignerBinding<'_, 'm, 'd, 'p>,
277    counters: &Paths<'_, 'm, 'd, 'p>,
278    provider: &P,
279    operation: &Reader<'d>,
280) -> Result<Report<'m, 'd, 'p, E>, CheckError<P::Error>> {
281    prepared
282        .document()
283        .message()
284        .require_session(operation, "cades.credentials")
285        .map_err(|cause| CheckError::Processing(crate::Error::Cms(cause)))?;
286    operation.charge(1).map_err(|cause| {
287        CheckError::Processing(crate::Error::encoding(cause, "cades.credentials"))
288    })?;
289    if !prepared.basic_format_requirements_met() {
290        return Err(CheckError::Nonconforming {
291            signature: prepared.document().locate(prepared.signer().span()).node,
292        });
293    }
294    check_properties(prepared, signer, counters, provider, operation)
295}
296
297/// Authenticates the complete protected root/counter property set without
298/// imposing `CAdES`-specific root services on a containing format. `PAdES` uses
299/// the same signed properties but forbids `CAdES`'s required signing-time.
300/// Actual CMS placement/cardinality, ESS, source/key binding and every counter
301/// are still checked before any handler. This does not assert `CAdES` B-B.
302/// EN319142-1 §6.3/Table1 and EN319122-1 §5.2.6 define the separate profiles.
303pub fn check_properties<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
304    prepared: &Preparation<'m, 'd>,
305    signer: &SignerBinding<'_, 'm, 'd, 'p>,
306    counters: &Paths<'_, 'm, 'd, 'p>,
307    provider: &P,
308    operation: &ScopedReader<'d, '_>,
309) -> Result<Report<'m, 'd, 'p, E>, CheckError<P::Error>> {
310    let processing = CheckError::Processing;
311    prepared
312        .document()
313        .message()
314        .require_session(operation, "cades.credentials")
315        .map_err(|cause| processing(Error::Cms(cause)))?;
316    let selected = prepared.document().locate(prepared.signer().span());
317    operation
318        .charge(1)
319        .map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
320    if !core::ptr::eq(prepared, signer.prepared())
321        || selected.snapshot != counters.snapshot()
322        || selected.node != counters.signature()
323        || counters.len() != prepared.counters().len()
324    {
325        return Err(processing(Error::Foreign {
326            at: selected.node.span().start,
327        }));
328    }
329    if !signer.is_bound() {
330        return Err(CheckError::Unbound {
331            signature: selected.node,
332        });
333    }
334    preflight_properties(prepared, counters, operation)?;
335    let mut complete = true;
336    let root = group(
337        prepared,
338        selected.node,
339        prepared.properties(),
340        signer.certificate(),
341        provider,
342        operation,
343        &mut complete,
344    )?;
345    let work = operation.execution();
346    let mut groups = work.vector();
347    for (ordinal, counter) in prepared.counters().iter().enumerate() {
348        operation
349            .charge(1)
350            .map_err(|cause| processing(Error::encoding(cause, "cades.credentials")))?;
351        let Some(properties) = counter.properties() else {
352            return Err(CheckError::Unbound {
353                signature: counter.node(),
354            });
355        };
356        let path = counters.counter(ordinal).path;
357        let target = path.members().last().ok_or(CheckError::Unbound {
358            signature: counter.node(),
359        })?;
360        let group = group(
361            prepared,
362            counter.node(),
363            properties,
364            target.certificate(),
365            provider,
366            operation,
367            &mut complete,
368        )?;
369        if !group.entries.is_empty() {
370            groups.try_push(group).map_err(|cause| {
371                processing(Error::resource(
372                    cause,
373                    "cades.credentials",
374                    counter.node().span().start,
375                ))
376            })?;
377        }
378    }
379    Ok(Report {
380        signature: selected,
381        root,
382        counters: groups.into_owned(),
383        complete,
384    })
385}
386
387fn preflight_properties<E>(
388    prepared: &Preparation<'_, '_>,
389    counters: &Paths<'_, '_, '_, '_>,
390    operation: &ScopedReader<'_, '_>,
391) -> Result<(), CheckError<E>> {
392    // A partial or misplaced open-value prefix cannot become a complete
393    // protected census. Finish every source check before the first handler.
394    if !prepared.signer().faults().is_empty()
395        || !prepared.profile().faults().is_empty()
396        || !prepared.profile().signing().faults().is_empty()
397        || !prepared.properties().faults().is_empty()
398    {
399        return Err(CheckError::Nonconforming {
400            signature: prepared.document().locate(prepared.signer().span()).node,
401        });
402    }
403    for (ordinal, expected) in prepared.counters().iter().enumerate() {
404        operation
405            .charge(1)
406            .map_err(|cause| CheckError::Processing(Error::encoding(cause, "cades.credentials")))?;
407        if counters.counter(ordinal).signature != expected.node() {
408            return Err(CheckError::Processing(Error::Foreign {
409                at: expected.node().span().start,
410            }));
411        }
412        if expected.malformed().is_some()
413            || expected
414                .properties()
415                .is_none_or(|value| !value.faults().is_empty())
416        {
417            return Err(CheckError::Nonconforming {
418                signature: expected.node(),
419            });
420        }
421    }
422    Ok(())
423}
424
425fn group<'m, 'd, 'p, E, P: Provider<'p, 'd, Evidence = E>>(
426    prepared: &Preparation<'m, 'd>,
427    signature: Node,
428    properties: &Properties<'d>,
429    signer: &Certificate<'d>,
430    provider: &P,
431    operation: &ScopedReader<'d, '_>,
432    complete: &mut bool,
433) -> Result<Group<'m, 'd, 'p, E>, CheckError<P::Error>> {
434    let work = operation.execution();
435    let mut entries = work.vector();
436    for value in properties.open_values() {
437        work.charge_work(1).map_err(|cause| {
438            CheckError::Processing(Error::resource(
439                cause,
440                "cades.credentials",
441                signature.span().start,
442            ))
443        })?;
444        if !matches!(
445            value.role,
446            OpenRole::AttributeCertificate
447                | OpenRole::OtherAttributeCertificate
448                | OpenRole::SignedAssertion
449        ) {
450            continue;
451        }
452        let source = prepared.document().locate(value.enclosure.span());
453        let outcome = if value.value.is_none() {
454            *complete = false;
455            Outcome::Absent
456        } else {
457            match provider
458                .authenticate(Context {
459                    source,
460                    signature,
461                    value: *value,
462                    signer,
463                    operation,
464                })
465                .map_err(|cause| CheckError::Provider {
466                    node: source.node,
467                    cause,
468                })? {
469                Authentication::Authenticated {
470                    evidence,
471                    authorities: Authorities::Paths([]),
472                } => {
473                    *complete = false;
474                    Outcome::MissingAuthorities(evidence)
475                }
476                Authentication::Unsupported => {
477                    *complete = false;
478                    Outcome::Authentication(Authentication::Unsupported)
479                }
480                Authentication::Rejected => {
481                    *complete = false;
482                    Outcome::Authentication(Authentication::Rejected)
483                }
484                authenticated @ Authentication::Authenticated { .. } => {
485                    Outcome::Authentication(authenticated)
486                }
487            }
488        };
489        entries
490            .try_push(Checked { source, outcome })
491            .map_err(|cause| {
492                CheckError::Processing(Error::resource(
493                    cause,
494                    "cades.credentials",
495                    source.node.span().start,
496                ))
497            })?;
498    }
499    Ok(Group {
500        signature,
501        entries: entries.into_owned(),
502    })
503}