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 execution = parent.execution();
124        let identifiers = [
125            self.v1().map(super::SigningCertificate::signer),
126            self.v2().map(super::SigningCertificate::signer),
127        ];
128        let mut answers = [None, None];
129        let mut queued = [None, None];
130        let mut identities = None;
131        for (index, identifier) in identifiers.iter().enumerate() {
132            let Some(identifier) = identifier else {
133                continue;
134            };
135            let at = identifier.element().span().start;
136            let field = identifier.field;
137            match prepare_request(provider, identifier, parent)? {
138                Request::Answer(answer) => answers[index] = Some(answer),
139                Request::Compute { identity, expected } => {
140                    let identities = match identities.as_mut() {
141                        Some(identities) => identities,
142                        None => identities.insert(execution.vector()),
143                    };
144                    let ordinal = identities.len();
145                    identities.try_push(identity).map_err(|reason| {
146                        BindingError::Processing(Error::resource(reason, field, at))
147                    })?;
148                    queued[index] = Some(Queued {
149                        identifier,
150                        expected,
151                        ordinal,
152                    });
153                }
154            }
155        }
156        let Some(identities) = identities else {
157            return Ok(Bindings {
158                v1: answers[0],
159                v2: answers[1],
160            });
161        };
162        let source_at = certificate.span().start;
163        let batch = DigestBatch::prepare(
164            provider,
165            identities.into_owned(),
166            &execution,
167            "ess.certificate",
168            source_at,
169        )
170        .map_err(|source| BindingError::Digest {
171            span: certificate.span(),
172            source,
173        })?;
174        let mut ranges = [certificate.as_bytes()];
175        let memo = batch
176            .run(&mut ranges, &execution)
177            .map_err(|source| BindingError::Pass {
178                span: certificate.span(),
179                source,
180            })?;
181        let mut issuer = None;
182        for (index, request) in queued.into_iter().enumerate() {
183            let Some(request) = request else {
184                continue;
185            };
186            let Some(digest) = memo
187                .for_request(request.ordinal, &execution)
188                .map_err(|source| BindingError::Digest {
189                    span: certificate.span(),
190                    source,
191                })?
192            else {
193                return Err(BindingError::LostRequest(request.ordinal));
194            };
195            answers[index] = Some(compare_binding(
196                provider,
197                request.identifier,
198                request.expected.as_bytes(),
199                digest.finished(),
200                certificate,
201                parent,
202                &mut issuer,
203            )?);
204        }
205        Ok(Bindings {
206            v1: answers[0],
207            v2: answers[1],
208        })
209    }
210}
211
212impl<'a> super::SigningCertificate<'a> {
213    /// Checks every later signed validation-set identifier against one actual
214    /// certificate. The first signer identifier is deliberately excluded.
215    ///
216    /// RFC5035 ยงยง3/5 limits certificates used during validation when later
217    /// identifiers exist. The format layer supplies actual path/credential
218    /// certificates; this operation neither builds a path nor selects trust.
219    /// All declarations are admitted before hashing. Resolved digest identities
220    /// share one complete certificate pass and one finalized output each.
221    /// <https://www.rfc-editor.org/rfc/rfc5035#section-3>
222    pub fn bind_validation_certificate<P: AsnDigest<'a>>(
223        &self,
224        provider: &'a P,
225        certificate: &Certificate<'_>,
226        parent: &Reader<'a>,
227    ) -> Result<OwnedVec<Binding>, BindingError<P::Error>> {
228        let execution = parent.execution();
229        let field = self.version.field();
230        let at = self.element.span().start;
231        let refusal = |reason| BindingError::Processing(Error::resource(reason, field, at));
232        let later = &self.identifiers()[1..];
233        let mut answers = execution.vector();
234        let mut queued = execution.vector();
235        let mut identities = execution.vector();
236        answers.try_reserve_exact(later.len()).map_err(refusal)?;
237        queued.try_reserve_exact(later.len()).map_err(refusal)?;
238        for identifier in later {
239            execution.charge_work(1).map_err(refusal)?;
240            match prepare_request(provider, identifier, parent)? {
241                Request::Answer(answer) => answers.try_push(answer).map_err(refusal)?,
242                Request::Compute { identity, expected } => {
243                    let ordinal = identities.len();
244                    identities.try_push(identity).map_err(refusal)?;
245                    queued
246                        .try_push((
247                            answers.len(),
248                            Queued {
249                                identifier,
250                                expected,
251                                ordinal,
252                            },
253                        ))
254                        .map_err(refusal)?;
255                    // Private pending slot: every queued request must finish
256                    // below or return Err. No pending catalogue is exposed.
257                    answers.try_push(Binding::Unsupported).map_err(refusal)?;
258                }
259            }
260        }
261        if !identities.is_empty() {
262            let batch =
263                DigestBatch::prepare(provider, identities.into_owned(), &execution, field, at)
264                    .map_err(|source| BindingError::Digest {
265                        span: certificate.span(),
266                        source,
267                    })?;
268            let mut ranges = [certificate.as_bytes()];
269            let memo = batch
270                .run(&mut ranges, &execution)
271                .map_err(|source| BindingError::Pass {
272                    span: certificate.span(),
273                    source,
274                })?;
275            let mut issuer = None;
276            for (slot, request) in queued {
277                execution.charge_work(1).map_err(refusal)?;
278                let Some(digest) =
279                    memo.for_request(request.ordinal, &execution)
280                        .map_err(|source| BindingError::Digest {
281                            span: certificate.span(),
282                            source,
283                        })?
284                else {
285                    return Err(BindingError::LostRequest(request.ordinal));
286                };
287                answers[slot] = compare_binding(
288                    provider,
289                    request.identifier,
290                    request.expected.as_bytes(),
291                    digest.finished(),
292                    certificate,
293                    parent,
294                    &mut issuer,
295                )?;
296            }
297        }
298        Ok(answers.into_owned())
299    }
300}
301
302fn prepare_request<'a, 'p, P: AsnDigest<'a>>(
303    provider: &'a P,
304    identifier: &CertificateId<'a>,
305    parent: &'p Reader<'a>,
306) -> PreparedRequest<'a, 'p, P> {
307    crate::provider::require_instance(
308        provider,
309        identifier.field,
310        identifier.element().span().start,
311    )
312    .map_err(BindingError::Processing)?;
313    let execution = parent.execution();
314    let at = identifier.element().span().start;
315    let field = identifier.field;
316    execution
317        .charge_work(1)
318        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
319    let resolved = crate::provider::resolve_digest(provider, identifier.digest_algorithm(), parent)
320        .map_err(BindingError::Processing)?;
321    match resolved {
322        Resolution::Unsupported => Ok(Request::Answer(Binding::Unsupported)),
323        Resolution::Malformed => Ok(Request::Answer(Binding::MalformedAlgorithm)),
324        Resolution::Supported(identity) => {
325            if let Some(width) = provider.width(&identity)
326                && width != identifier.hashed().len()
327            {
328                return Ok(Request::Answer(Binding::MalformedHash));
329            }
330            // Only a constructed logical hash needs a joined buffer. Every
331            // declaration's admission finishes before any hash state starts.
332            let expected = prepare_expected(identifier.hashed(), &execution, field, at)?;
333            Ok(Request::Compute { identity, expected })
334        }
335    }
336}
337
338fn prepare_expected<'a, W: Work, E>(
339    octets: &Octets<'a>,
340    execution: &W,
341    field: &'static str,
342    at: u64,
343) -> Result<Expected<'a, W>, BindingError<E>> {
344    if let Some(bytes) = octets.as_one() {
345        return Ok(Expected::Borrowed(bytes));
346    }
347    let mut joined = execution.vector();
348    joined
349        .try_reserve_exact(octets.len())
350        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
351    for piece in octets.pieces() {
352        execution.charge_work(1).map_err(|reason| {
353            BindingError::Processing(Error::resource(reason, field, piece.span().start))
354        })?;
355        joined
356            .try_extend_from_slice(piece.as_bytes())
357            .map_err(|reason| {
358                BindingError::Processing(Error::resource(reason, field, piece.span().start))
359            })?;
360    }
361    Ok(Expected::Joined(joined))
362}
363
364fn compare_binding<'a, 'c, 'p, P: Digest<'a>>(
365    provider: &P,
366    identifier: &CertificateId<'a>,
367    expected: &[u8],
368    digest: Finished<'_, P, P::Identity, P::Output>,
369    certificate: &Certificate<'c>,
370    parent: &'p Reader<'_>,
371    issuer: &mut Option<structured_pkix::Ordered<'c, Execution<'p>>>,
372) -> Result<Binding, BindingError<P::Error>> {
373    // Variable-width identities still require complete declared/actual output
374    // agreement. A width mismatch is malformed, never an ordinary hash mismatch.
375    if expected.len() != digest.output().as_ref().len() {
376        return Ok(Binding::MalformedHash);
377    }
378    let matches = provider
379        .matches(
380            digest.identity(),
381            digest.output(),
382            expected,
383            &parent.execution(),
384        )
385        .map_err(|source| BindingError::Callback {
386            field: identifier.field,
387            span: identifier.element().span(),
388            source,
389        })?;
390    if !matches {
391        return Ok(Binding::HashDiffers);
392    }
393    match identifier
394        .matches_issuer_with(certificate, parent, issuer)
395        .map_err(BindingError::Processing)?
396    {
397        IssuerAgreement::Unstated | IssuerAgreement::Matches => Ok(Binding::Matches),
398        IssuerAgreement::Differs => Ok(Binding::IssuerDiffers),
399        IssuerAgreement::Unreadable => Ok(Binding::IssuerUnreadable),
400    }
401}