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