Skip to main content

structured_xades/pki/
counter.rs

1//! Actual countersignature signer association without requiring `XAdES` properties.
2
3use crate::certificate::{CertificateXml, KeyAssociation};
4use crate::counter;
5use crate::xml::{Access, AccessError, AccessResult, Snapshot, Xml};
6use structured_asn1::memory::{OwnedVec, Work};
7use structured_ltv::pki::{CheckedPath, Counter, CounterReceipts};
8
9/// One actual child-signature receipt and its selected signer's checked path.
10/// This establishes neither revocation nor incorporation of validation data.
11/// A plain `XMLDSig` counter need not contain `SigningCertificateV2` to enter the
12/// material census; original successful key evidence establishes its signer.
13/// The scalar original-source receipt is copied without allocation so this
14/// result can outlive a counter walk's temporary frame. Only the independently
15/// checked path remains borrowed; no temporary XML view is retained.
16pub struct CounterPath<'view, 'document, N, D, G> {
17    counter: counter::Checked<N, D, G>,
18    path: &'view CheckedPath<'document>,
19}
20
21impl<'view, 'document, N, D, G> CounterPath<'view, 'document, N, D, G> {
22    /// Original actual child/parent `SignatureValue` coverage receipt.
23    #[must_use]
24    pub fn counter(&self) -> &counter::Checked<N, D, G> {
25        &self.counter
26    }
27
28    /// Original checked path associated with the child's successful key.
29    #[must_use]
30    pub fn path(&self) -> &'view CheckedPath<'document> {
31        self.path
32    }
33}
34
35/// Completed key association, distinct from processing/resource failure.
36pub enum CounterBinding<'view, 'document, N, D, G> {
37    /// Actual successful child key agrees with this path's effective key.
38    Bound(CounterPath<'view, 'document, N, D, G>),
39    /// Original selected-key evidence establishes a different association.
40    Differs,
41    /// Evidence does not establish the necessary selected-key association.
42    Unproven,
43}
44
45/// Counter/path association in this XML owner's original identity domain.
46pub type CounterPathResult<'view, 'document, X> = AccessResult<
47    CounterBinding<'view, 'document, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
48    X,
49>;
50
51/// Complete original countersignature actors, never a successful subset.
52/// Construction visits every original counter through the common iterative
53/// walker. These paths still need their own status and material evaluation.
54pub struct CounterPaths<'view, 'document, N, D, G> {
55    snapshot: Snapshot<D, G>,
56    signature: N,
57    actors: OwnedVec<CounterActor<'view, 'document, N, D, G>>,
58}
59
60/// Actual counter signer plus its independently protected credential sources.
61/// Plain counters and counters with no role property have no such catalogue.
62/// A present role is authenticated and read without imposing the root's B level.
63pub struct CounterActor<'view, 'document, N, D, G> {
64    signer: CounterPath<'view, 'document, N, D, G>,
65    containers: Option<crate::container::Containers<N, D, G>>,
66    roles: Option<crate::role::Roles<N, D, G>>,
67}
68
69impl<'view, 'document, N, D, G> CounterActor<'view, 'document, N, D, G> {
70    /// Actual child signer and its checked effective-key path.
71    #[must_use]
72    pub fn signer(&self) -> &CounterPath<'view, 'document, N, D, G> {
73        &self.signer
74    }
75    /// Original protected role credentials, which still need their own handler.
76    #[must_use]
77    pub fn roles(&self) -> Option<&crate::role::Roles<N, D, G>> {
78        self.roles.as_ref()
79    }
80    /// Original child qualifying-property layout, if this is an `XAdES`
81    /// counter. This is not a child baseline claim. Plain `XMLDSig` has none.
82    #[must_use]
83    pub fn containers(&self) -> Option<&crate::container::Containers<N, D, G>> {
84        self.containers.as_ref()
85    }
86}
87
88impl<'view, 'document, N: Copy, D: Copy, G: Copy> CounterPaths<'view, 'document, N, D, G> {
89    /// Original root source/generation under which all counters were visited.
90    #[must_use]
91    pub fn snapshot(&self) -> Snapshot<D, G> {
92        self.snapshot
93    }
94    /// Root signature whose complete incorporated counters were checked.
95    #[must_use]
96    pub fn signature(&self) -> N {
97        self.signature
98    }
99    /// Every original counter's bound signer path in depth-first source order.
100    #[must_use]
101    pub fn actors(&self) -> &[CounterActor<'view, 'document, N, D, G>] {
102        &self.actors
103    }
104}
105
106// The complete XMLDSig counter census is the common participant census input.
107impl<'view, 'document, N: Copy, D: Copy, G: Copy> CounterReceipts<'view, 'document>
108    for CounterPaths<'view, 'document, N, D, G>
109{
110    type Id = N;
111    type Document = D;
112    type Generation = G;
113
114    fn snapshot(&self) -> Snapshot<D, G> {
115        self.snapshot
116    }
117
118    fn signature(&self) -> N {
119        self.signature
120    }
121
122    fn len(&self) -> usize {
123        self.actors.len()
124    }
125
126    fn counter(&self, ordinal: usize) -> Counter<'view, 'document, N> {
127        let signer = &self.actors[ordinal].signer;
128        let source = signer.counter.original();
129        Counter {
130            signature: source.child(),
131            property: source.node(),
132            path: signer.path,
133        }
134    }
135}
136
137/// Actual path selection or source-bound key association could not complete.
138#[derive(Debug)]
139pub enum CounterSelectionError<PE, E, N> {
140    /// A present child credential property lacks original whole-property binding.
141    Binding(crate::binding::BindingError<E, N>),
142    /// Original child role structure/source/resource refused before selection.
143    Property(crate::properties::Error<E, N>),
144    /// Caller path discovery/validation refused, retaining its own cause.
145    Selection {
146        /// Original child signature requiring a path.
147        signature: N,
148        /// Exact selection failure; never interpreted as no counter.
149        cause: PE,
150    },
151    /// Current source/generation or operation refused before key association.
152    Access(AccessError<E, N>),
153    /// Completed selected-key evidence contradicts the supplied path.
154    Differs {
155        /// Original child signature, not the root's key.
156        signature: N,
157    },
158    /// No original successful-key receipt establishes this path association.
159    Unproven {
160        /// Original child signature still requiring an associated path.
161        signature: N,
162    },
163}
164
165/// Complete counter-source traversal, preserving selection and XML causes.
166pub type CounterPathsResult<'view, 'document, X, PE> = counter::WalkResult<
167    CounterPaths<'view, 'document, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
168    X,
169    CounterSelectionError<PE, <X as Xml>::Error, <X as Xml>::Id>,
170>;
171
172/// Bind a path for EVERY original incorporated counter, including plain
173/// `XMLDSig` children, before constructing a complete counter-actor census.
174/// The same iterative source walker, actual child coverage receipts and
175/// selected-key association kernel serve independent and whole-root checks.
176/// Path selection may use caller acquisition/trust policy, but must fund its
177/// data-dependent work through the supplied current operation before execution.
178/// Selection does not replay the original XML mathematical verification.
179///
180/// The callback cannot return references into its short-lived counter receipt.
181/// Its output borrows an independently checked path. A late missing path,
182/// mismatched/unproven key, XML failure or resource refusal drops all retained
183/// counter results; external selector side effects cannot be rolled back.
184pub fn bind_counters<'view, 'document, X: CertificateXml, W: Work, PE>(
185    root: &crate::binding::Selected<X>,
186    access: &Access<'_, X, W>,
187    mut select: impl FnMut(&counter::Binding<X>, &W) -> Result<&'view CheckedPath<'document>, PE>,
188) -> CounterPathsResult<'view, 'document, X, PE> {
189    let actors = counter::walk(access, root, |counter, layout| {
190        let signature = counter.original().child();
191        let roles = match layout {
192            Some(layout) => counter_roles(access, layout)?,
193            None => None,
194        };
195        let path = access
196            .query(signature, |_, _, work| Ok(select(counter, work)))
197            .map_err(CounterSelectionError::Access)?
198            .map_err(|cause| CounterSelectionError::Selection { signature, cause })?;
199        match bind_counter(path, counter, access).map_err(CounterSelectionError::Access)? {
200            CounterBinding::Bound(signer) => Ok(CounterActor {
201                signer,
202                roles,
203                containers: layout.copied(),
204            }),
205            CounterBinding::Differs => Err(CounterSelectionError::Differs { signature }),
206            CounterBinding::Unproven => Err(CounterSelectionError::Unproven { signature }),
207        }
208    })?;
209    Ok(CounterPaths {
210        snapshot: access.snapshot(),
211        signature: root.containers().signature(),
212        actors,
213    })
214}
215
216type RoleResult<X, PE> = Result<
217    Option<crate::role::Catalogue<X>>,
218    CounterSelectionError<PE, <X as Xml>::Error, <X as Xml>::Id>,
219>;
220
221fn counter_roles<X: CertificateXml, W: Work, PE>(
222    access: &Access<'_, X, W>,
223    layout: &crate::container::Layout<X>,
224) -> RoleResult<X, PE> {
225    let Some(node) =
226        crate::role::locate(access, layout).map_err(CounterSelectionError::Property)?
227    else {
228        return Ok(None);
229    };
230    let bound = crate::binding::bind(access, layout).map_err(CounterSelectionError::Binding)?;
231    crate::role::read_node(
232        access,
233        layout.snapshot(),
234        bound.containers().signature(),
235        node,
236    )
237    .map(Some)
238    .map_err(CounterSelectionError::Property)
239}
240
241/// Bind a checked child signature to its actual selected certificate/key.
242/// EN319132-1 §6.3(aa) includes incorporated countersignature actors in the
243/// material obligation, including plain `XMLDSig` counters (§5.2.7.2).
244/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
245///
246/// Uses the original successful XML-owner decision and this checked path's
247/// inherited parameters. It does not reverify, resolve a different candidate,
248/// fabricate certificate-reference properties or infer trust from `KeyInfo`.
249/// No catalogue allocation/copy is required. A foreign or stale source is
250/// terminal before owner dispatch; opaque original evidence may still prove
251/// the association, while absent evidence remains explicitly Unproven.
252///
253/// # Panics
254/// Only if the nonempty checked-path construction invariant fails.
255pub fn bind_counter<'view, 'document, X: CertificateXml, W: Work>(
256    path: &'view CheckedPath<'document>,
257    counter: &counter::Binding<X>,
258    access: &Access<'_, X, W>,
259) -> CounterPathResult<'view, 'document, X> {
260    let source = counter.original();
261    if source.snapshot() != access.snapshot() {
262        return Err(AccessError::Changed {
263            node: source.child(),
264        });
265    }
266    let target = path.members().last().expect("a checked path has a target");
267    let association = access.query(source.child(), |xml, snapshot, work| {
268        xml.key_association(
269            snapshot,
270            source.child(),
271            target.certificate(),
272            path.facts().key,
273            work,
274        )
275    })?;
276    Ok(match association {
277        KeyAssociation::Same => CounterBinding::Bound(CounterPath {
278            counter: *counter,
279            path,
280        }),
281        KeyAssociation::Differs => CounterBinding::Differs,
282        KeyAssociation::Unproven => CounterBinding::Unproven,
283    })
284}