Skip to main content

structured_xades/
credential.rs

1//! Source-bound independent credentials and their actual authority paths.
2
3use crate::role;
4use crate::xml::{Access, AccessError, Snapshot, Xml};
5use structured_asn1::memory::{MemoryError, OwnedVec, Work};
6use structured_ltv::pki::{self as ltv, Authorities, CredentialGroup, CredentialReceipts};
7
8/// One original credential protected by the selected `SignedProperties` binding.
9/// Construction is private; a self-claimed role cannot become a credential.
10#[derive(Debug, Clone, Copy)]
11pub struct Credential<N, D, G> {
12    snapshot: Snapshot<D, G>,
13    signature: N,
14    entry: role::Entry<N>,
15}
16
17impl<N: Copy, D: Copy, G: Copy> Credential<N, D, G> {
18    /// Exact original source/generation, never a reconstructed assertion.
19    #[must_use]
20    pub fn snapshot(self) -> Snapshot<D, G> {
21        self.snapshot
22    }
23    /// Signature protecting this original credential's role property.
24    #[must_use]
25    pub fn signature(self) -> N {
26        self.signature
27    }
28    /// Original payload, group and selected credential syntax.
29    #[must_use]
30    pub fn entry(self) -> role::Entry<N> {
31        self.entry
32    }
33}
34
35pub use structured_ltv::credential::Authentication;
36
37/// Format-specific authentication at the original protected credential source.
38///
39/// EN319132-1 §5.2.6 leaves concrete `SignedAssertion` and
40/// `OtherAttributeCertificate` content types outside `XAdES`. Their owner reuses
41/// its actual format/signature mechanisms; this core does not invent SAML,
42/// attribute-certificate syntax or another crypto dispatcher.
43/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
44///
45/// Implementations authenticate THIS exact original credential, including its
46/// binding to the signer where required by its format, with the returned paths'
47/// actual effective keys. They must not substitute a receipt for another
48/// credential or omit a path used by authentication. Source decoding, signature
49/// work, retained evidence and all intermediate capacity are pre-admitted under
50/// current Access/Work and the format owner's enforced limits. Errors preserve
51/// resource/processing causes and truthful inner-source provenance. No implicit
52/// acquisition, reparsing XML or application authorization is performed here.
53pub trait Provider<'path, 'document, X: Xml> {
54    /// Actual retained format evidence; may own admitted data or original IDs.
55    type Evidence;
56    /// Original format/provider/resource failure, not semantic rejection.
57    type Error;
58
59    /// Authenticate this original source once under the current operation.
60    fn authenticate<W: Work>(
61        &self,
62        access: &Access<'_, X, W>,
63        credential: Credential<X::Id, X::Document, X::Generation>,
64    ) -> Result<Authentication<'path, 'document, Self::Evidence>, Self::Error>;
65}
66
67/// Actual authentication result retained with its immutable original source.
68pub struct Checked<'path, 'document, E, N, D, G> {
69    source: Credential<N, D, G>,
70    evidence: E,
71    authorities: Authorities<'path, 'document>,
72}
73
74impl<'path, 'document, E, N: Copy, D: Copy, G: Copy> Checked<'path, 'document, E, N, D, G> {
75    /// Same original protected credential processed by the format owner.
76    #[must_use]
77    pub fn source(&self) -> Credential<N, D, G> {
78        self.source
79    }
80    /// Original retained authentication evidence, without cloning it.
81    #[must_use]
82    pub fn evidence(&self) -> &E {
83        &self.evidence
84    }
85    /// Actual authority inputs, including an explicit direct anchor or
86    /// certificate-free format. Subsequent path traversal must be paid.
87    #[must_use]
88    pub fn authorities(&self) -> Authorities<'path, 'document> {
89        self.authorities
90    }
91}
92
93/// Complete credential-actor set for this original signed role catalogue.
94/// This is neither a baseline grade nor proof of status/material availability.
95pub struct Credentials<'path, 'document, E, N, D, G> {
96    snapshot: Snapshot<D, G>,
97    signature: N,
98    entries: OwnedVec<Checked<'path, 'document, E, N, D, G>>,
99}
100
101impl<'path, 'document, E, N: Copy, D: Copy, G: Copy> Credentials<'path, 'document, E, N, D, G> {
102    /// Original protected role source/generation.
103    #[must_use]
104    pub fn snapshot(&self) -> Snapshot<D, G> {
105        self.snapshot
106    }
107    /// Original selected signature, independent of a credential issuer.
108    #[must_use]
109    pub fn signature(&self) -> N {
110        self.signature
111    }
112    /// Every independently authenticated credential in original order.
113    #[must_use]
114    pub fn entries(&self) -> &[Checked<'path, 'document, E, N, D, G>] {
115        &self.entries
116    }
117}
118
119// One signature's authenticated credentials are a common credential group.
120impl<'path, 'document, E, N: Copy, D: Copy, G: Copy> CredentialGroup<'path, 'document>
121    for Credentials<'path, 'document, E, N, D, G>
122{
123    type Id = N;
124
125    fn signature(&self) -> N {
126        self.signature
127    }
128
129    fn len(&self) -> usize {
130        self.entries.len()
131    }
132
133    fn credential(&self, ordinal: usize) -> ltv::Credential<'path, 'document, N> {
134        let checked = &self.entries[ordinal];
135        ltv::Credential {
136            signature: checked.source.signature,
137            node: checked.source.entry.node,
138            authorities: checked.authorities,
139        }
140    }
141}
142
143/// Refusal does not return a successfully checked credential prefix.
144#[derive(Debug)]
145pub enum Error<E, PE, N> {
146    /// Caller supplied a complete counter set from another selected signature.
147    Foreign {
148        /// Original root signature expected by this operation.
149        signature: N,
150    },
151    /// Original source/generation or operation refused.
152    Access(AccessError<E, N>),
153    /// The selected original format is unsupported, not absent or authenticated.
154    Unsupported {
155        /// Original credential payload.
156        node: N,
157    },
158    /// Completed original credential authentication was negative.
159    Rejected {
160        /// Original credential payload.
161        node: N,
162    },
163    /// A handler selected certificate paths but returned an empty set. Direct
164    /// anchors and certificate-free formats require their explicit arms; this
165    /// is incomplete provider evidence, not bad XML.
166    MissingAuthorities {
167        /// Original credential payload.
168        node: N,
169    },
170    /// Original provider failure with its credential source retained.
171    Provider {
172        /// Original credential payload.
173        node: N,
174        /// Unchanged provider/resource cause.
175        cause: PE,
176    },
177    /// Retained catalogue storage/copy work was not admitted.
178    Resource {
179        /// Original credential payload or catalogue for empty input.
180        node: N,
181        /// Actual memory/work refusal.
182        cause: MemoryError,
183    },
184}
185
186/// Complete original credential authentication and retained-source result.
187pub type CheckResult<'path, 'document, X, P> = Result<
188    Credentials<
189        'path,
190        'document,
191        <P as Provider<'path, 'document, X>>::Evidence,
192        <X as Xml>::Id,
193        <X as Xml>::Document,
194        <X as Xml>::Generation,
195    >,
196    Error<<X as Xml>::Error, <P as Provider<'path, 'document, X>>::Error, <X as Xml>::Id>,
197>;
198
199/// Authenticate EVERY original non-claimed credential before returning the
200/// complete authority-input set required by EN319132-1 §6.3(aa). The original
201/// catalogue determines this set, never a caller-selected list of successes.
202/// Claimed roles are traversed and paid but need no independent issuer check.
203/// A late unsupported/rejected credential or terminal refusal drops all earlier
204/// retained evidence. External provider side effects cannot be rolled back.
205pub fn check<'path, 'document, X: Xml, P: Provider<'path, 'document, X>, W: Work>(
206    roles: &role::Catalogue<X>,
207    provider: &P,
208    access: &Access<'_, X, W>,
209) -> CheckResult<'path, 'document, X, P> {
210    if roles.snapshot() != access.snapshot() {
211        return Err(Error::Access(AccessError::Changed { node: roles.node() }));
212    }
213    let mut entries = access
214        .query(roles.node(), |_, _, work| Ok(work.vector()))
215        .map_err(Error::Access)?;
216    for entry in roles.entries() {
217        access
218            .query(entry.node, |_, _, work| Ok(work.charge_work(1)))
219            .map_err(Error::Access)?
220            .map_err(|cause| Error::Resource {
221                node: entry.node,
222                cause,
223            })?;
224        if entry.kind == role::Kind::Claimed {
225            continue;
226        }
227        let source = Credential {
228            snapshot: roles.snapshot(),
229            signature: roles.signature(),
230            entry: *entry,
231        };
232        let outcome = access
233            .query(entry.node, |_, _, _| {
234                Ok(provider.authenticate(access, source))
235            })
236            .map_err(Error::Access)?
237            .map_err(|cause| Error::Provider {
238                node: entry.node,
239                cause,
240            })?;
241        let (evidence, authorities) = match outcome {
242            Authentication::Unsupported => return Err(Error::Unsupported { node: entry.node }),
243            Authentication::Rejected => return Err(Error::Rejected { node: entry.node }),
244            Authentication::Authenticated {
245                evidence,
246                authorities,
247            } => (evidence, authorities),
248        };
249        if let Authorities::Paths(paths) = authorities
250            && paths.is_empty()
251        {
252            return Err(Error::MissingAuthorities { node: entry.node });
253        }
254        entries
255            .try_push(Checked {
256                source,
257                evidence,
258                authorities,
259            })
260            .map_err(|cause| Error::Resource {
261                node: entry.node,
262                cause,
263            })?;
264    }
265    access
266        .query(roles.node(), |_, _, _| Ok(()))
267        .map_err(Error::Access)?;
268    Ok(Credentials {
269        snapshot: roles.snapshot(),
270        signature: roles.signature(),
271        entries: entries.into_owned(),
272    })
273}
274
275/// Actual credentials of one incorporated counter, retaining its source edge.
276pub struct CounterCredentials<'path, 'document, E, N, D, G> {
277    source: crate::counter::Checked<N, D, G>,
278    credentials: Credentials<'path, 'document, E, N, D, G>,
279}
280
281impl<'path, 'document, E, N, D, G> CounterCredentials<'path, 'document, E, N, D, G> {
282    /// Original checked child/parent signature coverage receipt.
283    #[must_use]
284    pub fn source(&self) -> &crate::counter::Checked<N, D, G> {
285        &self.source
286    }
287    /// Every credential of this counter, without a copied catalogue/evidence.
288    #[must_use]
289    pub fn credentials(&self) -> &Credentials<'path, 'document, E, N, D, G> {
290        &self.credentials
291    }
292}
293
294/// Complete current-role credentials of the root and all incorporated counters.
295/// No caller-provided success list determines which groups are visited. Other
296/// actor path/status/material and format-specific obligations remain separate.
297pub struct ParticipantCredentials<'path, 'document, E, N, D, G> {
298    snapshot: Snapshot<D, G>,
299    signature: N,
300    root: Option<Credentials<'path, 'document, E, N, D, G>>,
301    counters: OwnedVec<CounterCredentials<'path, 'document, E, N, D, G>>,
302}
303
304impl<'path, 'document, E, N: Copy, D: Copy, G: Copy>
305    ParticipantCredentials<'path, 'document, E, N, D, G>
306{
307    /// Original root source and generation for the whole participant set.
308    #[must_use]
309    pub fn snapshot(&self) -> Snapshot<D, G> {
310        self.snapshot
311    }
312    /// Original root signature, independent of every credential issuer.
313    #[must_use]
314    pub fn signature(&self) -> N {
315        self.signature
316    }
317    /// All original root credentials, if a role property was present.
318    #[must_use]
319    pub fn root(&self) -> Option<&Credentials<'path, 'document, E, N, D, G>> {
320        self.root.as_ref()
321    }
322    /// Every incorporated counter with a protected current role property.
323    /// Other counters remain in the separate complete counter-signer census.
324    #[must_use]
325    pub fn counters(&self) -> &[CounterCredentials<'path, 'document, E, N, D, G>] {
326        &self.counters
327    }
328}
329
330// The root and counter credential groups are the common credential census.
331impl<'path, 'document, E, N: Copy, D: Copy, G: Copy> CredentialReceipts<'path, 'document>
332    for ParticipantCredentials<'path, 'document, E, N, D, G>
333{
334    type Id = N;
335    type Document = D;
336    type Generation = G;
337    type Group = Credentials<'path, 'document, E, N, D, G>;
338
339    fn snapshot(&self) -> Snapshot<D, G> {
340        self.snapshot
341    }
342
343    fn signature(&self) -> N {
344        self.signature
345    }
346
347    fn root(&self) -> Option<&Self::Group> {
348        self.root.as_ref()
349    }
350
351    fn counters(&self) -> usize {
352        self.counters.len()
353    }
354
355    fn counter(&self, ordinal: usize) -> (N, &Self::Group) {
356        let counter = &self.counters[ordinal];
357        (counter.source.original().child(), &counter.credentials)
358    }
359}
360
361/// Source-bound complete credential participants in this XML identity domain.
362pub type ParticipantResult<'path, 'document, X, P> = Result<
363    ParticipantCredentials<
364        'path,
365        'document,
366        <P as Provider<'path, 'document, X>>::Evidence,
367        <X as Xml>::Id,
368        <X as Xml>::Document,
369        <X as Xml>::Generation,
370    >,
371    Error<<X as Xml>::Error, <P as Provider<'path, 'document, X>>::Error, <X as Xml>::Id>,
372>;
373
374/// Check every credential group of the original root and COMPLETE counter set.
375/// Reuses `check` rather than another credential decoder/authenticator. Missing
376/// paths, unsupported formats and all late refusals release partial groups.
377/// No root/counter signature or certificate-path mathematics is repeated.
378pub fn check_participants<'path, 'document, X: Xml, P: Provider<'path, 'document, X>, W: Work>(
379    prepared: &crate::prepare::Prepared<X>,
380    counters: &crate::pki::CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
381    provider: &P,
382    access: &Access<'_, X, W>,
383) -> ParticipantResult<'path, 'document, X, P> {
384    use structured_asn1::memory::HashWork;
385    let layout = prepared.bound().containers();
386    let signature = layout.signature();
387    if layout.snapshot() != access.snapshot() || counters.snapshot() != access.snapshot() {
388        return Err(Error::Access(AccessError::Changed { node: signature }));
389    }
390    let mut groups = access
391        .query(signature, |_, _, work| Ok(work.vector()))
392        .map_err(Error::Access)?;
393    let cost = signature
394        .hash_work()
395        .and_then(|cost| {
396            cost.checked_add(counters.signature().hash_work()?)
397                .ok_or(MemoryError::WorkExceeded)
398        })
399        .map_err(|cause| Error::Resource {
400            node: signature,
401            cause,
402        })?;
403    access
404        .execution()
405        .charge_work(cost)
406        .map_err(|cause| Error::Resource {
407            node: signature,
408            cause,
409        })?;
410    if signature != counters.signature() {
411        return Err(Error::Foreign { signature });
412    }
413    let root = match prepared.roles() {
414        Some(roles) => Some(check(roles, provider, access)?),
415        None => None,
416    };
417    for actor in counters.actors() {
418        access
419            .query(signature, |_, _, work| Ok(work.charge_work(1)))
420            .map_err(Error::Access)?
421            .map_err(|cause| Error::Resource {
422                node: signature,
423                cause,
424            })?;
425        let Some(roles) = actor.roles() else {
426            continue;
427        };
428        let credentials = check(roles, provider, access)?;
429        groups
430            .try_push(CounterCredentials {
431                source: *actor.signer().counter(),
432                credentials,
433            })
434            .map_err(|cause| Error::Resource {
435                node: roles.node(),
436                cause,
437            })?;
438    }
439    access
440        .query(signature, |_, _, _| Ok(()))
441        .map_err(Error::Access)?;
442    Ok(ParticipantCredentials {
443        snapshot: layout.snapshot(),
444        signature,
445        root,
446        counters: groups.into_owned(),
447    })
448}