Skip to main content

structured_cms/ess/
check.rs

1//! Selected-provider certificate hash and issuer facts, without trust claims.
2
3use super::{CertificateId, IssuerAgreement, SigningAttributes};
4use crate::digest::{DigestBatch, DigestError, PassError};
5use crate::provider::Finished;
6use crate::stream::RangesError;
7use crate::{Error, Octets};
8use structured_asn1::digest::{Digest, Resolution};
9use structured_asn1::memory::{Execution, OwnedVec, Work};
10use structured_asn1::{Reader, Span};
11use structured_pkix::Certificate;
12use structured_pkix::algorithm::digest::AsnDigest;
13
14/// Agreement of one ESS first identifier with a caller-selected certificate.
15/// None of these answers independently establishes signature/path trust or
16/// erases the parsed attribute's conformance/restriction diagnostics.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Binding {
19    /// Whole-certificate hash and the optional readable issuer/serial agree.
20    Matches,
21    /// Computed whole-certificate hash does not agree.
22    HashDiffers,
23    /// Hash agrees but readable issuer/serial identifies something else.
24    IssuerDiffers,
25    /// Written issuer/serial is unreadable; hash agreement is insufficient.
26    IssuerUnreadable,
27    /// Known digest width cannot produce the declared certificate hash.
28    MalformedHash,
29    /// Selected provider rejects the written algorithm's parameter syntax.
30    MalformedAlgorithm,
31    /// The selected provider cannot compute this explicit mechanism.
32    Unsupported,
33}
34
35/// Both independent answers; absence does not imply a successful binding.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Bindings {
38    /// V1 answer when that signed attribute existed.
39    pub v1: Option<Binding>,
40    /// V2 answer when that signed attribute existed.
41    pub v2: Option<Binding>,
42}
43
44/// Resource, source-pass and provider failures remain outside semantic answers.
45#[derive(Debug)]
46pub enum BindingError<E> {
47    /// Original common processing/resource refusal and its source location.
48    Processing(Error),
49    /// Original comparison failure, including backend resources.
50    Callback {
51        /// Original enclosing ESS field whose callback refused.
52        field: &'static str,
53        /// Real original identifier span; no offset is synthesized.
54        span: Span,
55        /// Original selected-provider cause.
56        source: E,
57    },
58    /// Digest preparation/refusal or provider output contract failure.
59    Digest {
60        /// Original whole-certificate range being processed.
61        span: Span,
62        /// Common digest/provider cause, independently of document conformance.
63        source: DigestError<E>,
64    },
65    /// Incomplete/failed certificate pass; no successful partial output.
66    Pass {
67        /// Original whole-certificate range supplied to the failed pass.
68        span: Span,
69        /// Original source/common/provider cause.
70        source: PassError<RangesError, E>,
71    },
72    /// An accepted request is absent from a completed common digest memo.
73    /// This is an internal processing contract failure, not document syntax
74    /// or a provider's negative cryptographic answer.
75    LostRequest(usize),
76}
77
78enum Expected<'a, W: Work> {
79    Borrowed(&'a [u8]),
80    Joined(OwnedVec<u8, W>),
81}
82
83impl<W: Work> Expected<'_, W> {
84    fn as_bytes(&self) -> &[u8] {
85        match self {
86            Self::Borrowed(bytes) => bytes,
87            Self::Joined(bytes) => bytes,
88        }
89    }
90}
91
92struct Queued<'s, 'a, W: Work> {
93    identifier: &'s CertificateId<'a>,
94    expected: Expected<'a, W>,
95    ordinal: usize,
96}
97
98enum Request<I, H> {
99    Answer(Binding),
100    Compute { identity: I, expected: H },
101}
102
103type PreparedRequest<'a, 'p, P> = Result<
104    Request<<P as Digest<'a>>::Identity, Expected<'a, Execution<'p>>>,
105    BindingError<<P as Digest<'a>>::Error>,
106>;
107
108impl<'a> SigningAttributes<'a> {
109    /// Checks both first identifiers over one complete original DER certificate.
110    ///
111    /// Resolution and known-width checks precede every state/input side effect.
112    /// Semantic digest identities are deduplicated through the selected provider;
113    /// neither digest states nor output identities are cloned. Each version keeps
114    /// its own outcome, while the target issuer is prepared at most once. Later
115    /// identifiers and policy terms remain signed validation-set restrictions in
116    /// this object; the caller must enforce them and signature/path/time policy.
117    pub fn bind_certificate<P: AsnDigest<'a>>(
118        &self,
119        provider: &'a P,
120        certificate: &Certificate<'_>,
121        parent: &Reader<'a>,
122    ) -> Result<Bindings, BindingError<P::Error>> {
123        let current = parent.execution();
124        // Hashing revisits the certificate source, even when ESS omits its
125        // optional issuerSerial. Keep its producing ceiling and the current
126        // operation on the entire batch, including provider state/storage.
127        let execution = certificate.execution_in(&current);
128        let identifiers = [
129            self.v1().map(super::SigningCertificate::signer),
130            self.v2().map(super::SigningCertificate::signer),
131        ];
132        let mut answers = [None, None];
133        let mut queued = [None, None];
134        let mut identities = None;
135        for (index, identifier) in identifiers.iter().enumerate() {
136            let Some(identifier) = identifier else {
137                continue;
138            };
139            let at = identifier.element().span().start;
140            let field = identifier.field;
141            match prepare_request(provider, identifier, parent)? {
142                Request::Answer(answer) => answers[index] = Some(answer),
143                Request::Compute { identity, expected } => {
144                    let identities = match identities.as_mut() {
145                        Some(identities) => identities,
146                        None => identities.insert(execution.vector()),
147                    };
148                    let ordinal = identities.len();
149                    identities.try_push(identity).map_err(|reason| {
150                        BindingError::Processing(Error::resource(reason, field, at))
151                    })?;
152                    queued[index] = Some(Queued {
153                        identifier,
154                        expected,
155                        ordinal,
156                    });
157                }
158            }
159        }
160        let Some(identities) = identities else {
161            return Ok(Bindings {
162                v1: answers[0],
163                v2: answers[1],
164            });
165        };
166        let source_at = certificate.span().start;
167        let batch = DigestBatch::prepare(
168            provider,
169            identities.into_owned(),
170            &execution,
171            "ess.certificate",
172            source_at,
173        )
174        .map_err(|source| BindingError::Digest {
175            span: certificate.span(),
176            source,
177        })?;
178        let mut ranges = [certificate.as_bytes()];
179        let memo = batch
180            .run(&mut ranges, &execution)
181            .map_err(|source| BindingError::Pass {
182                span: certificate.span(),
183                source,
184            })?;
185        let mut issuer = None;
186        for (index, request) in queued.into_iter().enumerate() {
187            let Some(request) = request else {
188                continue;
189            };
190            let Some(digest) = memo
191                .for_request(request.ordinal, &execution)
192                .map_err(|source| BindingError::Digest {
193                    span: certificate.span(),
194                    source,
195                })?
196            else {
197                return Err(BindingError::LostRequest(request.ordinal));
198            };
199            answers[index] = Some(compare_binding(
200                provider,
201                request.identifier,
202                request.expected.as_bytes(),
203                digest.finished(),
204                certificate,
205                parent,
206                &mut issuer,
207            )?);
208        }
209        Ok(Bindings {
210            v1: answers[0],
211            v2: answers[1],
212        })
213    }
214}
215
216impl<'a> super::SigningCertificate<'a> {
217    /// Checks every later signed validation-set identifier against one actual
218    /// certificate. The first signer identifier is deliberately excluded.
219    ///
220    /// RFC5035 ยงยง3/5 limits certificates used during validation when later
221    /// identifiers exist. The format layer supplies actual path/credential
222    /// certificates; this operation neither builds a path nor selects trust.
223    /// All declarations are admitted before hashing. Resolved digest identities
224    /// share one complete certificate pass and one finalized output each.
225    /// <https://www.rfc-editor.org/rfc/rfc5035#section-3>
226    pub fn bind_validation_certificate<P: AsnDigest<'a>>(
227        &self,
228        provider: &'a P,
229        certificate: &Certificate<'_>,
230        parent: &Reader<'a>,
231    ) -> Result<OwnedVec<Binding>, BindingError<P::Error>> {
232        let current = parent.execution();
233        // Later validation-set identifiers hash the same original source as
234        // first-signer identifiers and must use the same execution bridge.
235        let execution = certificate.execution_in(&current);
236        let field = self.version.field();
237        let at = self.element.span().start;
238        let refusal = |reason| BindingError::Processing(Error::resource(reason, field, at));
239        let later = &self.identifiers()[1..];
240        let mut answers = execution.vector();
241        let mut queued = execution.vector();
242        let mut identities = execution.vector();
243        answers.try_reserve_exact(later.len()).map_err(refusal)?;
244        queued.try_reserve_exact(later.len()).map_err(refusal)?;
245        for identifier in later {
246            execution.charge_work(1).map_err(refusal)?;
247            match prepare_request(provider, identifier, parent)? {
248                Request::Answer(answer) => answers.try_push(answer).map_err(refusal)?,
249                Request::Compute { identity, expected } => {
250                    let ordinal = identities.len();
251                    identities.try_push(identity).map_err(refusal)?;
252                    queued
253                        .try_push((
254                            answers.len(),
255                            Queued {
256                                identifier,
257                                expected,
258                                ordinal,
259                            },
260                        ))
261                        .map_err(refusal)?;
262                    // Private pending slot: every queued request must finish
263                    // below or return Err. No pending catalogue is exposed.
264                    answers.try_push(Binding::Unsupported).map_err(refusal)?;
265                }
266            }
267        }
268        if !identities.is_empty() {
269            let batch =
270                DigestBatch::prepare(provider, identities.into_owned(), &execution, field, at)
271                    .map_err(|source| BindingError::Digest {
272                        span: certificate.span(),
273                        source,
274                    })?;
275            let mut ranges = [certificate.as_bytes()];
276            let memo = batch
277                .run(&mut ranges, &execution)
278                .map_err(|source| BindingError::Pass {
279                    span: certificate.span(),
280                    source,
281                })?;
282            let mut issuer = None;
283            for (slot, request) in queued {
284                execution.charge_work(1).map_err(refusal)?;
285                let Some(digest) =
286                    memo.for_request(request.ordinal, &execution)
287                        .map_err(|source| BindingError::Digest {
288                            span: certificate.span(),
289                            source,
290                        })?
291                else {
292                    return Err(BindingError::LostRequest(request.ordinal));
293                };
294                answers[slot] = compare_binding(
295                    provider,
296                    request.identifier,
297                    request.expected.as_bytes(),
298                    digest.finished(),
299                    certificate,
300                    parent,
301                    &mut issuer,
302                )?;
303            }
304        }
305        Ok(answers.into_owned())
306    }
307}
308
309fn prepare_request<'a, 'p, P: AsnDigest<'a>>(
310    provider: &'a P,
311    identifier: &CertificateId<'a>,
312    parent: &'p Reader<'a>,
313) -> PreparedRequest<'a, 'p, P> {
314    crate::provider::require_instance(
315        provider,
316        identifier.field,
317        identifier.element().span().start,
318    )
319    .map_err(BindingError::Processing)?;
320    let execution = parent.execution();
321    let at = identifier.element().span().start;
322    let field = identifier.field;
323    execution
324        .charge_work(1)
325        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
326    let resolved = crate::provider::resolve_digest(provider, identifier.digest_algorithm(), parent)
327        .map_err(BindingError::Processing)?;
328    match resolved {
329        Resolution::Unsupported => Ok(Request::Answer(Binding::Unsupported)),
330        Resolution::Malformed => Ok(Request::Answer(Binding::MalformedAlgorithm)),
331        Resolution::Supported(identity) => {
332            if let Some(width) = provider.width(&identity)
333                && width != identifier.hashed().len()
334            {
335                return Ok(Request::Answer(Binding::MalformedHash));
336            }
337            // Only a constructed logical hash needs a joined buffer. Every
338            // declaration's admission finishes before any hash state starts.
339            let expected = prepare_expected(identifier.hashed(), &execution, field, at)?;
340            Ok(Request::Compute { identity, expected })
341        }
342    }
343}
344
345fn prepare_expected<'a, W: Work, E>(
346    octets: &Octets<'a>,
347    execution: &W,
348    field: &'static str,
349    at: u64,
350) -> Result<Expected<'a, W>, BindingError<E>> {
351    if let Some(bytes) = octets.as_one() {
352        return Ok(Expected::Borrowed(bytes));
353    }
354    let mut joined = execution.vector();
355    joined
356        .try_reserve_exact(octets.len())
357        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
358    for piece in octets.pieces() {
359        execution.charge_work(1).map_err(|reason| {
360            BindingError::Processing(Error::resource(reason, field, piece.span().start))
361        })?;
362        joined
363            .try_extend_from_slice(piece.as_bytes())
364            .map_err(|reason| {
365                BindingError::Processing(Error::resource(reason, field, piece.span().start))
366            })?;
367    }
368    Ok(Expected::Joined(joined))
369}
370
371fn compare_binding<'a, 'c, 'p, P: Digest<'a>>(
372    provider: &P,
373    identifier: &CertificateId<'a>,
374    expected: &[u8],
375    digest: Finished<'_, P, P::Identity, P::Output>,
376    certificate: &Certificate<'c>,
377    parent: &'p Reader<'_>,
378    issuer: &mut Option<structured_pkix::Ordered<'c, Execution<'p>>>,
379) -> Result<Binding, BindingError<P::Error>> {
380    // Variable-width identities still require complete declared/actual output
381    // agreement. A width mismatch is malformed, never an ordinary hash mismatch.
382    if expected.len() != digest.output().as_ref().len() {
383        return Ok(Binding::MalformedHash);
384    }
385    let matches = provider
386        .matches(
387            digest.identity(),
388            digest.output(),
389            expected,
390            &parent.execution(),
391        )
392        .map_err(|source| BindingError::Callback {
393            field: identifier.field,
394            span: identifier.element().span(),
395            source,
396        })?;
397    if !matches {
398        return Ok(Binding::HashDiffers);
399    }
400    match identifier
401        .matches_issuer_with(certificate, parent, issuer)
402        .map_err(BindingError::Processing)?
403    {
404        IssuerAgreement::Unstated | IssuerAgreement::Matches => Ok(Binding::Matches),
405        IssuerAgreement::Differs => Ok(Binding::IssuerDiffers),
406        IssuerAgreement::Unreadable => Ok(Binding::IssuerUnreadable),
407    }
408}