Skip to main content

structured_cms/attribute_certificate/validation/
digest.rs

1//! Explicit object-digest Holder binding (RFC5755 §7.3), not authorization.
2//!
3//! A key assertion covers DER effective `SubjectPublicKeyInfo`, not just key bits
4//! or the subject's parameter-less SPKI. A certificate assertion covers the
5//! entire original DER Certificate, including its signature. Input selection is
6//! explicit; other Holder alternatives are never tried as a fallback.
7//! <https://www.rfc-editor.org/rfc/rfc5755#section-7.3>
8
9use super::super::{AttributeCertificate, DigestedObject};
10use crate::Error;
11use structured_asn1::digest::Resolution;
12use structured_asn1::memory::Work;
13use structured_pkix::Certificate;
14use structured_pkix::algorithm::digest::{self as algorithms, AsnDigest};
15use structured_pkix::key::effective::{EffectiveSpki, VisitError};
16use structured_pkix::path::WorkingKey;
17
18const FIELD: &str = "attributeCertificate.holder.objectDigestInfo";
19
20/// The selected digest provider's representation check, before key hashing.
21///
22/// It must validate the original key and effective parameters against its
23/// algorithm's encoding rules (RFC3279 §2.3 and applicable updates, e.g.
24/// RFC5480), using the existing shared key readers. DER framing alone does not
25/// validate RSA/DSA/EC/DH/KEA payloads or repair invalid written parameters.
26/// Unsupported algorithms/policy remain Unsupported, never a digest mismatch.
27/// This is the same provider's narrow capability, not an implicit engine.
28/// All transitive work/storage is admitted in the supplied execution; key
29/// mathematics, strength and authenticated proof-of-possession remain separate.
30pub trait ObjectDigestProvider<'p>: AsnDigest<'p> {
31    /// Check algorithm-specific wire representation and effective parameters.
32    fn validate_public_key<W: Work>(
33        &self,
34        key: WorkingKey<'_>,
35        work: &W,
36    ) -> Result<Resolution<()>, Self::Error>;
37}
38
39/// The caller's explicit object. A key need not have a public-key certificate.
40#[derive(Debug, Clone, Copy)]
41pub enum Object<'c, 'a> {
42    /// Entire original DER certificate, including its signature.
43    Certificate(&'c Certificate<'a>),
44    /// Effective public key, including any inherited parameter source.
45    PublicKey(WorkingKey<'a>),
46}
47
48/// A hash comparison bound to this original AC and this explicitly supplied object.
49/// No certificate trust, key possession or authorization follows from it.
50#[derive(Debug)]
51pub struct BoundObject<'r, 'a, 's> {
52    certificate: &'r AttributeCertificate<'a>,
53    object: Object<'r, 's>,
54}
55impl<'r, 'a, 's> BoundObject<'r, 'a, 's> {
56    /// Original AC whose Holder assertion was checked.
57    #[must_use]
58    pub fn certificate(&self) -> &'r AttributeCertificate<'a> {
59        self.certificate
60    }
61    /// Actual caller-selected object covered by the completed comparison.
62    #[must_use]
63    pub fn object(&self) -> Object<'r, 's> {
64        self.object
65    }
66}
67
68/// A completed semantic refusal; processing and callback failures use Result.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Failure {
71    /// No objectDigestInfo Holder alternative was written.
72    Absent,
73    /// Written object choice differs from the caller's explicit object.
74    ObjectType,
75    /// otherObjectTypes or otherObjectTypeID is forbidden by this profile.
76    OtherObject,
77    /// Known digest algorithm has invalid parameters.
78    Algorithm,
79    /// Digest bits have padding or the wrong fixed algorithm width.
80    Hash,
81    /// Provider rejected the key's algorithm-specific representation.
82    KeyEncoding,
83    /// Completed digest comparison found different bytes.
84    Differs,
85}
86
87/// A capability the selected provider cannot supply.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Capability {
90    /// Written hash is unsupported or refused by selected policy.
91    Digest,
92    /// Key representation cannot be supplied by this provider/policy.
93    Key,
94}
95
96/// Bound facts, semantic refusal and unavailable capability remain distinct.
97#[derive(Debug)]
98pub enum Binding<'r, 'a, 's> {
99    /// Complete successful comparison of the chosen object.
100    Bound(BoundObject<'r, 'a, 's>),
101    /// Complete profile/comparison refusal, without fallback.
102    Rejected(Failure),
103    /// An operation was not supported; no comparison is asserted.
104    Unsupported(Capability),
105}
106
107/// Where a selected-provider operation failed.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Phase {
110    /// Algorithm-specific key representation gate.
111    KeyEncoding,
112    /// Digest state initialization.
113    Start,
114    /// Whole original certificate input.
115    CertificateInput,
116    /// Digest state finalization.
117    Finish,
118    /// Finished output versus written digest.
119    Comparison,
120}
121
122/// Original execution failures and provider contract failures, never mismatch.
123#[derive(Debug)]
124pub enum BindingError<E> {
125    /// Located source/current processing failure.
126    Processing(Error),
127    /// Original provider cause and the operation that failed.
128    Callback {
129        /// Operation that failed.
130        phase: Phase,
131        /// Original provider cause.
132        source: E,
133    },
134    /// Effective key traversal or its original callback cause/part.
135    KeyInput(VisitError<E>),
136    /// Provider identity/output contradicts the standard's fixed output width.
137    OutputWidth {
138        /// Width imposed by the written digest algorithm.
139        expected: usize,
140        /// Width the provider advertised or produced.
141        actual: usize,
142    },
143}
144impl<E> From<Error> for BindingError<E> {
145    fn from(error: Error) -> Self {
146        Self::Processing(error)
147    }
148}
149
150impl<'a> AttributeCertificate<'a> {
151    /// Bind the written objectDigestInfo to one explicit object with one provider.
152    ///
153    /// Profile/object/algorithm/width gates precede hash state creation. The
154    /// public-key branch checks representation with the selected provider before
155    /// hashing canonical effective SPKI chunks. Certificate hashing includes its
156    /// signature. Every input callback receives its own source/current execution;
157    /// comparison receives the assertion's execution. States/output are consumed
158    /// once and dropped on any failure; no successful partial fact escapes.
159    pub fn bind_holder_digest<'r, 's, 'p, P: ObjectDigestProvider<'p>, W: Work>(
160        &'r self,
161        object: Object<'r, 's>,
162        provider: &'p P,
163        work: &W,
164    ) -> Result<Binding<'r, 'a, 's>, BindingError<P::Error>> {
165        let execution = work.execution_for(self.parser);
166        let at = self.signed_element().span().start;
167        execution
168            .charge_work(4)
169            .map_err(|e| Error::resource(e, FIELD, at))?;
170        let Some(assertion) = &self.information.holder.object_digest else {
171            return Ok(Binding::Rejected(Failure::Absent));
172        };
173        if assertion.object_type == DigestedObject::Other || assertion.other_type.is_some() {
174            return Ok(Binding::Rejected(Failure::OtherObject));
175        }
176        if !matches!(
177            (&assertion.object_type, object),
178            (DigestedObject::PublicKey, Object::PublicKey(_))
179                | (DigestedObject::PublicKeyCertificate, Object::Certificate(_))
180        ) {
181            return Ok(Binding::Rejected(Failure::ObjectType));
182        }
183        let algorithm =
184            match algorithms::algorithm(&assertion.algorithm, &execution).map_err(Error::pkix)? {
185                Resolution::Supported(value) => value,
186                Resolution::Unsupported => return Ok(Binding::Unsupported(Capability::Digest)),
187                Resolution::Malformed => return Ok(Binding::Rejected(Failure::Algorithm)),
188            };
189        let Some(expected) = assertion
190            .digest
191            .octets()
192            .filter(|bytes| bytes.len() == algorithm.width())
193        else {
194            return Ok(Binding::Rejected(Failure::Hash));
195        };
196        let Some(identity) = provider.identity(algorithm) else {
197            return Ok(Binding::Unsupported(Capability::Digest));
198        };
199        check_width(provider.width(&identity), algorithm.width())?;
200        let calculation = match prepare(object, provider, &execution, |input| {
201            hash(
202                input,
203                provider,
204                &identity,
205                algorithm.width(),
206                expected,
207                &execution,
208            )
209        })? {
210            Resolution::Supported(calculation) => calculation,
211            Resolution::Unsupported => return Ok(Binding::Unsupported(Capability::Key)),
212            Resolution::Malformed => return Ok(Binding::Rejected(Failure::KeyEncoding)),
213        };
214        let matches = match calculation {
215            Resolution::Supported(matches) => matches,
216            Resolution::Unsupported => return Ok(Binding::Unsupported(Capability::Digest)),
217            Resolution::Malformed => return Ok(Binding::Rejected(Failure::Algorithm)),
218        };
219        Ok(if matches {
220            Binding::Bound(BoundObject {
221                certificate: self,
222                object,
223            })
224        } else {
225            Binding::Rejected(Failure::Differs)
226        })
227    }
228}
229
230fn check_width<E>(actual: Option<usize>, expected: usize) -> Result<(), BindingError<E>> {
231    if let Some(actual) = actual
232        && actual != expected
233    {
234        return Err(BindingError::OutputWidth { expected, actual });
235    }
236    Ok(())
237}
238
239#[derive(Clone, Copy)]
240enum Input<'v, 's> {
241    Certificate(&'v Certificate<'s>),
242    Key(&'v EffectiveSpki<'s>),
243}
244
245fn prepare<'s, 'p, P: ObjectDigestProvider<'p>, W: Work, T>(
246    object: Object<'_, 's>,
247    provider: &'p P,
248    work: &W,
249    visit: impl FnOnce(Input<'_, 's>) -> Result<T, BindingError<P::Error>>,
250) -> Result<Resolution<T>, BindingError<P::Error>> {
251    let key = match object {
252        Object::Certificate(certificate) => {
253            return visit(Input::Certificate(certificate)).map(Resolution::Supported);
254        }
255        Object::PublicKey(key) => key,
256    };
257    let encoded = EffectiveSpki::read(key, work).map_err(Error::pkix)?;
258    Ok(
259        match provider
260            .validate_public_key(key, &encoded.execution_in(work))
261            .map_err(|source| BindingError::Callback {
262                phase: Phase::KeyEncoding,
263                source,
264            })? {
265            Resolution::Supported(()) => {
266                return visit(Input::Key(&encoded)).map(Resolution::Supported);
267            }
268            Resolution::Unsupported => Resolution::Unsupported,
269            Resolution::Malformed => Resolution::Malformed,
270        },
271    )
272}
273
274fn hash<'p, P: ObjectDigestProvider<'p>, W: Work>(
275    input: Input<'_, '_>,
276    provider: &'p P,
277    identity: &P::Identity,
278    width: usize,
279    expected: &[u8],
280    work: &W,
281) -> Result<Resolution<bool>, BindingError<P::Error>> {
282    let mut state =
283        match provider
284            .start(identity, work)
285            .map_err(|source| BindingError::Callback {
286                phase: Phase::Start,
287                source,
288            })? {
289            Resolution::Supported(state) => state,
290            Resolution::Unsupported => return Ok(Resolution::Unsupported),
291            Resolution::Malformed => return Ok(Resolution::Malformed),
292        };
293    match input {
294        Input::Certificate(certificate) => {
295            let execution = certificate.execution_in(work);
296            execution.charge_work(1).map_err(|e| {
297                Error::resource(
298                    e,
299                    "attributeCertificate.objectDigestInfo.certificateInput",
300                    certificate.span().start,
301                )
302            })?;
303            provider
304                .update(&mut state, certificate.as_bytes(), &execution)
305                .map_err(|source| BindingError::Callback {
306                    phase: Phase::CertificateInput,
307                    source,
308                })?;
309        }
310        Input::Key(encoded) => encoded
311            .visit(work, |_, bytes, source| {
312                provider.update(&mut state, bytes, source)
313            })
314            .map_err(BindingError::KeyInput)?,
315    }
316    let output = provider
317        .finish(state, work)
318        .map_err(|source| BindingError::Callback {
319            phase: Phase::Finish,
320            source,
321        })?;
322    check_width(Some(output.as_ref().len()), width)?;
323    provider
324        .matches(identity, &output, expected, work)
325        .map(Resolution::Supported)
326        .map_err(|source| BindingError::Callback {
327            phase: Phase::Comparison,
328            source,
329        })
330}