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::SignatureVerifier;
8use spki::der::referenced::OwnedToRef as _;
9use x509_cert::Certificate;
10use x509_ocsp::{BasicOcspResponse, CertStatus, OcspResponse, OcspResponseStatus};
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");
21
22/// Offline OCSP-based revocation checker.
23///
24/// Parses a pre-fetched DER-encoded OCSP response, verifies its signature
25/// against the issuer's SPKI, checks the validity window of the matching
26/// [`SingleResponse`][x509_ocsp::SingleResponse], and reports the certificate's
27/// revocation status.
28///
29/// # Feature
30///
31/// Only available when the `ocsp` feature is enabled.
32///
33/// # Limitations (v0.1)
34///
35/// - The OCSP response is re-parsed from DER on every [`check_revocation`] call.
36///   For chains with multiple certificates validated against the same response,
37///   this is O(N) redundant parsing. Tracked for v0.2 (cache the parsed
38///   `BasicOcspResponse` in `new`).
39/// - Only issuer-signed (direct) OCSP responses are supported.
40///   Delegated OCSP responders (responses signed by a separate responder
41///   certificate, not by the issuer directly) will fail with
42///   [`Error::OcspSignatureInvalid`] because the signature is verified against
43///   the issuer's key. This is a v0.1 limitation tracked for v0.2.
44///
45/// [`check_revocation`]: crate::RevocationChecker::check_revocation
46/// - `SingleResponse` matching uses both serial number and the `CertID`
47///   `issuerNameHash`/`issuerKeyHash` fields (RFC 6960 §4.1.1). An OCSP
48///   response from a different CA with the same serial number will be rejected
49///   by the hash checks.
50/// - The `ResponderId` field is not verified against the issuer identity.
51/// - If no `SingleResponse` matches the certificate's serial number,
52///   `OcspStatusUnknown` is returned (hard-fail).
53/// - [`RevocationChecker::check_revocation_against_anchor`] is not overridden.
54///   The certificate immediately issued by the trust anchor is not
55///   revocation-checked by this type; revocation against the anchor is the
56///   responsibility of the path validator (a v0.1 limitation).
57///
58/// [`RevocationChecker::check_revocation_against_anchor`]: crate::RevocationChecker::check_revocation_against_anchor
59#[derive(Clone, Debug)]
60pub struct OcspChecker<V> {
61    response_der: Vec<u8>,
62    now_unix: u64,
63    verifier: V,
64}
65
66impl<V: SignatureVerifier> OcspChecker<V> {
67    /// Create a new `OcspChecker`.
68    ///
69    /// - `response_der` — DER-encoded `OCSPResponse` (any `Into<Vec<u8>>`, e.g. `Vec<u8>` or `&[u8]`)
70    /// - `now_unix`     — current time as seconds since the Unix epoch
71    /// - `verifier`     — signature verifier used to authenticate the OCSP response
72    #[must_use]
73    pub fn new(response_der: impl Into<Vec<u8>>, now_unix: u64, verifier: V) -> Self {
74        Self {
75            response_der: response_der.into(),
76            now_unix,
77            verifier,
78        }
79    }
80}
81
82impl<V: SignatureVerifier> RevocationChecker for OcspChecker<V> {
83    fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()> {
84        // (1) Parse the outer OCSPResponse.
85        let resp = OcspResponse::from_der(&self.response_der).map_err(Error::OcspParseError)?;
86
87        // (2) Require responseStatus == successful; any other → OcspStatusUnknown.
88        if resp.response_status != OcspResponseStatus::Successful {
89            return Err(Error::OcspStatusUnknown);
90        }
91
92        // (3) Extract responseBytes (must be present for a Successful response).
93        let resp_bytes = resp.response_bytes.ok_or(Error::OcspMalformed)?;
94
95        // (4) Verify responseType is id-pkix-ocsp-basic.
96        if resp_bytes.response_type != OID_PKIX_OCSP_BASIC {
97            return Err(Error::OcspMalformed);
98        }
99
100        // (5) Parse the BasicOCSPResponse.
101        let basic = BasicOcspResponse::from_der(resp_bytes.response.as_bytes())
102            .map_err(Error::OcspParseError)?;
103
104        // (6) Verify the OCSP signature against the issuer's SPKI.
105        //
106        // v0.1 limitation: assumes the response is signed directly by the issuer.
107        // The signature covers the DER encoding of ResponseData (tbs_response_data).
108        let tbs_bytes = basic
109            .tbs_response_data
110            .to_der()
111            .map_err(Error::OcspParseError)?;
112        self.verifier
113            .verify_signature(
114                basic.signature_algorithm.owned_to_ref(),
115                issuer
116                    .tbs_certificate
117                    .subject_public_key_info
118                    .owned_to_ref(),
119                &tbs_bytes,
120                basic.signature.raw_bytes(),
121            )
122            .map_err(|_| Error::OcspSignatureInvalid)?;
123
124        // (7) Find the SingleResponse for this certificate (match by serial number).
125        let cert_serial = &cert.tbs_certificate.serial_number;
126        let single = basic
127            .tbs_response_data
128            .responses
129            .iter()
130            .find(|r| &r.cert_id.serial_number == cert_serial)
131            .ok_or(Error::OcspStatusUnknown)?;
132
133        // (7a) Verify CertID issuer hashes (RFC 6960 §4.1.1).
134        //
135        // issuerNameHash = hash(DER(issuer.subject))
136        // issuerKeyHash  = hash(issuer.spki.subject_public_key.raw_bytes())
137        //
138        // Without this check a response produced for a cert with the same serial
139        // number issued by a *different* CA could pass serial-only matching.
140        let hash_oid = &single.cert_id.hash_algorithm.oid;
141        let name_der = issuer
142            .tbs_certificate
143            .subject
144            .to_der()
145            .map_err(Error::OcspParseError)?;
146        let key_raw = issuer
147            .tbs_certificate
148            .subject_public_key_info
149            .subject_public_key
150            .raw_bytes();
151        let expected_name_hash = hash_certid_input(hash_oid, &name_der)?;
152        let expected_key_hash = hash_certid_input(hash_oid, key_raw)?;
153        if single.cert_id.issuer_name_hash.as_bytes() != expected_name_hash.as_slice()
154            || single.cert_id.issuer_key_hash.as_bytes() != expected_key_hash.as_slice()
155        {
156            return Err(Error::OcspStatusUnknown);
157        }
158
159        // (8) Check validity windows.
160        //
161        // producedAt must not be in the future: a future-dated response indicates
162        // clock skew or a response generated after "now"; reject as unknown.
163        let produced_at = basic
164            .tbs_response_data
165            .produced_at
166            .as_ref()
167            .to_unix_duration()
168            .as_secs();
169        if self.now_unix < produced_at {
170            return Err(Error::OcspStatusUnknown);
171        }
172        // thisUpdate ≤ now: the SingleResponse is not yet valid.
173        let this_update = single.this_update.as_ref().to_unix_duration().as_secs();
174        if self.now_unix < this_update {
175            return Err(Error::OcspStatusUnknown);
176        }
177        // now ≤ nextUpdate: absent nextUpdate is treated as unknown (no expiry info
178        // means we cannot trust the freshness of the status).
179        match &single.next_update {
180            Some(next_update) => {
181                if self.now_unix > next_update.as_ref().to_unix_duration().as_secs() {
182                    return Err(Error::OcspStatusUnknown);
183                }
184            }
185            None => return Err(Error::OcspStatusUnknown),
186        }
187
188        // (9) Return based on certStatus.
189        match single.cert_status {
190            CertStatus::Good(_) => Ok(()),
191            CertStatus::Revoked(ref info) => Err(Error::Revoked {
192                serial: cert_serial.clone(),
193                reason_code: info.revocation_reason,
194            }),
195            CertStatus::Unknown(_) => Err(Error::OcspStatusUnknown),
196        }
197    }
198}
199
200/// Hash `data` using the algorithm identified by `oid`.
201///
202/// Supports SHA-1 (OID 1.3.14.3.2.26) and SHA-256 (OID 2.16.840.1.101.3.4.2.1).
203/// Returns [`Error::OcspMalformed`] for any other OID.
204fn hash_certid_input(oid: &der::asn1::ObjectIdentifier, data: &[u8]) -> crate::Result<Vec<u8>> {
205    match *oid {
206        OID_SHA1 => {
207            use sha1::Digest as _;
208            Ok(sha1::Sha1::digest(data).to_vec())
209        }
210        OID_SHA256 => {
211            use sha2::Digest as _;
212            Ok(sha2::Sha256::digest(data).to_vec())
213        }
214        _ => Err(Error::OcspMalformed),
215    }
216}