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::{DigestProvider, Resolution};
6use crate::stream::RangesError;
7use crate::{Error, Octets};
8use structured_asn1::memory::{Execution, OwnedVec, Work};
9use structured_asn1::{Reader, Span};
10use structured_pkix::Certificate;
11
12/// Agreement of one ESS first identifier with a caller-selected certificate.
13/// None of these answers independently establishes signature/path trust or
14/// erases the parsed attribute's conformance/restriction diagnostics.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Binding {
17    /// Whole-certificate hash and the optional readable issuer/serial agree.
18    Matches,
19    /// Computed whole-certificate hash does not agree.
20    HashDiffers,
21    /// Hash agrees but readable issuer/serial identifies something else.
22    IssuerDiffers,
23    /// Written issuer/serial is unreadable; hash agreement is insufficient.
24    IssuerUnreadable,
25    /// Known digest width cannot produce the declared certificate hash.
26    MalformedHash,
27    /// Selected provider rejects the written algorithm's parameter syntax.
28    MalformedAlgorithm,
29    /// The selected provider cannot compute this explicit mechanism.
30    Unsupported,
31}
32
33/// Both independent answers; absence does not imply a successful binding.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Bindings {
36    /// V1 answer when that signed attribute existed.
37    pub v1: Option<Binding>,
38    /// V2 answer when that signed attribute existed.
39    pub v2: Option<Binding>,
40}
41
42/// Resource, source-pass and provider failures remain outside semantic answers.
43#[derive(Debug)]
44pub enum BindingError<E> {
45    /// Original common processing/resource refusal and its source location.
46    Processing(Error),
47    /// Original resolver/comparison failure, including backend resources.
48    Callback {
49        /// Original enclosing ESS field whose callback refused.
50        field: &'static str,
51        /// Real original identifier span; no offset is synthesized.
52        span: Span,
53        /// Original selected-provider cause.
54        source: E,
55    },
56    /// Digest preparation/refusal or provider output contract failure.
57    Digest {
58        /// Original whole-certificate range being processed.
59        span: Span,
60        /// Common digest/provider cause, independently of document conformance.
61        source: DigestError<E>,
62    },
63    /// Incomplete/failed certificate pass; no successful partial output.
64    Pass {
65        /// Original whole-certificate range supplied to the failed pass.
66        span: Span,
67        /// Original source/common/provider cause.
68        source: PassError<RangesError, E>,
69    },
70    /// An accepted request is absent from a completed common digest memo.
71    /// This is an internal processing contract failure, not document syntax
72    /// or a provider's negative cryptographic answer.
73    LostRequest(usize),
74}
75
76enum Expected<'a, W: Work> {
77    Borrowed(&'a [u8]),
78    Joined(OwnedVec<u8, W>),
79}
80
81impl<W: Work> Expected<'_, W> {
82    fn as_bytes(&self) -> &[u8] {
83        match self {
84            Self::Borrowed(bytes) => bytes,
85            Self::Joined(bytes) => bytes,
86        }
87    }
88}
89
90struct Queued<'s, 'a, W: Work> {
91    identifier: &'s CertificateId<'a>,
92    expected: Expected<'a, W>,
93    ordinal: usize,
94}
95
96enum Request<I, H> {
97    Answer(Binding),
98    Compute { identity: I, expected: H },
99}
100
101type PreparedRequest<'a, 'p, P> = Result<
102    Request<<P as DigestProvider<'a>>::Identity, Expected<'a, Execution<'p>>>,
103    BindingError<<P as DigestProvider<'a>>::Error>,
104>;
105
106impl<'a> SigningAttributes<'a> {
107    /// Checks both first identifiers over one complete original DER certificate.
108    ///
109    /// Resolution and known-width checks precede every state/input side effect.
110    /// Semantic digest identities are deduplicated through the selected provider;
111    /// neither digest states nor output identities are cloned. Each version keeps
112    /// its own outcome, while the target issuer is prepared at most once. Later
113    /// identifiers and policy terms remain signed validation-set restrictions in
114    /// this object; the caller must enforce them and signature/path/time policy.
115    pub fn bind_certificate<P: DigestProvider<'a>>(
116        &self,
117        provider: &'a P,
118        certificate: &Certificate<'_>,
119        parent: &Reader<'a>,
120    ) -> Result<Bindings, BindingError<P::Error>> {
121        let execution = parent.execution();
122        let identifiers = [
123            self.v1().map(super::SigningCertificate::signer),
124            self.v2().map(super::SigningCertificate::signer),
125        ];
126        let mut answers = [None, None];
127        let mut queued = [None, None];
128        let mut identities = None;
129        for (index, identifier) in identifiers.iter().enumerate() {
130            let Some(identifier) = identifier else {
131                continue;
132            };
133            let at = identifier.element().span().start;
134            let field = identifier.field;
135            match prepare_request(provider, identifier, parent)? {
136                Request::Answer(answer) => answers[index] = Some(answer),
137                Request::Compute { identity, expected } => {
138                    let identities = match identities.as_mut() {
139                        Some(identities) => identities,
140                        None => identities.insert(execution.vector()),
141                    };
142                    let ordinal = identities.len();
143                    identities.try_push(identity).map_err(|reason| {
144                        BindingError::Processing(Error::resource(reason, field, at))
145                    })?;
146                    queued[index] = Some(Queued {
147                        identifier,
148                        expected,
149                        ordinal,
150                    });
151                }
152            }
153        }
154        let Some(identities) = identities else {
155            return Ok(Bindings {
156                v1: answers[0],
157                v2: answers[1],
158            });
159        };
160        let source_at = certificate.span().start;
161        let batch = DigestBatch::prepare(
162            provider,
163            identities.into_owned(),
164            &execution,
165            "ess.certificate",
166            source_at,
167        )
168        .map_err(|source| BindingError::Digest {
169            span: certificate.span(),
170            source,
171        })?;
172        let mut ranges = [certificate.as_bytes()];
173        let memo = batch
174            .run(&mut ranges, &execution)
175            .map_err(|source| BindingError::Pass {
176                span: certificate.span(),
177                source,
178            })?;
179        let mut issuer = None;
180        for (index, request) in queued.into_iter().enumerate() {
181            let Some(request) = request else {
182                continue;
183            };
184            let Some(digest) = memo
185                .for_request(request.ordinal, &execution)
186                .map_err(|source| BindingError::Digest {
187                    span: certificate.span(),
188                    source,
189                })?
190            else {
191                return Err(BindingError::LostRequest(request.ordinal));
192            };
193            answers[index] = Some(compare_binding(
194                provider,
195                request.identifier,
196                request.expected.as_bytes(),
197                digest.prehash(),
198                certificate,
199                parent,
200                &mut issuer,
201            )?);
202        }
203        Ok(Bindings {
204            v1: answers[0],
205            v2: answers[1],
206        })
207    }
208}
209
210fn prepare_request<'a, 'p, P: DigestProvider<'a>>(
211    provider: &'a P,
212    identifier: &CertificateId<'a>,
213    parent: &'p Reader<'a>,
214) -> PreparedRequest<'a, 'p, P> {
215    crate::provider::require_instance(
216        provider,
217        identifier.field,
218        identifier.element().span().start,
219    )
220    .map_err(BindingError::Processing)?;
221    let execution = parent.execution();
222    let at = identifier.element().span().start;
223    let field = identifier.field;
224    execution
225        .charge_work(1)
226        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
227    let resolved = provider
228        .resolve_digest(identifier.digest_algorithm(), parent.encoding(), parent)
229        .map_err(|source| BindingError::Callback {
230            field,
231            span: identifier.element().span(),
232            source,
233        })?;
234    match resolved {
235        Resolution::Unsupported => Ok(Request::Answer(Binding::Unsupported)),
236        Resolution::Malformed => Ok(Request::Answer(Binding::MalformedAlgorithm)),
237        Resolution::Supported(identity) => {
238            if let Some(width) = provider.digest_width(&identity)
239                && width != identifier.hashed().len()
240            {
241                return Ok(Request::Answer(Binding::MalformedHash));
242            }
243            // Only a constructed logical hash needs a joined buffer. Every
244            // declaration's admission finishes before any hash state starts.
245            let expected = prepare_expected(identifier.hashed(), &execution, field, at)?;
246            Ok(Request::Compute { identity, expected })
247        }
248    }
249}
250
251fn prepare_expected<'a, W: Work, E>(
252    octets: &Octets<'a>,
253    execution: &W,
254    field: &'static str,
255    at: u64,
256) -> Result<Expected<'a, W>, BindingError<E>> {
257    if let Some(bytes) = octets.as_one() {
258        return Ok(Expected::Borrowed(bytes));
259    }
260    let mut joined = execution.vector();
261    joined
262        .try_reserve_exact(octets.len())
263        .map_err(|reason| BindingError::Processing(Error::resource(reason, field, at)))?;
264    for piece in octets.pieces() {
265        execution.charge_work(1).map_err(|reason| {
266            BindingError::Processing(Error::resource(reason, field, piece.span().start))
267        })?;
268        joined
269            .try_extend_from_slice(piece.as_bytes())
270            .map_err(|reason| {
271                BindingError::Processing(Error::resource(reason, field, piece.span().start))
272            })?;
273    }
274    Ok(Expected::Joined(joined))
275}
276
277fn compare_binding<'a, 'c, 'p, P: DigestProvider<'a>>(
278    provider: &P,
279    identifier: &CertificateId<'a>,
280    expected: &[u8],
281    digest: crate::provider::Prehash<'_, '_, P, P::Identity>,
282    certificate: &Certificate<'c>,
283    parent: &'p Reader<'_>,
284    issuer: &mut Option<structured_pkix::Ordered<'c, Execution<'p>>>,
285) -> Result<Binding, BindingError<P::Error>> {
286    // Variable-width identities still require complete declared/actual output
287    // agreement. A width mismatch is malformed, never an ordinary hash mismatch.
288    if expected.len() != digest.as_bytes().len() {
289        return Ok(Binding::MalformedHash);
290    }
291    let matches = provider
292        .matches_digest(digest, expected, &parent.execution())
293        .map_err(|source| BindingError::Callback {
294            field: identifier.field,
295            span: identifier.element().span(),
296            source,
297        })?;
298    if !matches {
299        return Ok(Binding::HashDiffers);
300    }
301    match identifier
302        .matches_issuer_with(certificate, parent, issuer)
303        .map_err(BindingError::Processing)?
304    {
305        IssuerAgreement::Unstated | IssuerAgreement::Matches => Ok(Binding::Matches),
306        IssuerAgreement::Differs => Ok(Binding::IssuerDiffers),
307        IssuerAgreement::Unreadable => Ok(Binding::IssuerUnreadable),
308    }
309}