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
212fn prepare_request<'a, 'p, P: AsnDigest<'a>>(
213    provider: &'a P,
214    identifier: &CertificateId<'a>,
215    parent: &'p Reader<'a>,
216) -> PreparedRequest<'a, 'p, P> {
217    crate::provider::require_instance(
218        provider,
219        identifier.field,
220        identifier.element().span().start,
221    )
222    .map_err(BindingError::Processing)?;
223    let execution = parent.execution();
224    let at = identifier.element().span().start;
225    let field = identifier.field;
226    execution
227        .charge_work(1)
228        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
229    let resolved = crate::provider::resolve_digest(provider, identifier.digest_algorithm(), parent)
230        .map_err(BindingError::Processing)?;
231    match resolved {
232        Resolution::Unsupported => Ok(Request::Answer(Binding::Unsupported)),
233        Resolution::Malformed => Ok(Request::Answer(Binding::MalformedAlgorithm)),
234        Resolution::Supported(identity) => {
235            if let Some(width) = provider.width(&identity)
236                && width != identifier.hashed().len()
237            {
238                return Ok(Request::Answer(Binding::MalformedHash));
239            }
240            // Only a constructed logical hash needs a joined buffer. Every
241            // declaration's admission finishes before any hash state starts.
242            let expected = prepare_expected(identifier.hashed(), &execution, field, at)?;
243            Ok(Request::Compute { identity, expected })
244        }
245    }
246}
247
248fn prepare_expected<'a, W: Work, E>(
249    octets: &Octets<'a>,
250    execution: &W,
251    field: &'static str,
252    at: u64,
253) -> Result<Expected<'a, W>, BindingError<E>> {
254    if let Some(bytes) = octets.as_one() {
255        return Ok(Expected::Borrowed(bytes));
256    }
257    let mut joined = execution.vector();
258    joined
259        .try_reserve_exact(octets.len())
260        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
261    for piece in octets.pieces() {
262        execution.charge_work(1).map_err(|reason| {
263            BindingError::Processing(Error::resource(reason, field, piece.span().start))
264        })?;
265        joined
266            .try_extend_from_slice(piece.as_bytes())
267            .map_err(|reason| {
268                BindingError::Processing(Error::resource(reason, field, piece.span().start))
269            })?;
270    }
271    Ok(Expected::Joined(joined))
272}
273
274fn compare_binding<'a, 'c, 'p, P: Digest<'a>>(
275    provider: &P,
276    identifier: &CertificateId<'a>,
277    expected: &[u8],
278    digest: Finished<'_, P, P::Identity, P::Output>,
279    certificate: &Certificate<'c>,
280    parent: &'p Reader<'_>,
281    issuer: &mut Option<structured_pkix::Ordered<'c, Execution<'p>>>,
282) -> Result<Binding, BindingError<P::Error>> {
283    // Variable-width identities still require complete declared/actual output
284    // agreement. A width mismatch is malformed, never an ordinary hash mismatch.
285    if expected.len() != digest.output().as_ref().len() {
286        return Ok(Binding::MalformedHash);
287    }
288    let matches = provider
289        .matches(
290            digest.identity(),
291            digest.output(),
292            expected,
293            &parent.execution(),
294        )
295        .map_err(|source| BindingError::Callback {
296            field: identifier.field,
297            span: identifier.element().span(),
298            source,
299        })?;
300    if !matches {
301        return Ok(Binding::HashDiffers);
302    }
303    match identifier
304        .matches_issuer_with(certificate, parent, issuer)
305        .map_err(BindingError::Processing)?
306    {
307        IssuerAgreement::Unstated | IssuerAgreement::Matches => Ok(Binding::Matches),
308        IssuerAgreement::Differs => Ok(Binding::IssuerDiffers),
309        IssuerAgreement::Unreadable => Ok(Binding::IssuerUnreadable),
310    }
311}