Skip to main content

structured_cades/
counters.rs

1//! Complete original countersignature bindings through the existing CMS driver.
2
3use crate::Error;
4use crate::basic::{PathRestriction, ess_matches, exact};
5use crate::prepare::Preparation;
6use crate::source::{DocumentId, Node, Original};
7use structured_asn1::memory::{OwnedVec, Work};
8use structured_asn1::{Reader, Span};
9use structured_cms::binding::Scope;
10use structured_cms::cades::Role;
11use structured_cms::counter::{Visit, WalkError};
12use structured_cms::directory::{self, CertificateAgreement};
13use structured_cms::prepare::{self, PrepareError};
14use structured_cms::provider::{SignatureAnswer, SignatureProvider};
15use structured_cms::verify::{Batch, ContentAgreement, PassError};
16use structured_cms::{Octets, signer::SignerInfo};
17use structured_ltv::pki::{CheckedPath, Counter, CounterReceipts};
18use structured_ltv::source::Snapshot;
19use structured_pkix::Certificate;
20use structured_pkix::algorithm::digest::AsnDigest;
21
22/// One actual counter's execution refusal; negative document answers stay facts.
23#[derive(Debug)]
24pub enum CheckError<E> {
25    /// Original common provenance, schema or resource refusal.
26    Processing(Error),
27    /// Original signature preflight/provider refusal.
28    Prepare(PrepareError<E>),
29    /// Original complete parent-signature pass/provider refusal.
30    Verify(PassError<structured_cms::octets::SourceError, E>),
31    /// Original selected-certificate hint query refusal.
32    Identifier(directory::Error),
33    /// Original ESS execution refusal.
34    Ess(structured_cms::ess::BindingError<E>),
35    /// Original signed validation-set/path restriction execution refusal.
36    Restriction(crate::basic::BindError<E>),
37}
38
39/// Completed insufficiency remains distinct from a processing failure.
40#[derive(Debug)]
41pub enum Outcome {
42    /// Original counter's arithmetic, certificate and profile binding hold.
43    Bound,
44    /// Its readable signature/payload/certificate/profile fact did not hold.
45    Differs,
46    /// Its selected provider cannot process the original mechanism.
47    Unsupported(prepare::Part),
48    /// Original malformed binding was retained, not turned into absence.
49    Malformed(structured_cms::Error),
50}
51
52/// Actual counter result retaining the checked path and original parent link.
53/// Only [`Context::check`] constructs it over the parent's ORIGINAL octets.
54pub struct Binding<'model, 'document, 'path> {
55    source: Original<'model, 'document>,
56    parent: Node,
57    path: &'path CheckedPath<'document>,
58    outcome: Outcome,
59    profile: structured_cms::cades::Profile<'document>,
60    identifier: Option<CertificateAgreement>,
61    signing: Option<structured_cms::ess::Bindings>,
62    content: Option<ContentAgreement>,
63    signature: Option<SignatureAnswer>,
64    restrictions: Option<PathRestriction>,
65}
66
67impl<'m, 'd, 'p> Binding<'m, 'd, 'p> {
68    fn unproven(
69        source: Original<'m, 'd>,
70        parent: Span,
71        path: &'p CheckedPath<'d>,
72        profile: structured_cms::cades::Profile<'d>,
73    ) -> Self {
74        Self {
75            source,
76            parent: Node::of(parent),
77            path,
78            outcome: Outcome::Differs,
79            profile,
80            identifier: None,
81            signing: None,
82            content: None,
83            signature: None,
84            restrictions: None,
85        }
86    }
87    /// Exact original child in its immutable source generation.
88    #[must_use]
89    pub fn source(&self) -> Original<'m, 'd> {
90        self.source
91    }
92    /// Original direct parent, not the root of a flattened counter list.
93    #[must_use]
94    pub fn parent(&self) -> Node {
95        self.parent
96    }
97    /// Original completed answer, never a trust or root baseline verdict.
98    #[must_use]
99    pub fn outcome(&self) -> &Outcome {
100        &self.outcome
101    }
102    /// Actual original profile and every independently retained schema fault.
103    #[must_use]
104    pub fn profile(&self) -> &structured_cms::cades::Profile<'d> {
105        &self.profile
106    }
107    /// Original SID result, including unreadable SKI details; None means that
108    /// no target certificate was available, not a completed mismatch.
109    #[must_use]
110    pub fn identifier(&self) -> Option<&CertificateAgreement> {
111        self.identifier.as_ref()
112    }
113    /// Original independent ESS answers, when certificate binding was reached.
114    #[must_use]
115    pub fn signing(&self) -> Option<structured_cms::ess::Bindings> {
116        self.signing
117    }
118    /// Original payload-to-attribute answer, preserving owned malformed detail.
119    #[must_use]
120    pub fn content(&self) -> Option<&ContentAgreement> {
121        self.content.as_ref()
122    }
123    /// Actual mathematical result, independently of payload/profile binding.
124    #[must_use]
125    pub fn signature(&self) -> Option<SignatureAnswer> {
126        self.signature
127    }
128    /// Actual later-certificate restriction result, if that stage was reached.
129    #[must_use]
130    pub fn restrictions(&self) -> Option<PathRestriction> {
131        self.restrictions
132    }
133}
134
135/// Short borrowed selection for one original child. It cannot escape the
136/// iterative walk; its returned binding may retain original bytes/path facts.
137pub struct Context<'scope, 'model, 'document> {
138    selected: Original<'model, 'document>,
139    signer: &'scope SignerInfo<'document>,
140    parent_signature: &'scope Octets<'document>,
141    parent: Span,
142    reader: &'scope Reader<'document>,
143}
144
145impl<'m, 'd> Context<'_, 'm, 'd> {
146    /// Original child used to choose its certificate/provider, without reparsing.
147    #[must_use]
148    pub fn signer(&self) -> &SignerInfo<'d> {
149        self.signer
150    }
151
152    /// Checks exactly this parent's signature octets through the shared CMS
153    /// counter scope, original attrs and selected provider. The application only
154    /// selects certificate/path/provider; it cannot supply a replacement payload
155    /// or a writable successful binding. No plain counter is required to carry
156    /// the root's content-type/signing-time/ESS baseline fields.
157    /// RFC5652 §11.4 and EN319122-1 §5.2.7 define this input and placement.
158    /// <https://www.rfc-editor.org/rfc/rfc5652#section-11.4>
159    pub fn check<'a, 'p, P>(
160        &'a self,
161        provider: &'a P,
162        certificate: &Certificate<'d>,
163        path: &'p CheckedPath<'d>,
164    ) -> Result<Binding<'m, 'd, 'p>, CheckError<P::Error>>
165    where
166        'd: 'a,
167        P: SignatureProvider<'a> + AsnDigest<'a>,
168    {
169        let at = self.signer.span().start;
170        let processing = |error| CheckError::Processing(Error::Cms(error));
171        let work = self.reader.execution();
172        let resource = |cause| CheckError::Processing(Error::resource(cause, "cades.counter", at));
173        let profile =
174            structured_cms::cades::profile(self.signer, Role::Countersignature, self.reader)
175                .map_err(processing)?;
176        let conforms = profile_conforms(self.signer, &profile, self.reader)?;
177        let mut result = Binding::unproven(self.selected, self.parent, path, profile);
178        let Some(target) = path.members().last() else {
179            return Ok(result);
180        };
181        let same_certificate = exact(
182            target.certificate().as_bytes(),
183            certificate.as_bytes(),
184            &work,
185        )
186        .map_err(resource)?;
187        result.identifier = Some(
188            directory::matches_certificate_in(self.signer.sid(), certificate, self.reader)
189                .map_err(CheckError::Identifier)?,
190        );
191        if !same_certificate
192            || !matches!(result.identifier, Some(CertificateAgreement::Matches))
193            || !conforms
194        {
195            return Ok(result);
196        }
197        let plan = prepare::resolve(
198            provider,
199            self.signer,
200            target.key(),
201            self.reader,
202            Scope::CounterSignature,
203        )
204        .map_err(CheckError::Prepare)?;
205        let plan = match plan {
206            prepare::Preparation::Ready(plan) => plan,
207            prepare::Preparation::Unsupported(part) => {
208                result.outcome = Outcome::Unsupported(part);
209                return Ok(result);
210            }
211            prepare::Preparation::Malformed(error) => {
212                result.outcome = Outcome::Malformed(error);
213                return Ok(result);
214            }
215        };
216        let mut requests = work.vector();
217        requests.try_push(plan).map_err(resource)?;
218        let batch = Batch::prepare(provider, requests.into_owned(), &work, "cades.counter", at)
219            .map_err(|source| {
220                CheckError::Verify(structured_cms::stream::StreamError::Callback(source))
221            })?;
222        let mut original_parent = self.parent_signature;
223        let checks = batch
224            .run(&mut original_parent, &work)
225            .map_err(CheckError::Verify)?;
226        let Some(checked) = checks.into_iter().next() else {
227            return Err(CheckError::Processing(Error::Foreign { at }));
228        };
229        let (_, _, content, signature) = checked.into_parts();
230        let math = matches!(
231            content,
232            ContentAgreement::Direct | ContentAgreement::Matches
233        ) && signature == Some(SignatureAnswer::Valid);
234        result.content = Some(content);
235        result.signature = signature;
236        let signing = result.profile.signing();
237        let ess = if signing.v1().is_some() || signing.v2().is_some() {
238            let bindings = signing
239                .bind_certificate(provider, certificate, self.reader)
240                .map_err(CheckError::Ess)?;
241            result.signing = Some(bindings);
242            ess_matches(bindings)
243        } else {
244            true
245        };
246        let restriction = crate::basic::restrictions(signing, Some(path), provider, self.reader)
247            .map_err(CheckError::Restriction)?;
248        result.restrictions = Some(restriction);
249        if math
250            && ess
251            && matches!(
252                restriction,
253                PathRestriction::Unstated | PathRestriction::Matches
254            )
255        {
256            result.outcome = Outcome::Bound;
257        }
258        Ok(result)
259    }
260}
261
262fn profile_conforms<E>(
263    signer: &SignerInfo<'_>,
264    profile: &structured_cms::cades::Profile<'_>,
265    reader: &Reader<'_>,
266) -> Result<bool, CheckError<E>> {
267    let mut conforms = signer.faults().is_empty()
268        && profile.faults().is_empty()
269        && profile.signing().faults().is_empty();
270    for attribute in [profile.signing().v1(), profile.signing().v2()]
271        .into_iter()
272        .flatten()
273    {
274        reader
275            .charge(1)
276            .map_err(|cause| CheckError::Processing(Error::encoding(cause, "cades.counter")))?;
277        if !attribute.faults().is_empty() {
278            conforms = false;
279        }
280    }
281    Ok(conforms)
282}
283
284/// Complete original counter outcomes. Negative outcomes are kept; they cannot
285/// be converted to the complete-path receipt required by the LTV participant set.
286pub struct Bindings<'model, 'document, 'path> {
287    signature: Original<'model, 'document>,
288    values: OwnedVec<Option<Binding<'model, 'document, 'path>>>,
289    complete: bool,
290}
291
292impl<'m, 'd, 'p> Bindings<'m, 'd, 'p> {
293    /// Every original readable counter, once, in original depth-first order.
294    #[must_use]
295    pub fn values(&self) -> &[Option<Binding<'m, 'd, 'p>>] {
296        &self.values
297    }
298    /// A complete actual-path census only when EVERY original counter bound.
299    #[must_use]
300    pub fn paths(&self) -> Option<Paths<'_, 'm, 'd, 'p>> {
301        if self.complete {
302            Some(Paths { bindings: self })
303        } else {
304            None
305        }
306    }
307}
308
309/// Borrowed complete actual counter paths, issued after checking all outcomes.
310pub struct Paths<'receipt, 'model, 'document, 'path> {
311    bindings: &'receipt Bindings<'model, 'document, 'path>,
312}
313
314impl<'m, 'd, 'p> CounterReceipts<'p, 'd> for Paths<'_, 'm, 'd, 'p> {
315    type Id = Node;
316    type Document = DocumentId<'m, 'd>;
317    type Generation = ();
318    fn snapshot(&self) -> Snapshot<DocumentId<'m, 'd>, ()> {
319        self.bindings.signature.snapshot
320    }
321    fn signature(&self) -> Node {
322        self.bindings.signature.node
323    }
324    fn len(&self) -> usize {
325        self.bindings.values.len()
326    }
327    fn counter(&self, ordinal: usize) -> Counter<'p, 'd, Node> {
328        let Some(entry) = &self.bindings.values[ordinal] else {
329            unreachable!("only a complete counter set exposes Paths")
330        };
331        Counter {
332            signature: entry.source.node,
333            property: entry.source.node,
334            path: entry.path,
335        }
336    }
337}
338
339/// Original walk or selected-handler refusal; no partial result is returned.
340pub type BindingsError<E> = WalkError<SelectionError<E>>;
341
342/// Counter selection failure or a substituted original receipt.
343#[derive(Debug)]
344pub enum SelectionError<E> {
345    /// Exact caller/provider selection failure, without conversion to mismatch.
346    Callback(E),
347    /// Selected result belongs to a different child, parent or source.
348    Foreign {
349        /// Actual child being selected.
350        at: u64,
351    },
352}
353
354/// Visits the complete original nested counter set, allowing only selection and
355/// use of [`Context::check`]. Returned bindings are checked against the actual
356/// current child/parent/source before being retained. A callback cannot replay a
357/// receipt for another child, and a malformed value cannot be silently skipped.
358pub fn bind<'m, 'd, 'p, E>(
359    prepared: &Preparation<'m, 'd>,
360    parent: &Reader<'d>,
361    mut select: impl FnMut(Context<'_, 'm, 'd>) -> Result<Binding<'m, 'd, 'p>, E>,
362) -> Result<Bindings<'m, 'd, 'p>, BindingsError<E>> {
363    let mut complete = true;
364    let values = structured_cms::counter::walk(prepared.signer(), parent, |visit, reader| {
365        let Visit::Signer {
366            signer,
367            signature,
368            parent: owner,
369        } = visit
370        else {
371            // The prepared report retains the unreadable value and its cause.
372            // No complete-path receipt will be issued for this census.
373            complete = false;
374            return Ok(None);
375        };
376        let selected = prepared.document().locate(signer.span());
377        let result = select(Context {
378            selected,
379            signer,
380            parent_signature: signature,
381            parent: owner,
382            reader,
383        })
384        .map_err(SelectionError::Callback)?;
385        if result.source != selected || result.parent != Node::of(owner) {
386            return Err(SelectionError::Foreign {
387                at: signer.span().start,
388            });
389        }
390        if !matches!(result.outcome, Outcome::Bound) {
391            complete = false;
392        }
393        Ok(Some(result))
394    })?;
395    Ok(Bindings {
396        signature: prepared.document().locate(prepared.signer().span()),
397        values,
398        complete,
399    })
400}