Skip to main content

structured_cades/
basic.rs

1//! Binding the original root to the actual CMS check and selected path.
2
3use crate::Error;
4use crate::prepare::Preparation;
5use crate::source::{DocumentId, Node, Original};
6use structured_asn1::Reader;
7use structured_asn1::memory::Work;
8use structured_cms::directory::{self, CertificateAgreement};
9use structured_cms::ess::{Binding, BindingError, Bindings};
10use structured_cms::provider::SignatureAnswer;
11use structured_cms::verify::{Checked, ContentAgreement, DocumentChecks};
12use structured_ltv::pki::CounterReceipts;
13use structured_ltv::pki::{CheckedPath, SignerReceipt};
14use structured_pkix::Certificate;
15use structured_pkix::algorithm::digest::AsnDigest;
16
17/// Same-source root field/certificate/math and complete counter composition.
18/// The report preserves its inputs and does not assert timestamp chronology,
19/// status usability, optional-policy acceptance or caller trust.
20pub struct Basic<'receipt, 'prepared, 'model, 'document, 'path> {
21    signer: &'receipt SignerBinding<'prepared, 'model, 'document, 'path>,
22    counters: crate::counters::Paths<'receipt, 'model, 'document, 'path>,
23    complete: bool,
24}
25
26impl<'r, 'p, 'm, 'd, 'v> Basic<'r, 'p, 'm, 'd, 'v> {
27    /// Actual selected original certificate/signature binding, without replay.
28    #[must_use]
29    pub fn binding(&self) -> &'r SignerBinding<'p, 'm, 'd, 'v> {
30        self.signer
31    }
32    /// Every original counter's already checked actual path.
33    #[must_use]
34    pub fn counter_paths(&self) -> &crate::counters::Paths<'r, 'm, 'd, 'v> {
35        &self.counters
36    }
37    /// Only mandatory B-B format and original root/counter mathematical binding.
38    /// A higher requested profile's missing timestamp cannot erase this fact.
39    #[must_use]
40    pub fn all_basic_requirements_met(&self) -> bool {
41        self.complete
42    }
43}
44
45impl<'r, 'p, 'm, 'd, 'v> structured_ltv::baseline::BasicStage<'v, 'd>
46    for Basic<'r, 'p, 'm, 'd, 'v>
47{
48    type Id = Node;
49    type Document = DocumentId<'m, 'd>;
50    type Generation = ();
51    type Signer = SignerBinding<'p, 'm, 'd, 'v>;
52    type Counters = crate::counters::Paths<'r, 'm, 'd, 'v>;
53    fn signer(&self) -> &Self::Signer {
54        self.signer
55    }
56    fn counters(&self) -> &Self::Counters {
57        &self.counters
58    }
59    fn all_requirements_met(&self) -> bool {
60        self.complete
61    }
62}
63
64/// Combine original root and full counter receipts with the actual B-B format
65/// reading. Receipt/source/counter membership is checked before returning a
66/// report; writable booleans and selected success subsets cannot enter it.
67pub fn basic<'r, 'p, 'm, 'd, 'v>(
68    signer: &'r SignerBinding<'p, 'm, 'd, 'v>,
69    counters: crate::counters::Paths<'r, 'm, 'd, 'v>,
70    operation: &Reader<'_>,
71) -> crate::Result<Basic<'r, 'p, 'm, 'd, 'v>> {
72    let prepared = signer.prepared();
73    prepared
74        .document()
75        .message()
76        .require_session(operation, "cades.basic")?;
77    let selected = prepared.document().locate(prepared.signer().span());
78    operation
79        .charge(1)
80        .map_err(|cause| Error::encoding(cause, "cades.basic"))?;
81    if counters.snapshot() != selected.snapshot
82        || counters.signature() != selected.node
83        || counters.len() != prepared.counters().len()
84    {
85        return Err(Error::Foreign {
86            at: selected.node.span().start,
87        });
88    }
89    for (ordinal, expected) in prepared.counters().iter().enumerate() {
90        operation
91            .charge(1)
92            .map_err(|cause| Error::encoding(cause, "cades.basic"))?;
93        if counters.counter(ordinal).signature != expected.node() {
94            return Err(Error::Foreign {
95                at: expected.node().span().start,
96            });
97        }
98    }
99    Ok(Basic {
100        signer,
101        counters,
102        complete: prepared.basic_format_requirements_met() && signer.is_bound(),
103    })
104}
105
106/// Original refusal at the actual shared operation, not a negative binding.
107#[derive(Debug)]
108pub enum BindError<E> {
109    /// Source, metadata, byte comparison or capacity refused.
110    Processing(Error),
111    /// Existing CMS SID query failed with its original cause.
112    Identifier(directory::Error),
113    /// Existing ESS hash/issuer binding failed with its original cause.
114    Ess(BindingError<E>),
115}
116
117/// Signed ESS validation-set restriction against the actual selected path.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum PathRestriction {
120    /// No later certificate identifier constrains the path.
121    Unstated,
122    /// Each actual path certificate belongs to every written restricted set.
123    Matches,
124    /// A signed restriction exists but its actual checked path was not supplied.
125    PathUnavailable,
126    /// A supplied path uses a certificate outside a signed restricted set.
127    Differs,
128    /// Original unreadable restriction or unsupported digest prevents proof.
129    Unproven,
130}
131
132/// Completed root binding facts, including negative mathematical outcomes.
133/// Construction performs ESS itself; externally writable `Bindings` cannot
134/// manufacture a receipt. A retained path is available only for an actual
135/// fully associated signer, never from a public boolean assertion.
136pub struct SignerBinding<'receipt, 'model, 'document, 'path> {
137    prepared: &'receipt Preparation<'model, 'document>,
138    path: Option<&'path CheckedPath<'document>>,
139    certificate: &'path Certificate<'document>,
140    identifier: CertificateAgreement,
141    ess: Bindings,
142    key_matches: bool,
143    included: Option<Original<'model, 'document>>,
144    mathematical: bool,
145    restrictions: PathRestriction,
146}
147
148impl<'r, 'm, 'd, 'p> SignerBinding<'r, 'm, 'd, 'p> {
149    /// Same original requested-level preparation.
150    #[must_use]
151    pub fn prepared(&self) -> &'r Preparation<'m, 'd> {
152        self.prepared
153    }
154    /// Candidate actually compared with SID, ESS and the CMS checked key.
155    #[must_use]
156    pub fn certificate(&self) -> &'p Certificate<'d> {
157        self.certificate
158    }
159    /// Original signer-identifier agreement, including unreadable SKI causes.
160    #[must_use]
161    pub fn identifier(&self) -> &CertificateAgreement {
162        &self.identifier
163    }
164    /// Both independently computed ESS answers. Neither absent value is success.
165    #[must_use]
166    pub fn ess(&self) -> Bindings {
167        self.ess
168    }
169    /// The supplied effective key agrees with this path's actual target key.
170    #[must_use]
171    pub fn key_matches(&self) -> bool {
172        self.key_matches
173    }
174    /// Exact original occurrence in root SignedData.certificates, if present.
175    #[must_use]
176    pub fn included(&self) -> Option<Original<'m, 'd>> {
177        self.included
178    }
179    /// CMS arithmetic and payload binding both succeeded. This alone says
180    /// nothing about the root profile, certificate, path, or caller trust.
181    #[must_use]
182    pub fn mathematical(&self) -> bool {
183        self.mathematical
184    }
185    /// Actual path-set restriction result, independent of signer certHash.
186    #[must_use]
187    pub fn restrictions(&self) -> PathRestriction {
188        self.restrictions
189    }
190    /// Every currently established root binding condition holds. The complete
191    /// B-B assembly additionally requires all format/attribute/counter facts.
192    #[must_use]
193    pub fn is_bound(&self) -> bool {
194        self.mathematical
195            && self.key_matches
196            && self.included.is_some()
197            // Inspection may retain a conflicting ESS version without choosing
198            // either assertion. Agreement with the other version cannot waive
199            // that failure or issue a signer/path receipt for it.
200            && self.prepared.profile().signing().faults().is_empty()
201            && matches!(self.identifier, CertificateAgreement::Matches)
202            && ess_matches(self.ess)
203            && matches!(
204                self.restrictions,
205                PathRestriction::Unstated | PathRestriction::Matches
206            )
207    }
208}
209
210impl<'m, 'd, 'p> SignerReceipt<'p, 'd> for SignerBinding<'_, 'm, 'd, 'p> {
211    type Id = Node;
212    type Document = DocumentId<'m, 'd>;
213    type Generation = ();
214    fn signature(&self) -> Original<'m, 'd> {
215        self.prepared
216            .document()
217            .locate(self.prepared.signer().span())
218    }
219    fn path(&self) -> Option<&'p CheckedPath<'d>> {
220        if self.is_bound() { self.path } else { None }
221    }
222}
223
224/// Binds the ORIGINAL CMS checked signer and effective key to its selected
225/// certificate and actual common-validator path. No signature or payload is
226/// replayed. SID/name, exact incorporation and ESS use their existing kernels.
227///
228/// EN319122-1 §§5.2.2/6.3(a) require the protected certificate binding and its
229/// original root occurrence. Trust selection, algorithm permissions and current
230/// or historical revocation acceptance remain explicit caller work.
231/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf>
232pub fn bind<'a, 'r, 'm, 'd, 'p, P: AsnDigest<'a>>(
233    prepared: &'r Preparation<'m, 'd>,
234    document_checks: &DocumentChecks<'_>,
235    checked: &Checked<'_>,
236    certificate: &'p Certificate<'d>,
237    path: Option<&'p CheckedPath<'d>>,
238    provider: &'a P,
239    parent: &Reader<'a>,
240) -> Result<SignerBinding<'r, 'm, 'd, 'p>, BindError<P::Error>>
241where
242    'd: 'a,
243{
244    let at = prepared.signer().span().start;
245    let processing = BindError::Processing;
246    prepared
247        .document()
248        .message()
249        .require_session(parent, "cades.basic")
250        .map_err(|error| processing(Error::Cms(error)))?;
251    if !core::ptr::eq(checked.signer(), prepared.signer()) {
252        return Err(processing(Error::Foreign { at }));
253    }
254    let work = parent.execution();
255    document_checks
256        .require(prepared.document().message(), checked, &work)
257        .map_err(|error| processing(Error::Cms(error)))?;
258    let refusal = |cause| processing(Error::resource(cause, "cades.basic", at));
259    work.charge_work(1).map_err(refusal)?;
260    let mathematical = matches!(
261        checked.content(),
262        ContentAgreement::Matches | ContentAgreement::Direct
263    ) && checked.signature() == Some(SignatureAnswer::Valid);
264    let identifier =
265        directory::matches_certificate_in(prepared.signer().sid(), certificate, parent)
266            .map_err(BindError::Identifier)?;
267    let mut key_matches = exact(
268        checked.key().key().as_bytes(),
269        certificate.tbs().public_key().as_bytes(),
270        &work,
271    )
272    .map_err(refusal)?;
273    if let Some(path) = path {
274        if let Some(target) = path.members().last() {
275            key_matches &= exact(
276                target.certificate().as_bytes(),
277                certificate.as_bytes(),
278                &work,
279            )
280            .map_err(refusal)?;
281            key_matches &=
282                same_parameters(checked.key().parameters(), target.key().parameters(), &work)
283                    .map_err(refusal)?;
284        } else {
285            key_matches = false;
286        }
287    } else {
288        key_matches &= same_parameters(
289            checked.key().parameters(),
290            certificate.tbs().public_key().algorithm().parameters(),
291            &work,
292        )
293        .map_err(refusal)?;
294    }
295    let mut included = None;
296    for original in prepared.document().message().certificates() {
297        work.charge_work(1).map_err(refusal)?;
298        if exact(original.as_bytes(), certificate.as_bytes(), &work).map_err(refusal)? {
299            included = Some(prepared.document().locate(original.span()));
300            break;
301        }
302    }
303    let ess = prepared
304        .profile()
305        .signing()
306        .bind_certificate(provider, certificate, parent)
307        .map_err(BindError::Ess)?;
308    let restrictions = restrictions(prepared.profile().signing(), path, provider, parent)?;
309    Ok(SignerBinding {
310        prepared,
311        path,
312        certificate,
313        identifier,
314        ess,
315        key_matches,
316        included,
317        mathematical,
318        restrictions,
319    })
320}
321
322pub(crate) fn restrictions<'a, P: AsnDigest<'a>>(
323    signing: &structured_cms::ess::SigningAttributes<'a>,
324    path: Option<&CheckedPath<'_>>,
325    provider: &'a P,
326    parent: &Reader<'a>,
327) -> Result<PathRestriction, BindError<P::Error>> {
328    let mut result = PathRestriction::Unstated;
329    for set in [signing.v1(), signing.v2()].into_iter().flatten() {
330        parent.charge(1).map_err(|cause| {
331            BindError::Processing(Error::encoding(cause, "cades.ess.restrictions"))
332        })?;
333        if set.certificates().len() <= 1 {
334            continue;
335        }
336        if !set.faults().is_empty() {
337            return Ok(PathRestriction::Unproven);
338        }
339        let Some(path) = path else {
340            return Ok(PathRestriction::PathUnavailable);
341        };
342        result = PathRestriction::Matches;
343        // RFC5035 §§3/5 constrains certificates actually used, not a demand
344        // that every signed identifier occur on this one path (attribute
345        // certificates may also be named). Do not impose proposed erratum6566.
346        // https://www.rfc-editor.org/rfc/rfc5035#section-3
347        let Some((_, authorities)) = path.members().split_last() else {
348            return Err(BindError::Processing(Error::Foreign {
349                at: set.element().span().start,
350            }));
351        };
352        // RFC5280 §6.1 excludes even a certificate-backed trust anchor from
353        // the prospective path. Its separate material-presence obligation
354        // does not make it an ESS-restricted path member.
355        // https://www.rfc-editor.org/rfc/rfc5280#section-6.1
356        for certificate in authorities.iter().map(|member| member.certificate()) {
357            let answers = set
358                .bind_validation_certificate(provider, certificate, parent)
359                .map_err(BindError::Ess)?;
360            let mut matched = false;
361            let mut unsupported = false;
362            for answer in &answers {
363                parent.charge(1).map_err(|cause| {
364                    BindError::Processing(Error::encoding(cause, "cades.ess.restrictions"))
365                })?;
366                if *answer == Binding::Matches {
367                    matched = true;
368                }
369                if *answer == Binding::Unsupported {
370                    unsupported = true;
371                }
372            }
373            if !matched {
374                return Ok(if unsupported {
375                    PathRestriction::Unproven
376                } else {
377                    PathRestriction::Differs
378                });
379            }
380        }
381    }
382    Ok(result)
383}
384
385pub(crate) fn ess_matches(bindings: Bindings) -> bool {
386    (bindings.v1.is_some() || bindings.v2.is_some())
387        && bindings.v1.is_none_or(|value| value == Binding::Matches)
388        && bindings.v2.is_none_or(|value| value == Binding::Matches)
389}
390
391pub(crate) fn exact(
392    left: &[u8],
393    right: &[u8],
394    work: &impl Work,
395) -> Result<bool, structured_asn1::memory::MemoryError> {
396    work.charge_work(1)?;
397    if core::ptr::eq(left, right) {
398        return Ok(true);
399    }
400    if left.len() != right.len() {
401        return Ok(false);
402    }
403    work.charge_work(left.len() as u64)?;
404    Ok(left == right)
405}
406
407fn same_parameters(
408    left: Option<&[u8]>,
409    right: Option<&[u8]>,
410    work: &impl Work,
411) -> Result<bool, structured_asn1::memory::MemoryError> {
412    work.charge_work(1)?;
413    match (left, right) {
414        (None, None) => Ok(true),
415        (Some(left), Some(right)) => exact(left, right, work),
416        _ => Ok(false),
417    }
418}