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}