Skip to main content

pkix_revocation/
ocsp.rs

1//! Offline OCSP-based revocation checker.
2//!
3//! Enabled by the `ocsp` feature.
4
5use crate::{Error, RevocationChecker};
6use der::{Decode as _, Encode as _};
7use pkix_path::{names_match, SignatureVerifier, TrustAnchor};
8use spki::der::referenced::OwnedToRef as _;
9use x509_cert::Certificate;
10use x509_ocsp::{BasicOcspResponse, CertStatus, OcspResponse, OcspResponseStatus, ResponderId};
11
12// OID 1.3.6.1.5.5.7.48.1.1 — id-pkix-ocsp-basic
13const OID_PKIX_OCSP_BASIC: der::asn1::ObjectIdentifier =
14    der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.1.1");
15
16// Hash algorithm OIDs used in CertID (RFC 6960 §4.1.1)
17const OID_SHA1: der::asn1::ObjectIdentifier =
18    der::asn1::ObjectIdentifier::new_unwrap("1.3.14.3.2.26");
19const OID_SHA256: der::asn1::ObjectIdentifier =
20    der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
21const OID_SHA384: der::asn1::ObjectIdentifier =
22    der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.2");
23const OID_SHA512: der::asn1::ObjectIdentifier =
24    der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.3");
25
26// OID 2.5.29.37 — id-ce-extKeyUsage (RFC 5280 §4.2.1.12)
27const OID_EXTENDED_KEY_USAGE: der::asn1::ObjectIdentifier =
28    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.37");
29
30// OID 1.3.6.1.5.5.7.3.9 — id-kp-OCSPSigning (RFC 6960 §4.2.2.2)
31const OID_KP_OCSP_SIGNING: der::asn1::ObjectIdentifier =
32    der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.9");
33
34/// Offline OCSP-based revocation checker.
35///
36/// Parses a pre-fetched DER-encoded OCSP response, verifies its signature
37/// against the issuer's SPKI, checks the validity window of the matching
38/// [`SingleResponse`][x509_ocsp::SingleResponse], and reports the certificate's
39/// revocation status.
40///
41/// # Feature
42///
43/// Only available when the `ocsp` feature is enabled.
44///
45/// # Supported responder shapes
46///
47/// - **Direct** (RFC 6960 §4.2.2.2): the response is signed by the cert's
48///   issuer CA. `ResponderId` matches the issuer's name or SHA-1(SPKI);
49///   the response signature verifies against the issuer's SPKI.
50/// - **CA Designated Responder** (RFC 6960 §4.2.2.2, "delegated"): the
51///   response is signed by a separate responder certificate embedded in
52///   the response's `certs` field. The responder cert MUST be issued
53///   directly by the same CA, MUST carry `id-kp-OCSPSigning` Extended Key
54///   Usage, MUST have a validity period containing the response's
55///   `producedAt`, and the issuer's signature on it MUST verify against
56///   the issuer's SPKI. Failures map to distinct error variants
57///   ([`Error::OcspResponderEkuMissing`],
58///   [`Error::OcspResponderEkuMalformed`],
59///   [`Error::OcspResponderCertNotIssuedByCa`],
60///   [`Error::OcspResponderCertExpired`],
61///   [`Error::OcspResponderCertSigInvalid`]).
62/// - The `id-pkix-ocsp-nocheck` extension on a delegate cert (RFC 6960
63///   §4.2.2.2.1) is **not** parsed by this crate: the checker is
64///   single-shot and never recurses into the delegate's revocation
65///   regardless of the extension. Callers wrapping this checker in a
66///   chain validator MUST honor `ocsp-nocheck` themselves to prevent
67///   infinite recursion.
68///
69/// # Limitations
70///
71/// - **Trusted Responder** (third RFC 6960 case — a responder whose key
72///   the requester trusts out-of-band) is not modeled. Callers needing
73///   it can supply the trusted responder cert as the `issuer` argument.
74/// - No OCSP request generation. The response DER must be supplied at
75///   construction time; the checker is offline. The OCSP `nonce` extension
76///   is therefore not generated or checked.
77/// - No AIA-based responder discovery (RFC 6960 §3.1). The
78///   `AuthorityInfoAccess` extension's `id-ad-ocsp` URL is not consulted —
79///   the caller is responsible for fetching the response out-of-band. See
80///   the planned `pkix-revocation-http` crate for online responder support.
81///
82/// # Behavior
83///
84/// - `SingleResponse` matching uses both serial number and the `CertID`
85///   `issuerNameHash`/`issuerKeyHash` fields (RFC 6960 §4.1.1). An OCSP
86///   response from a different CA with the same serial number will be rejected
87///   by the hash checks.
88/// - The `ResponderId` field is verified against the issuer identity per
89///   RFC 6960 §2.2: `byName` is compared against the issuer's subject DN using
90///   [`pkix_path::names_match`]; `byKey` is compared against SHA-1 of the
91///   issuer's SPKI `subjectPublicKey` bit string.
92/// - If no `SingleResponse` matches the certificate's serial number,
93///   `OcspStatusUnknown` is returned (hard-fail).
94/// - [`RevocationChecker::check_revocation_against_anchor`] is overridden.
95///   For the certificate issued directly by a trust anchor, the checker
96///   uses the anchor's subject DN and SPKI to verify the OCSP response.
97///   The response DER must be supplied at construction time; this method
98///   always attempts to verify it against the anchor.
99///
100/// [`check_revocation`]: crate::RevocationChecker::check_revocation
101/// [`RevocationChecker::check_revocation_against_anchor`]: crate::RevocationChecker::check_revocation_against_anchor
102#[derive(Clone, Debug)]
103pub struct OcspChecker<V> {
104    /// Pre-parsed `BasicOCSPResponse`, decoded once at construction time and
105    /// reused on every check. Signature verification still happens per-call
106    /// because the issuer SPKI varies between
107    /// [`RevocationChecker::check_revocation`] (issuer cert) and
108    /// [`RevocationChecker::check_revocation_against_anchor`] (anchor SPKI).
109    basic: BasicOcspResponse,
110    now_unix: u64,
111    verifier: V,
112}
113
114impl<V: SignatureVerifier> OcspChecker<V> {
115    /// Create a new `OcspChecker`.
116    ///
117    /// - `response_der` — DER-encoded `OCSPResponse` (any `AsRef<[u8]>`, e.g. `Vec<u8>` or `&[u8]`)
118    /// - `now_unix`     — current time as seconds since the Unix epoch
119    /// - `verifier`     — signature verifier used to authenticate the OCSP response
120    ///
121    /// The response is parsed once at construction time; subsequent
122    /// [`RevocationChecker::check_revocation`] calls reuse the cached
123    /// [`BasicOcspResponse`].
124    ///
125    /// # Errors
126    ///
127    /// Returns [`Error::OcspParseError`] if `response_der` cannot be DER-decoded
128    /// or [`Error::OcspMalformed`] if the response status is non-`Successful`,
129    /// `responseBytes` is absent, or the inner `responseType` is not
130    /// `id-pkix-ocsp-basic`.
131    pub fn new(response_der: impl AsRef<[u8]>, now_unix: u64, verifier: V) -> crate::Result<Self> {
132        let basic = parse_basic_response(response_der.as_ref())?;
133        Ok(Self {
134            basic,
135            now_unix,
136            verifier,
137        })
138    }
139}
140
141impl<V: SignatureVerifier> RevocationChecker for OcspChecker<V> {
142    fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()> {
143        // (0) Verify that `issuer` is actually the issuer of `cert`.
144        //
145        // Defense-in-depth: a caller could pass a mismatched `issuer` certificate
146        // whose key happens to verify the OCSP response signature, but which did
147        // not actually issue `cert`. Rejecting early prevents the downstream
148        // signature and CertID hash checks from operating on the wrong identity.
149        if !names_match(
150            &issuer.tbs_certificate.subject,
151            &cert.tbs_certificate.issuer,
152        ) {
153            return Err(Error::OcspIssuerCertMismatch);
154        }
155
156        // (1)-(5) parsing was performed in `new()`; reuse the cached
157        // `BasicOcspResponse`.
158        let basic = &self.basic;
159
160        // (6) RFC 6960 §4.2.2.2: resolve which key signs this response —
161        // either the issuer (direct) or a CA Designated Responder cert
162        // embedded in `basic.certs` (delegated). The resolver also
163        // performs the ResponderId match (step 6b in the legacy flow)
164        // because for delegated responses the ResponderId must match the
165        // delegate, not the issuer.
166        let issuer_subject = &issuer.tbs_certificate.subject;
167        let issuer_spki_owned = &issuer.tbs_certificate.subject_public_key_info;
168        let issuer_spki_raw = issuer_spki_owned.subject_public_key.raw_bytes();
169        let signing_spki = resolve_signing_key_for_response(
170            basic,
171            issuer_subject,
172            issuer_spki_owned.owned_to_ref(),
173            issuer_spki_raw,
174            &self.verifier,
175            produced_at_unix_secs(basic),
176        )?;
177
178        // (6 cont.) Verify the response signature using the resolved key.
179        let tbs_bytes = basic
180            .tbs_response_data
181            .to_der()
182            .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
183        self.verifier
184            .verify_signature(
185                basic.signature_algorithm.owned_to_ref(),
186                signing_spki,
187                &tbs_bytes,
188                basic.signature.raw_bytes(),
189            )
190            .map_err(|_| Error::OcspSignatureInvalid)?;
191
192        // (7) Find the SingleResponse for this certificate (match by serial number).
193        let cert_serial = &cert.tbs_certificate.serial_number;
194        let single = basic
195            .tbs_response_data
196            .responses
197            .iter()
198            .find(|r| &r.cert_id.serial_number == cert_serial)
199            .ok_or(Error::OcspStatusUnknown)?;
200
201        // (7a) Verify CertID issuer hashes (RFC 6960 §4.1.1).
202        //
203        // issuerNameHash = hash(DER(issuer.subject))
204        // issuerKeyHash  = hash(issuer.spki.subject_public_key.raw_bytes())
205        //
206        // Without this check a response produced for a cert with the same serial
207        // number issued by a *different* CA could pass serial-only matching.
208        let hash_oid = &single.cert_id.hash_algorithm.oid;
209        let name_der = issuer
210            .tbs_certificate
211            .subject
212            .to_der()
213            .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
214        let key_raw = issuer
215            .tbs_certificate
216            .subject_public_key_info
217            .subject_public_key
218            .raw_bytes();
219        let expected_name_hash = hash_certid_input(hash_oid, &name_der)?;
220        let expected_key_hash = hash_certid_input(hash_oid, key_raw)?;
221        if single.cert_id.issuer_name_hash.as_bytes() != expected_name_hash.as_slice()
222            || single.cert_id.issuer_key_hash.as_bytes() != expected_key_hash.as_slice()
223        {
224            // The response was produced for a certificate from a different CA;
225            // this is not a responder-reported "unknown" — it is an identity mismatch.
226            return Err(Error::OcspCertIdMismatch);
227        }
228
229        // (8) Check validity windows.
230        //
231        // producedAt must not be in the future.  A future-dated `producedAt` is
232        // structurally suspicious — a legitimate responder cannot claim to have
233        // produced a response after "now".  This is not a case of the responder
234        // saying "unknown"; it is a malformed or tampered response, so we return
235        // `OcspMalformed` rather than `OcspStatusUnknown`.
236        let produced_at = basic
237            .tbs_response_data
238            .produced_at
239            .as_ref()
240            .to_unix_duration()
241            .as_secs();
242        if self.now_unix < produced_at {
243            return Err(Error::OcspMalformed);
244        }
245        // thisUpdate ≤ now: the SingleResponse is not yet valid (stale clock or
246        // pre-dated response).  This is the same freshness failure as a past-due
247        // nextUpdate, so return `OcspExpired` for consistent caller semantics.
248        let this_update = single.this_update.as_ref().to_unix_duration().as_secs();
249        if self.now_unix < this_update {
250            return Err(Error::OcspExpired);
251        }
252        // now ≤ nextUpdate: absent nextUpdate is treated as stale (no freshness
253        // guarantee means we cannot rely on the response).
254        let next_update = single.next_update.as_ref().ok_or(Error::OcspExpired)?;
255        if self.now_unix > next_update.as_ref().to_unix_duration().as_secs() {
256            return Err(Error::OcspExpired);
257        }
258
259        // (9) Return based on certStatus.
260        match single.cert_status {
261            CertStatus::Good(_) => Ok(()),
262            CertStatus::Revoked(ref info) => Err(Error::Revoked {
263                serial: cert_serial.clone(),
264                reason_code: info.revocation_reason,
265            }),
266            CertStatus::Unknown(_) => Err(Error::OcspStatusUnknown),
267        }
268    }
269
270    /// Check revocation for `cert` issued directly by a trust anchor.
271    ///
272    /// Parses the pre-loaded OCSP response and verifies it against the anchor's
273    /// SPKI and subject DN.  The anchor fields (`subject` and
274    /// `subject_public_key_info`) are used in place of the missing issuer
275    /// `Certificate`.
276    ///
277    /// # Limitations
278    ///
279    /// OCSP responder discovery via the Authority Information Access extension
280    /// (RFC 6960 §3.1) is not implemented.  The response DER must be supplied
281    /// at construction time and is always verified.  If the serial number is
282    /// not found in the response, [`Error::OcspStatusUnknown`] is returned.
283    fn check_revocation_against_anchor(
284        &self,
285        cert: &Certificate,
286        anchor: &TrustAnchor,
287    ) -> crate::Result<()> {
288        // (0) Verify that the anchor is actually the issuer of `cert`.
289        //
290        // Defense-in-depth: guards against a caller passing an anchor whose SPKI
291        // happens to verify the OCSP response but which did not issue `cert`.
292        if !names_match(&anchor.subject, &cert.tbs_certificate.issuer) {
293            return Err(Error::OcspIssuerCertMismatch);
294        }
295
296        // (1)-(5) parsing was performed in `new()`; reuse the cached
297        // `BasicOcspResponse`.
298        let basic = &self.basic;
299
300        // (6) Resolve signing key (direct vs delegated) and verify the
301        // response signature. See `check_revocation` above for the full
302        // commentary on the two paths.
303        let anchor_subject = &anchor.subject;
304        let anchor_spki_raw = anchor.subject_public_key_info.subject_public_key.raw_bytes();
305        let signing_spki = resolve_signing_key_for_response(
306            basic,
307            anchor_subject,
308            anchor.subject_public_key_info.owned_to_ref(),
309            anchor_spki_raw,
310            &self.verifier,
311            produced_at_unix_secs(basic),
312        )?;
313
314        let tbs_bytes = basic
315            .tbs_response_data
316            .to_der()
317            .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
318        self.verifier
319            .verify_signature(
320                basic.signature_algorithm.owned_to_ref(),
321                signing_spki,
322                &tbs_bytes,
323                basic.signature.raw_bytes(),
324            )
325            .map_err(|_| Error::OcspSignatureInvalid)?;
326
327        // (7) Find the SingleResponse for this certificate.
328        let cert_serial = &cert.tbs_certificate.serial_number;
329        let single = basic
330            .tbs_response_data
331            .responses
332            .iter()
333            .find(|r| &r.cert_id.serial_number == cert_serial)
334            .ok_or(Error::OcspStatusUnknown)?;
335
336        // (7a) Verify CertID issuer hashes using the anchor's name/SPKI.
337        let hash_oid = &single.cert_id.hash_algorithm.oid;
338        let anchor_name_der = anchor
339            .subject
340            .to_der()
341            .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
342        let anchor_key_raw = anchor
343            .subject_public_key_info
344            .subject_public_key
345            .raw_bytes();
346        let expected_name_hash = hash_certid_input(hash_oid, &anchor_name_der)?;
347        let expected_key_hash = hash_certid_input(hash_oid, anchor_key_raw)?;
348        if single.cert_id.issuer_name_hash.as_bytes() != expected_name_hash.as_slice()
349            || single.cert_id.issuer_key_hash.as_bytes() != expected_key_hash.as_slice()
350        {
351            // Response covers a certificate from a different CA — identity mismatch,
352            // not a responder-reported "unknown".
353            return Err(Error::OcspCertIdMismatch);
354        }
355
356        // (8) Check validity windows.
357        //
358        // producedAt must not be in the future.  A future-dated `producedAt` is
359        // structurally suspicious — a legitimate responder cannot claim to have
360        // produced a response after "now".  Return `OcspMalformed` rather than
361        // `OcspStatusUnknown` because this is not a responder-reported "unknown"
362        // status but a structurally invalid response.
363        let produced_at = basic
364            .tbs_response_data
365            .produced_at
366            .as_ref()
367            .to_unix_duration()
368            .as_secs();
369        if self.now_unix < produced_at {
370            return Err(Error::OcspMalformed);
371        }
372        // thisUpdate ≤ now: same freshness failure as nextUpdate expired.
373        let this_update = single.this_update.as_ref().to_unix_duration().as_secs();
374        if self.now_unix < this_update {
375            return Err(Error::OcspExpired);
376        }
377        let next_update = single.next_update.as_ref().ok_or(Error::OcspExpired)?;
378        if self.now_unix > next_update.as_ref().to_unix_duration().as_secs() {
379            return Err(Error::OcspExpired);
380        }
381
382        // (9) Return based on certStatus.
383        match single.cert_status {
384            CertStatus::Good(_) => Ok(()),
385            CertStatus::Revoked(ref info) => Err(Error::Revoked {
386                serial: cert_serial.clone(),
387                reason_code: info.revocation_reason,
388            }),
389            CertStatus::Unknown(_) => Err(Error::OcspStatusUnknown),
390        }
391    }
392}
393
394/// Parse an `OCSPResponse` DER blob and extract its inner `BasicOCSPResponse`.
395///
396/// Performs steps (1)–(5) of the OCSP processing pipeline (decode outer
397/// `OCSPResponse`, require `Successful` status, extract `responseBytes`,
398/// verify `responseType`, decode inner `BasicOCSPResponse`). Signature
399/// verification is intentionally **not** performed here so the caller can
400/// supply the appropriate issuer SPKI per call.
401fn parse_basic_response(response_der: &[u8]) -> crate::Result<BasicOcspResponse> {
402    // (1) Parse the outer OCSPResponse.
403    let resp = OcspResponse::from_der(response_der)
404        .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
405
406    // (2) Require responseStatus == successful; any other (TryLater,
407    // InternalError, MalformedRequest, SigRequired, Unauthorized) → OcspMalformed.
408    // These are server-side error codes, not a responder-reported "unknown" status.
409    if resp.response_status != OcspResponseStatus::Successful {
410        return Err(Error::OcspMalformed);
411    }
412
413    // (3) Extract responseBytes (must be present for a Successful response).
414    let resp_bytes = resp.response_bytes.ok_or(Error::OcspMalformed)?;
415
416    // (4) Verify responseType is id-pkix-ocsp-basic.
417    if resp_bytes.response_type != OID_PKIX_OCSP_BASIC {
418        return Err(Error::OcspMalformed);
419    }
420
421    // (5) Parse the BasicOCSPResponse.
422    BasicOcspResponse::from_der(resp_bytes.response.as_bytes())
423        .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))
424}
425
426/// Stack-allocated hash output for `CertID` hash comparisons.
427///
428/// Holds the digest bytes for one of the four hash algorithms recognised in
429/// `CertID` (RFC 6960 §4.1.1), without heap allocation.
430enum HashOutput {
431    Sha1([u8; 20]),
432    Sha256([u8; 32]),
433    Sha384([u8; 48]),
434    Sha512([u8; 64]),
435}
436
437impl HashOutput {
438    const fn as_slice(&self) -> &[u8] {
439        match self {
440            Self::Sha1(b) => b,
441            Self::Sha256(b) => b,
442            Self::Sha384(b) => b,
443            Self::Sha512(b) => b,
444        }
445    }
446}
447
448/// Hash `data` using the algorithm identified by `oid`.
449///
450/// Supports SHA-1 (OID 1.3.14.3.2.26), SHA-256 (OID 2.16.840.1.101.3.4.2.1),
451/// SHA-384 (OID 2.16.840.1.101.3.4.2.2), and SHA-512 (OID 2.16.840.1.101.3.4.2.3).
452/// Returns [`Error::OcspMalformed`] for any other OID.
453fn hash_certid_input(oid: &der::asn1::ObjectIdentifier, data: &[u8]) -> crate::Result<HashOutput> {
454    // sha1 and sha2 both re-export `digest::Digest`; one import is sufficient.
455    use sha1::Digest as _;
456    match *oid {
457        OID_SHA1 => Ok(HashOutput::Sha1(sha1::Sha1::digest(data).into())),
458        OID_SHA256 => Ok(HashOutput::Sha256(sha2::Sha256::digest(data).into())),
459        OID_SHA384 => Ok(HashOutput::Sha384(sha2::Sha384::digest(data).into())),
460        OID_SHA512 => Ok(HashOutput::Sha512(sha2::Sha512::digest(data).into())),
461        _ => Err(Error::OcspMalformed),
462    }
463}
464
465/// Bool-returning core of `ResponderId` matching.
466///
467/// RFC 6960 §2.2 defines two `ResponderId` shapes:
468/// - `byName`: the contained Name must equal `subject` (RFC 4518 comparison
469///   via [`pkix_path::names_match`]).
470/// - `byKey`: the contained `KeyHash` must equal SHA-1 of `spki_raw` (the
471///   raw `subjectPublicKey` BIT STRING contents — no tag, length, or unused-
472///   bits prefix).
473///
474/// Returns `true` iff the supplied identity matches the response's
475/// `ResponderId`. Used both directly (delegated OCSP, where one ResponderId
476/// is checked against many candidate identities and "no match" is not an
477/// error) and via [`verify_responder_id_impl`] (direct OCSP, where mismatch
478/// is fatal).
479fn responder_id_matches(
480    id: &ResponderId,
481    subject: &x509_cert::name::Name,
482    spki_raw: &[u8],
483) -> bool {
484    match id {
485        ResponderId::ByName(name) => names_match(name, subject),
486        ResponderId::ByKey(key_hash) => {
487            // SHA-1 is mandated by RFC 6960 §2.3 for byKey ResponderId
488            // computation. This is a key-identifier lookup, not an
489            // authentication primitive — pre-image resistance suffices,
490            // collision resistance is not required.
491            use sha1::Digest as _;
492            let expected: [u8; 20] = sha1::Sha1::digest(spki_raw).into();
493            key_hash.as_bytes() == expected.as_ref()
494        }
495    }
496}
497
498/// Check whether `cert` carries the `id-kp-OCSPSigning` Extended Key Usage
499/// (RFC 6960 §4.2.2.2 — required on a "CA Designated Responder" certificate).
500///
501/// - `Ok(true)`: EKU extension present and contains `id-kp-OCSPSigning`.
502/// - `Ok(false)`: EKU absent, or present but does not contain that OID.
503/// - `Err(`[`Error::OcspResponderEkuMalformed`]`)`: EKU present but cannot
504///   be DER-decoded. Fail-closed: a malformed EKU on a candidate responder
505///   cert rejects the response rather than silently treating the cert as
506///   if the OCSPSigning purpose were absent.
507///
508/// Mirrors the fail-closed pattern in `pkix-path` (`try_find_cert_ext` ->
509/// `MalformedCertificate` on parse error). Re-implemented locally because
510/// `try_find_cert_ext` is private to `pkix-path`; promoting it would widen
511/// the trait surface for one use site.
512fn cert_has_ocsp_signing_eku(cert: &Certificate) -> crate::Result<bool> {
513    use x509_cert::ext::pkix::ExtendedKeyUsage;
514
515    let extns = match cert.tbs_certificate.extensions.as_deref() {
516        Some(e) => e,
517        None => return Ok(false),
518    };
519    let extn = match extns.iter().find(|e| e.extn_id == OID_EXTENDED_KEY_USAGE) {
520        Some(e) => e,
521        None => return Ok(false),
522    };
523    let eku = ExtendedKeyUsage::from_der(extn.extn_value.as_bytes())
524        .map_err(|_| Error::OcspResponderEkuMalformed)?;
525    Ok(eku.0.contains(&OID_KP_OCSP_SIGNING))
526}
527
528/// Validate a delegated OCSP responder cert against its supposed issuer.
529///
530/// RFC 6960 §4.2.2.2 — for a "CA Designated Responder" cert to be trusted:
531///
532/// 1. It must be issued directly by the CA whose certs the responder
533///    asserts status for (not by some unrelated CA with the OCSPSigning
534///    EKU).
535/// 2. It must carry `id-kp-OCSPSigning` in its `ExtendedKeyUsage`.
536/// 3. Its validity period must include the time the response was
537///    generated (the response's `producedAt`).
538/// 4. The CA's signature on its TBS must verify against the issuer's SPKI.
539///
540/// Each requirement maps to a distinct error variant for diagnostic
541/// clarity; failures short-circuit (no later check runs after an earlier
542/// one fails).
543///
544/// **Note on `id-pkix-ocsp-nocheck`**: per RFC 6960 §4.2.2.2.1 a delegate
545/// cert MAY carry the `id-pkix-ocsp-nocheck` extension to signal that
546/// callers MUST NOT recursively check the delegate's own revocation
547/// status. This crate is a single-shot offline checker — it never
548/// recurses into the delegate's revocation regardless of the extension —
549/// so we neither parse nor enforce that extension. Documentation only.
550fn validate_delegate_responder_cert<V: SignatureVerifier>(
551    delegate: &Certificate,
552    issuer_subject: &x509_cert::name::Name,
553    issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
554    verifier: &V,
555    produced_at_unix: u64,
556) -> crate::Result<()> {
557    // (a) Issued by the same CA that issued the cert under check.
558    if !names_match(&delegate.tbs_certificate.issuer, issuer_subject) {
559        return Err(Error::OcspResponderCertNotIssuedByCa);
560    }
561
562    // (b) Carries id-kp-OCSPSigning EKU.
563    match cert_has_ocsp_signing_eku(delegate)? {
564        true => {}
565        false => return Err(Error::OcspResponderEkuMissing),
566    }
567
568    // (c) Validity window includes the response's producedAt time.
569    //     Using producedAt (not now_unix) means we reject responses
570    //     produced after the responder cert expired even if the rest of
571    //     the response window is still fresh — the signing key was no
572    //     longer authoritative when the response claims to have been
573    //     generated.
574    let nb = delegate
575        .tbs_certificate
576        .validity
577        .not_before
578        .to_unix_duration()
579        .as_secs();
580    let na = delegate
581        .tbs_certificate
582        .validity
583        .not_after
584        .to_unix_duration()
585        .as_secs();
586    if produced_at_unix < nb || produced_at_unix > na {
587        return Err(Error::OcspResponderCertExpired);
588    }
589
590    // (d) Issuer's signature on the delegate cert verifies. This is a
591    //     SEPARATE signature from the OCSP response signature — distinct
592    //     error variant for diagnostics. The TBS bytes are re-encoded
593    //     here rather than spliced from the original DER because we only
594    //     have the parsed `Certificate` value; for the typical responder-
595    //     cert size this is microseconds.
596    let tbs_bytes = delegate
597        .tbs_certificate
598        .to_der()
599        .map_err(|e| Error::OcspParseError(crate::DerError::new(e)))?;
600    verifier
601        .verify_signature(
602            delegate.signature_algorithm.owned_to_ref(),
603            issuer_spki,
604            &tbs_bytes,
605            delegate.signature.raw_bytes(),
606        )
607        .map_err(|_| Error::OcspResponderCertSigInvalid)?;
608
609    Ok(())
610}
611
612/// Resolve which key signs the OCSP response. Returns the SPKI to use for
613/// the response signature check, plus a borrow lifetime tied to either
614/// the issuer reference or the embedded responder cert (whichever path
615/// applies).
616///
617/// RFC 6960 §4.2.2.2 — three signing-key cases:
618/// 1. **Direct**: the response is signed by the CA itself. `ResponderId`
619///    matches the issuer's name or SHA-1(SPKI). Use the issuer's SPKI.
620/// 2. **CA Designated Responder (delegated)**: the response is signed by
621///    a separate cert with the OCSPSigning EKU, embedded in the response's
622///    `certs` field. Find the cert matching `ResponderId`, validate it as
623///    a designated responder, and use its SPKI.
624/// 3. **Trusted Responder** (out of scope for this crate): a responder
625///    whose key the caller already trusts out-of-band. Not supported here;
626///    callers needing this can supply the trusted responder cert as the
627///    `issuer` argument when the cert under check has been issued by it.
628///
629/// The returned reference borrows from one of `basic` or `issuer`,
630/// constrained to the same lifetime via `'a`. The caller drops the SPKI
631/// ref before any further mutation of either source.
632fn resolve_signing_key_for_response<'a, V: SignatureVerifier>(
633    basic: &'a BasicOcspResponse,
634    issuer_subject: &'a x509_cert::name::Name,
635    issuer_spki: spki::SubjectPublicKeyInfoRef<'a>,
636    issuer_spki_raw: &[u8],
637    verifier: &V,
638    produced_at_unix: u64,
639) -> crate::Result<spki::SubjectPublicKeyInfoRef<'a>> {
640    let rid = &basic.tbs_response_data.responder_id;
641
642    // Direct path: ResponderId matches the issuer.
643    if responder_id_matches(rid, issuer_subject, issuer_spki_raw) {
644        return Ok(issuer_spki);
645    }
646
647    // Delegated path: scan `basic.certs` for a cert whose identity matches
648    // ResponderId, then validate it as a CA Designated Responder.
649    let certs: &[Certificate] = match basic.certs.as_deref() {
650        Some(c) => c,
651        None => &[],
652    };
653    for delegate in certs {
654        let d_subject = &delegate.tbs_certificate.subject;
655        let d_spki_raw = delegate
656            .tbs_certificate
657            .subject_public_key_info
658            .subject_public_key
659            .raw_bytes();
660        if !responder_id_matches(rid, d_subject, d_spki_raw) {
661            continue;
662        }
663
664        // Found a candidate. Validate it strictly — any failure is a
665        // distinct error variant; we do NOT fall through to "try the
666        // next candidate" because RFC 6960 §4.2.2.2 only contemplates
667        // one signing key per response, and silently skipping a
668        // candidate after partial validation would obscure attacks.
669        validate_delegate_responder_cert(
670            delegate,
671            issuer_subject,
672            issuer_spki,
673            verifier,
674            produced_at_unix,
675        )?;
676
677        return Ok(delegate
678            .tbs_certificate
679            .subject_public_key_info
680            .owned_to_ref());
681    }
682
683    // ResponderId matches neither the issuer nor any embedded cert.
684    Err(Error::OcspResponderIdMismatch)
685}
686
687/// Extract the `producedAt` time from a [`BasicOcspResponse`] as a Unix
688/// timestamp. Used by the delegated-responder validity check; declared
689/// here rather than inline to keep the call sites tight.
690fn produced_at_unix_secs(basic: &BasicOcspResponse) -> u64 {
691    basic
692        .tbs_response_data
693        .produced_at
694        .as_ref()
695        .to_unix_duration()
696        .as_secs()
697}
698
699// ---------------------------------------------------------------------------
700// Unit tests for hash_certid_input
701// ---------------------------------------------------------------------------
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    /// SHA-384 of b"test".
708    ///
709    /// Oracle: python3 -c "import hashlib, binascii; print(binascii.hexlify(hashlib.sha384(b'test').`digest()).decode()`)"
710    /// → 768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9
711    #[test]
712    fn hash_certid_sha384() {
713        let expected: &[u8] = &[
714            0x76, 0x84, 0x12, 0x32, 0x0f, 0x7b, 0x0a, 0xa5, 0x81, 0x2f, 0xce, 0x42, 0x8d, 0xc4,
715            0x70, 0x6b, 0x3c, 0xae, 0x50, 0xe0, 0x2a, 0x64, 0xca, 0xa1, 0x6a, 0x78, 0x22, 0x49,
716            0xbf, 0xe8, 0xef, 0xc4, 0xb7, 0xef, 0x1c, 0xcb, 0x12, 0x62, 0x55, 0xd1, 0x96, 0x04,
717            0x7d, 0xfe, 0xdf, 0x17, 0xa0, 0xa9,
718        ];
719        let result = hash_certid_input(&OID_SHA384, b"test").expect("SHA-384 must succeed");
720        assert_eq!(
721            result.as_slice(),
722            expected,
723            "SHA-384(\"test\") must match Python oracle"
724        );
725    }
726
727    /// SHA-512 of b"test".
728    ///
729    /// Oracle: python3 -c "import hashlib, binascii; print(binascii.hexlify(hashlib.sha512(b'test').`digest()).decode()`)"
730    /// → ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff
731    #[test]
732    fn hash_certid_sha512() {
733        let expected: &[u8] = &[
734            0xee, 0x26, 0xb0, 0xdd, 0x4a, 0xf7, 0xe7, 0x49, 0xaa, 0x1a, 0x8e, 0xe3, 0xc1, 0x0a,
735            0xe9, 0x92, 0x3f, 0x61, 0x89, 0x80, 0x77, 0x2e, 0x47, 0x3f, 0x88, 0x19, 0xa5, 0xd4,
736            0x94, 0x0e, 0x0d, 0xb2, 0x7a, 0xc1, 0x85, 0xf8, 0xa0, 0xe1, 0xd5, 0xf8, 0x4f, 0x88,
737            0xbc, 0x88, 0x7f, 0xd6, 0x7b, 0x14, 0x37, 0x32, 0xc3, 0x04, 0xcc, 0x5f, 0xa9, 0xad,
738            0x8e, 0x6f, 0x57, 0xf5, 0x00, 0x28, 0xa8, 0xff,
739        ];
740        let result = hash_certid_input(&OID_SHA512, b"test").expect("SHA-512 must succeed");
741        assert_eq!(
742            result.as_slice(),
743            expected,
744            "SHA-512(\"test\") must match Python oracle"
745        );
746    }
747
748    /// Unknown OID must return `OcspMalformed`.
749    #[test]
750    fn hash_certid_unknown_oid_returns_malformed() {
751        let unknown = der::asn1::ObjectIdentifier::new_unwrap("1.2.3.4.5");
752        let result = hash_certid_input(&unknown, b"test");
753        assert!(
754            matches!(result, Err(Error::OcspMalformed)),
755            "unknown hash OID must return OcspMalformed"
756        );
757    }
758}