Skip to main content

structured_pkix/ocsp/
digest.rs

1//! OCSP identity hashes through the shared digest contract.
2
3use super::CertId;
4use super::ResponderId;
5use crate::Certificate;
6use crate::algorithm::digest::{self, Algorithm, AsnDigest};
7use crate::error::{CallbackError, CallbackResult, Error, ErrorCode};
8use structured_asn1::digest::{OnceError, Resolution, matches_once};
9use structured_asn1::memory::{MemoryError, Work};
10
11// A provider output of the wrong width is located at the selector it hashed for.
12fn once<E>(error: OnceError<E>, field: &'static str, at: u64) -> CallbackError<E> {
13    match error {
14        OnceError::Provider(error) => CallbackError::Callback(error),
15        OnceError::OutputWidth { .. } => {
16            CallbackError::Processing(Error::new(ErrorCode::DigestOutput, field, at))
17        }
18    }
19}
20
21/// Identity comparison is not responder authentication or a certificate verdict.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CertMatch {
24    /// Serial, issuer name and both original-byte hashes match.
25    Matches,
26    /// A completed semantic or digest comparison found different identity.
27    Differs,
28    /// The advertised fixed hash width contradicts a carried hash value.
29    MalformedHashWidth,
30    /// The encoded hash identifier writes parameters its algorithm does not take.
31    MalformedHashAlgorithm,
32    /// The consumer does not support the encoded hash algorithm.
33    UnsupportedHash,
34}
35
36/// Responder identity alone, without signature, authority or status acceptance.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ResponderMatch {
39    /// Subject Name or imposed SHA-1 public-key hash identifies this certificate.
40    Matches,
41    /// Completed name or complete digest comparison differs.
42    Differs,
43    /// Written hash or provider-advertised SHA-1 width contradicts twenty octets.
44    MalformedHashWidth,
45    /// Protocol-imposed SHA-1 is unsupported by the selected provider/policy.
46    UnsupportedHash,
47}
48
49impl ResponderId<'_> {
50    /// Compare an original responder selector with the actual signer certificate.
51    /// RFC6960 §4.2.1 hashes subjectPublicKey contents with SHA-1, excluding
52    /// BIT STRING tag, length and unused-bits count. Name comparison reuses the
53    /// shared PKIX Name kernel. No signature recheck or standing-key fabrication.
54    /// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.1>
55    ///
56    /// `field` and `at` identify the real enclosing selector in the caller's
57    /// source; callbacks preserve their own original processing/resource cause.
58    pub fn matches_certificate<'d, D: AsnDigest<'d> + ?Sized, E: Work>(
59        &self,
60        certificate: &Certificate<'_>,
61        digest: &'d D,
62        execution: &E,
63        field: &'static str,
64        at: u64,
65    ) -> CallbackResult<ResponderMatch, D::Error> {
66        execution
67            .charge_work(1)
68            .map_err(|reason| Error::resource(reason, field, at))?;
69        let same = match self {
70            Self::Name(name) => name.matches(certificate.tbs().subject(), execution)?,
71            Self::KeyHash(expected) => {
72                if expected.len() != Algorithm::Sha1.width() {
73                    return Ok(ResponderMatch::MalformedHashWidth);
74                }
75                let Resolution::Supported(identity) = digest::supported(digest, Algorithm::Sha1)
76                else {
77                    return Ok(ResponderMatch::UnsupportedHash);
78                };
79                if digest
80                    .width(&identity)
81                    .is_some_and(|width| width != Algorithm::Sha1.width())
82                {
83                    return Ok(ResponderMatch::MalformedHashWidth);
84                }
85                let input = certificate.tbs().public_key().key().as_bytes();
86                match matches_once(digest, &identity, input, expected, execution)
87                    .map_err(|error| once(error, field, at))?
88                {
89                    Resolution::Supported(same) => same,
90                    Resolution::Unsupported | Resolution::Malformed => {
91                        return Ok(ResponderMatch::UnsupportedHash);
92                    }
93                }
94            }
95        };
96        Ok(if same {
97            ResponderMatch::Matches
98        } else {
99            ResponderMatch::Differs
100        })
101    }
102}
103
104impl CertId<'_> {
105    /// Match the actual target and candidate issuer without re-encoding names.
106    /// RFC6960 §4.1.1 hashes original target issuer DER and issuer public-key
107    /// contents. The caller retains their authenticated issuer/path provenance;
108    /// matching hashes does not verify the target certificate's signature.
109    /// <https://www.rfc-editor.org/rfc/rfc6960#section-4.1.1>
110    pub fn matches<'d, D: AsnDigest<'d> + ?Sized, E: Work>(
111        &self,
112        target: &Certificate<'_>,
113        issuer: &Certificate<'_>,
114        digest: &'d D,
115        execution: &E,
116    ) -> CallbackResult<CertMatch, D::Error> {
117        const FIELD: &str = "ocspResponse.certID";
118        let at = self.span.start;
119        let left = self.serial.as_bytes();
120        let right = target.tbs().serial().as_bytes();
121        let work = u64::try_from(left.len().max(right.len()))
122            .ok()
123            .and_then(|n| n.checked_mul(2))
124            .and_then(|n| n.checked_add(1))
125            .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, FIELD, at))?;
126        execution
127            .charge_work(work)
128            .map_err(|reason| Error::resource(reason, FIELD, at))?;
129        if left != right {
130            return Ok(CertMatch::Differs);
131        }
132        if !target
133            .tbs()
134            .issuer()
135            .matches(issuer.tbs().subject(), execution)?
136        {
137            return Ok(CertMatch::Differs);
138        }
139        let identity = match digest::resolve(digest, &self.algorithm, execution)? {
140            Resolution::Supported(identity) => identity,
141            Resolution::Unsupported => return Ok(CertMatch::UnsupportedHash),
142            Resolution::Malformed => return Ok(CertMatch::MalformedHashAlgorithm),
143        };
144        // A malformed known width is classified before either hashing callback.
145        if let Some(width) = digest.width(&identity)
146            && (self.issuer_name_hash.len() != width || self.issuer_key_hash.len() != width)
147        {
148            return Ok(CertMatch::MalformedHashWidth);
149        }
150        for (input, expected) in [
151            (target.tbs().issuer().as_bytes(), self.issuer_name_hash),
152            (
153                issuer.tbs().public_key().key().as_bytes(),
154                self.issuer_key_hash,
155            ),
156        ] {
157            match matches_once(digest, &identity, input, expected, execution)
158                .map_err(|error| once(error, FIELD, at))?
159            {
160                Resolution::Supported(true) => {}
161                Resolution::Supported(false) => return Ok(CertMatch::Differs),
162                Resolution::Unsupported | Resolution::Malformed => {
163                    return Ok(CertMatch::UnsupportedHash);
164                }
165            }
166        }
167        Ok(CertMatch::Matches)
168    }
169}
170
171#[cfg(test)]
172mod tests;