Skip to main content

native_ossl/
ocsp.rs

1//! OCSP — Online Certificate Status Protocol (`RFC 2560` / `RFC 6960`).
2//!
3//! Provides the full client-side OCSP stack:
4//!
5//! - [`OcspCertId`] — identifies a certificate to query
6//! - [`OcspRequest`] — encodes the DER request to send to a responder
7//! - [`OcspResponse`] — decodes and validates a DER response
8//! - [`OcspBasicResp`] — the signed inner response; drives per-cert status lookup
9//! - [`OcspSingleStatus`] — per-certificate status result from [`OcspBasicResp::find_status`]
10//!
11//! # Typical flow
12//!
13//! ```ignore
14//! // Build a request for a specific certificate.
15//! let id = OcspCertId::from_cert(None, &end_entity_cert, &issuer_cert)?;
16//! let mut req = OcspRequest::new()?;
17//! req.add_cert_id(id)?;
18//! let req_der = req.to_der()?;
19//!
20//! // ... send req_der over HTTP, receive resp_der ...
21//!
22//! let resp = OcspResponse::from_der(&resp_der)?;
23//! assert_eq!(resp.status(), OcspResponseStatus::Successful);
24//!
25//! let basic = resp.basic()?;
26//! basic.verify(&trust_store, 0)?;
27//!
28//! let id2 = OcspCertId::from_cert(None, &end_entity_cert, &issuer_cert)?;
29//! match basic.find_status(&id2)? {
30//!     Some(s) if s.cert_status == OcspCertStatus::Good => println!("certificate is good"),
31//!     Some(s) => println!("certificate status: {:?}", s.cert_status),
32//!     None => println!("certificate not found in response"),
33//! }
34//! ```
35//!
36//! HTTP transport is **out of scope** — the caller is responsible for fetching
37//! the OCSP response from the responder URL and passing the raw DER bytes.
38
39use crate::bio::MemBio;
40use crate::error::ErrorStack;
41use native_ossl_sys as sys;
42
43// ── OcspResponseStatus ────────────────────────────────────────────────────────
44
45/// OCSP response status (RFC 6960 §4.2.1).
46///
47/// This is the *top-level* status of the response packet itself, not the status
48/// of any individual certificate.  A `Successful` response still requires
49/// per-certificate inspection via [`OcspBasicResp::find_status`].
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OcspResponseStatus {
52    /// `successful` (0) — Response packet is valid.
53    Successful,
54    /// `malformedRequest` (1) — Server could not parse the request.
55    MalformedRequest,
56    /// `internalError` (2) — Server internal error.
57    InternalError,
58    /// `tryLater` (3) — Server is busy; retry later.
59    TryLater,
60    /// `sigRequired` (5) — Signed request required by policy.
61    SigRequired,
62    /// `unauthorized` (6) — Unauthorized request.
63    Unauthorized,
64    /// Unknown status code (forward-compatibility guard).
65    Unknown(i32),
66}
67
68impl From<i32> for OcspResponseStatus {
69    fn from(v: i32) -> Self {
70        match v {
71            0 => Self::Successful,
72            1 => Self::MalformedRequest,
73            2 => Self::InternalError,
74            3 => Self::TryLater,
75            5 => Self::SigRequired,
76            6 => Self::Unauthorized,
77            n => Self::Unknown(n),
78        }
79    }
80}
81
82// ── OcspCertStatus ────────────────────────────────────────────────────────────
83
84/// Per-certificate revocation status from an `OCSP_SINGLERESP`.
85///
86/// Returned inside [`OcspSingleStatus`] by [`OcspBasicResp::find_status`].
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum OcspCertStatus {
89    /// Certificate is currently valid (`V_OCSP_CERTSTATUS_GOOD = 0`).
90    Good,
91    /// Certificate has been revoked (`V_OCSP_CERTSTATUS_REVOKED = 1`).
92    ///
93    /// `reason` is one of the `CRLReason` codes (RFC 5280 §5.3.1):
94    /// 0=unspecified, 1=keyCompromise, 2=cACompromise, 3=affiliationChanged,
95    /// 4=superseded, 5=cessationOfOperation, 6=certificateHold, 8=removeFromCRL,
96    /// 9=privilegeWithdrawn, 10=aACompromise.  -1 means no reason was given.
97    Revoked { reason: i32 },
98    /// Responder does not know this certificate (`V_OCSP_CERTSTATUS_UNKNOWN = 2`).
99    Unknown,
100}
101
102impl OcspCertStatus {
103    fn from_raw(status: i32, reason: i32) -> Self {
104        match status {
105            0 => Self::Good,
106            1 => Self::Revoked { reason },
107            _ => Self::Unknown,
108        }
109    }
110}
111
112// ── OcspSingleStatus ──────────────────────────────────────────────────────────
113
114/// Status of a single certificate, returned by [`OcspBasicResp::find_status`].
115#[derive(Debug, Clone)]
116pub struct OcspSingleStatus {
117    /// Per-certificate status.
118    pub cert_status: OcspCertStatus,
119    /// `thisUpdate` time as a human-readable UTC string, if present.
120    pub this_update: Option<String>,
121    /// `nextUpdate` time as a human-readable UTC string, if present.
122    pub next_update: Option<String>,
123    /// Revocation time as a human-readable UTC string (`cert_status == Revoked` only).
124    pub revocation_time: Option<String>,
125}
126
127// ── OcspCertId ───────────────────────────────────────────────────────────────
128
129/// Certificate identifier for OCSP (`OCSP_CERTID*`).
130///
131/// Created from a subject certificate and its issuer with [`OcspCertId::from_cert`].
132/// Add to a request with [`OcspRequest::add_cert_id`], or use to look up a status
133/// in a response with [`OcspBasicResp::find_status`].
134///
135/// `Clone` is implemented via `OCSP_CERTID_dup`.
136pub struct OcspCertId {
137    ptr: *mut sys::OCSP_CERTID,
138}
139
140unsafe impl Send for OcspCertId {}
141
142impl Clone for OcspCertId {
143    fn clone(&self) -> Self {
144        let ptr = unsafe { sys::OCSP_CERTID_dup(self.ptr) };
145        // OCSP_CERTID_dup returns null only on allocation failure; treat as abort.
146        assert!(!ptr.is_null(), "OCSP_CERTID_dup: allocation failure");
147        OcspCertId { ptr }
148    }
149}
150
151impl Drop for OcspCertId {
152    fn drop(&mut self) {
153        unsafe { sys::OCSP_CERTID_free(self.ptr) };
154    }
155}
156
157impl OcspCertId {
158    /// Build a cert ID from a subject certificate and its direct issuer.
159    ///
160    /// `digest` is the hash algorithm used to hash the issuer name and key
161    /// (default: SHA-1 when `None`, per RFC 6960).  SHA-1 is required by most
162    /// deployed OCSP responders; pass `Some(sha256_alg)` only when the responder
163    /// is known to support it.
164    ///
165    /// # Errors
166    pub fn from_cert(
167        digest: Option<&crate::digest::DigestAlg>,
168        subject: &crate::x509::X509,
169        issuer: &crate::x509::X509,
170    ) -> Result<Self, ErrorStack> {
171        let dgst_ptr = digest.map_or(std::ptr::null(), crate::digest::DigestAlg::as_ptr);
172        let ptr = unsafe { sys::OCSP_cert_to_id(dgst_ptr, subject.as_ptr(), issuer.as_ptr()) };
173        if ptr.is_null() {
174            return Err(ErrorStack::drain());
175        }
176        Ok(OcspCertId { ptr })
177    }
178}
179
180// ── OcspRequest ───────────────────────────────────────────────────────────────
181
182/// An OCSP request (`OCSP_REQUEST*`).
183///
184/// Build with [`OcspRequest::new`], populate with [`OcspRequest::add_cert_id`],
185/// then encode with [`OcspRequest::to_der`] and send to the OCSP responder.
186pub struct OcspRequest {
187    ptr: *mut sys::OCSP_REQUEST,
188}
189
190unsafe impl Send for OcspRequest {}
191
192impl Drop for OcspRequest {
193    fn drop(&mut self) {
194        unsafe { sys::OCSP_REQUEST_free(self.ptr) };
195    }
196}
197
198impl OcspRequest {
199    /// Create a new, empty OCSP request.
200    ///
201    /// # Errors
202    pub fn new() -> Result<Self, ErrorStack> {
203        let ptr = unsafe { sys::OCSP_REQUEST_new() };
204        if ptr.is_null() {
205            return Err(ErrorStack::drain());
206        }
207        Ok(OcspRequest { ptr })
208    }
209
210    /// Decode an OCSP request from DER bytes.
211    ///
212    /// # Errors
213    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
214        let mut ptr = std::ptr::null_mut::<sys::OCSP_REQUEST>();
215        let mut p = der.as_ptr();
216        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
217        let result = unsafe {
218            sys::d2i_OCSP_REQUEST(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
219        };
220        if result.is_null() {
221            return Err(ErrorStack::drain());
222        }
223        Ok(OcspRequest { ptr })
224    }
225
226    /// Add a certificate identifier to the request.
227    ///
228    /// `cert_id` ownership is transferred to the request (`add0` semantics);
229    /// the `OcspCertId` is consumed.
230    ///
231    /// # Errors
232    pub fn add_cert_id(&mut self, cert_id: OcspCertId) -> Result<(), ErrorStack> {
233        // OCSP_request_add0_id transfers ownership of the CERTID on success only.
234        // Do not forget cert_id until after the null check — on failure the CERTID
235        // is NOT consumed by OpenSSL and must still be freed by our Drop impl.
236        let rc = unsafe { sys::OCSP_request_add0_id(self.ptr, cert_id.ptr) };
237        if rc.is_null() {
238            return Err(ErrorStack::drain());
239        }
240        // Success: ownership transferred; suppress Drop.
241        std::mem::forget(cert_id);
242        Ok(())
243    }
244
245    /// Encode the OCSP request to DER bytes.
246    ///
247    /// # Errors
248    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
249        let len = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::null_mut()) };
250        if len < 0 {
251            return Err(ErrorStack::drain());
252        }
253        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
254        let mut buf = vec![0u8; len as usize];
255        let mut out_ptr = buf.as_mut_ptr();
256        let written = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
257        if written < 0 {
258            return Err(ErrorStack::drain());
259        }
260        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
261        buf.truncate(written as usize);
262        Ok(buf)
263    }
264}
265
266// ── OcspBasicResp ─────────────────────────────────────────────────────────────
267
268/// The signed inner OCSP response (`OCSP_BASICRESP*`).
269///
270/// Extracted from an [`OcspResponse`] via [`OcspResponse::basic`].
271/// Provides signature verification and per-certificate status lookup.
272pub struct OcspBasicResp {
273    ptr: *mut sys::OCSP_BASICRESP,
274}
275
276unsafe impl Send for OcspBasicResp {}
277
278impl Drop for OcspBasicResp {
279    fn drop(&mut self) {
280        unsafe { sys::OCSP_BASICRESP_free(self.ptr) };
281    }
282}
283
284impl OcspBasicResp {
285    /// Verify the response signature against `store`.
286    ///
287    /// `flags` is passed directly to `OCSP_basic_verify` (use 0 for defaults,
288    /// which verifies the signature and checks the signing certificate chain).
289    ///
290    /// Returns `Ok(true)` if the signature is valid.
291    ///
292    /// # Errors
293    pub fn verify(&self, store: &crate::x509::X509Store, flags: u64) -> Result<bool, ErrorStack> {
294        match unsafe {
295            sys::OCSP_basic_verify(self.ptr, std::ptr::null_mut(), store.as_ptr(), flags)
296        } {
297            1 => Ok(true),
298            0 => Ok(false),
299            _ => Err(ErrorStack::drain()),
300        }
301    }
302
303    /// Number of `SingleResponse` entries in this basic response.
304    #[must_use]
305    pub fn count(&self) -> usize {
306        let n = unsafe { sys::OCSP_resp_count(self.ptr) };
307        usize::try_from(n).unwrap_or(0)
308    }
309
310    /// Look up the status for a specific certificate by its [`OcspCertId`].
311    ///
312    /// Returns `Ok(Some(status))` if the responder included a `SingleResponse`
313    /// for that certificate, `Ok(None)` if not found, or `Err` on a fatal
314    /// OpenSSL error.
315    ///
316    /// The `cert_id` is passed by shared reference; its pointer is only used
317    /// for the duration of this call (`OCSP_resp_find_status` does not store it).
318    ///
319    /// # Errors
320    pub fn find_status(
321        &self,
322        cert_id: &OcspCertId,
323    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
324        let mut status: i32 = -1;
325        let mut reason: i32 = -1;
326        let mut revtime: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
327        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
328        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
329
330        let rc = unsafe {
331            sys::OCSP_resp_find_status(
332                self.ptr,
333                cert_id.ptr,
334                std::ptr::addr_of_mut!(status),
335                std::ptr::addr_of_mut!(reason),
336                std::ptr::addr_of_mut!(revtime),
337                std::ptr::addr_of_mut!(thisupd),
338                std::ptr::addr_of_mut!(nextupd),
339            )
340        };
341
342        // rc == 1 → found; rc == 0 → not found; anything else → error.
343        match rc {
344            1 => Ok(Some(OcspSingleStatus {
345                cert_status: OcspCertStatus::from_raw(status, reason),
346                this_update: generalizedtime_to_str(thisupd),
347                next_update: generalizedtime_to_str(nextupd),
348                revocation_time: generalizedtime_to_str(revtime),
349            })),
350            0 => Ok(None),
351            _ => Err(ErrorStack::drain()),
352        }
353    }
354
355    /// Validate the `thisUpdate` / `nextUpdate` window of a `SingleResponse`.
356    ///
357    /// `sec` is the acceptable clock-skew in seconds (typically 300).
358    /// `maxsec` limits how far in the future `nextUpdate` may be (-1 = no limit).
359    ///
360    /// # Errors
361    pub fn check_validity(
362        &self,
363        cert_id: &OcspCertId,
364        sec: i64,
365        maxsec: i64,
366    ) -> Result<bool, ErrorStack> {
367        // Re-run find_status to get thisupd / nextupd pointers.
368        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
369        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
370        let rc = unsafe {
371            sys::OCSP_resp_find_status(
372                self.ptr,
373                cert_id.ptr,
374                std::ptr::null_mut(), // status
375                std::ptr::null_mut(), // reason
376                std::ptr::null_mut(), // revtime
377                std::ptr::addr_of_mut!(thisupd),
378                std::ptr::addr_of_mut!(nextupd),
379            )
380        };
381        // rc == 1 → found; rc == 0 → not found; negative → fatal error.
382        match rc {
383            1 => {}
384            0 => return Ok(false),
385            _ => return Err(ErrorStack::drain()),
386        }
387        match unsafe { sys::OCSP_check_validity(thisupd, nextupd, sec, maxsec) } {
388            1 => Ok(true),
389            0 => Ok(false),
390            _ => Err(ErrorStack::drain()),
391        }
392    }
393}
394
395// ── OcspResponse ──────────────────────────────────────────────────────────────
396
397/// An OCSP response (`OCSP_RESPONSE*`).
398///
399/// Decode from DER with [`OcspResponse::from_der`].  Check the top-level
400/// [`OcspResponse::status`], then extract the signed inner response with
401/// [`OcspResponse::basic`] for per-certificate status lookup.
402pub struct OcspResponse {
403    ptr: *mut sys::OCSP_RESPONSE,
404}
405
406unsafe impl Send for OcspResponse {}
407
408impl Drop for OcspResponse {
409    fn drop(&mut self) {
410        unsafe { sys::OCSP_RESPONSE_free(self.ptr) };
411    }
412}
413
414impl OcspResponse {
415    /// Decode an OCSP response from DER bytes.
416    ///
417    /// # Errors
418    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
419        let mut ptr = std::ptr::null_mut::<sys::OCSP_RESPONSE>();
420        let mut p = der.as_ptr();
421        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
422        let result = unsafe {
423            sys::d2i_OCSP_RESPONSE(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
424        };
425        if result.is_null() {
426            return Err(ErrorStack::drain());
427        }
428        Ok(OcspResponse { ptr })
429    }
430
431    /// Encode the OCSP response to DER bytes.
432    ///
433    /// # Errors
434    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
435        let len = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::null_mut()) };
436        if len < 0 {
437            return Err(ErrorStack::drain());
438        }
439        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
440        let mut buf = vec![0u8; len as usize];
441        let mut out_ptr = buf.as_mut_ptr();
442        let written = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
443        if written < 0 {
444            return Err(ErrorStack::drain());
445        }
446        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
447        buf.truncate(written as usize);
448        Ok(buf)
449    }
450
451    /// Overall OCSP response status (top-level packet status, not cert status).
452    ///
453    /// A `Successful` value means the server processed the request; it does not
454    /// mean any individual certificate is good.  Use [`Self::basic`] and then
455    /// [`OcspBasicResp::find_status`] for per-certificate results.
456    #[must_use]
457    pub fn status(&self) -> OcspResponseStatus {
458        OcspResponseStatus::from(unsafe { sys::OCSP_response_status(self.ptr) })
459    }
460
461    /// Extract the signed inner response (`OCSP_BASICRESP*`).
462    ///
463    /// Only valid when [`Self::status`] is [`OcspResponseStatus::Successful`].
464    ///
465    /// # Errors
466    ///
467    /// Returns `Err` if the response has no basic response body (e.g. the
468    /// top-level status is not `Successful`).
469    pub fn basic(&self) -> Result<OcspBasicResp, ErrorStack> {
470        let ptr = unsafe { sys::OCSP_response_get1_basic(self.ptr) };
471        if ptr.is_null() {
472            return Err(ErrorStack::drain());
473        }
474        Ok(OcspBasicResp { ptr })
475    }
476
477    /// Convenience: verify the basic response signature and look up a cert status
478    /// in one call.
479    ///
480    /// Equivalent to `resp.basic()?.verify(store, 0)?; resp.basic()?.find_status(id)`.
481    ///
482    /// # Errors
483    pub fn verified_status(
484        &self,
485        store: &crate::x509::X509Store,
486        cert_id: &OcspCertId,
487    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
488        let basic = self.basic()?;
489        // verify() returns Ok(false) when the signature is invalid — treat that
490        // as an error to prevent certificate status from an unverified response.
491        if !basic.verify(store, 0)? {
492            return Err(ErrorStack::drain());
493        }
494        basic.find_status(cert_id)
495    }
496
497    /// Build a minimal `OCSP_RESPONSE` (status = successful, no basic response)
498    /// and return it as DER. Used for testing only.
499    #[cfg(test)]
500    fn new_successful_der() -> Vec<u8> {
501        // DER: SEQUENCE { ENUMERATED 0 }
502        // OCSPResponseStatus successful(0) with no responseBytes.
503        vec![0x30, 0x03, 0x0A, 0x01, 0x00]
504    }
505}
506
507// ── Private helpers ───────────────────────────────────────────────────────────
508
509/// Convert an `ASN1_GENERALIZEDTIME*` (which is really `ASN1_STRING*`) to a
510/// human-readable string via `ASN1_TIME_print` on a memory BIO.
511fn generalizedtime_to_str(t: *mut sys::ASN1_GENERALIZEDTIME) -> Option<String> {
512    if t.is_null() {
513        return None;
514    }
515    // ASN1_GENERALIZEDTIME is typedef'd to asn1_string_st, same as ASN1_TIME.
516    // ASN1_TIME_print handles both UTCTime and GeneralizedTime.
517    let Ok(mut bio) = MemBio::new() else {
518        // BIO allocation failed; clear the error queue so callers do not
519        // see a stale allocation error as if it came from their own call.
520        unsafe { sys::ERR_clear_error() };
521        return None;
522    };
523    let rc = unsafe { sys::ASN1_TIME_print(bio.as_ptr(), t.cast::<sys::ASN1_TIME>()) };
524    if rc != 1 {
525        unsafe { sys::ERR_clear_error() };
526        return None;
527    }
528    String::from_utf8(bio.into_vec()).ok()
529}
530
531// ── Tests ─────────────────────────────────────────────────────────────────────
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536    use crate::pkey::{KeygenCtx, Pkey, Private, Public};
537    use crate::x509::{X509Builder, X509NameOwned};
538
539    /// Build a minimal CA + end-entity certificate pair for testing.
540    fn make_ca_and_ee() -> (
541        crate::x509::X509,
542        Pkey<Private>,
543        crate::x509::X509,
544        Pkey<Private>,
545    ) {
546        // CA key + cert (self-signed)
547        let mut ca_kgen = KeygenCtx::new(c"ED25519").unwrap();
548        let ca_priv = ca_kgen.generate().unwrap();
549        let ca_pub = Pkey::<Public>::from(ca_priv.clone());
550
551        let mut ca_name = X509NameOwned::new().unwrap();
552        ca_name.add_entry_by_txt(c"CN", b"OCSP Test CA").unwrap();
553
554        let ca_cert = X509Builder::new()
555            .unwrap()
556            .set_version(2)
557            .unwrap()
558            .set_serial_number(1)
559            .unwrap()
560            .set_not_before_offset(0)
561            .unwrap()
562            .set_not_after_offset(365 * 86400)
563            .unwrap()
564            .set_subject_name(&ca_name)
565            .unwrap()
566            .set_issuer_name(&ca_name)
567            .unwrap()
568            .set_public_key(&ca_pub)
569            .unwrap()
570            .sign(&ca_priv, None)
571            .unwrap()
572            .build();
573
574        // EE key + cert (signed by CA)
575        let mut ee_kgen = KeygenCtx::new(c"ED25519").unwrap();
576        let ee_priv = ee_kgen.generate().unwrap();
577        let ee_pub = Pkey::<Public>::from(ee_priv.clone());
578
579        let mut ee_name = X509NameOwned::new().unwrap();
580        ee_name.add_entry_by_txt(c"CN", b"OCSP Test EE").unwrap();
581
582        let ee_cert = X509Builder::new()
583            .unwrap()
584            .set_version(2)
585            .unwrap()
586            .set_serial_number(2)
587            .unwrap()
588            .set_not_before_offset(0)
589            .unwrap()
590            .set_not_after_offset(365 * 86400)
591            .unwrap()
592            .set_subject_name(&ee_name)
593            .unwrap()
594            .set_issuer_name(&ca_name)
595            .unwrap()
596            .set_public_key(&ee_pub)
597            .unwrap()
598            .sign(&ca_priv, None)
599            .unwrap()
600            .build();
601
602        (ca_cert, ca_priv, ee_cert, ee_priv)
603    }
604
605    // ── OcspCertId tests ──────────────────────────────────────────────────────
606
607    #[test]
608    fn cert_id_from_cert() {
609        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
610        // SHA-1 is the OCSP default; pass None for the digest.
611        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
612        // Clone must not crash.
613        let _id2 = id.clone();
614    }
615
616    // ── OcspRequest tests ─────────────────────────────────────────────────────
617
618    #[test]
619    fn ocsp_request_new_and_to_der() {
620        let req = OcspRequest::new().unwrap();
621        let der = req.to_der().unwrap();
622        assert!(!der.is_empty());
623    }
624
625    #[test]
626    fn ocsp_request_with_cert_id() {
627        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
628        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
629
630        let mut req = OcspRequest::new().unwrap();
631        req.add_cert_id(id).unwrap();
632        let der = req.to_der().unwrap();
633        assert!(!der.is_empty());
634        // DER with a cert ID is larger than an empty request.
635        let empty_der = OcspRequest::new().unwrap().to_der().unwrap();
636        assert!(der.len() > empty_der.len());
637    }
638
639    #[test]
640    fn ocsp_request_der_roundtrip() {
641        let req = OcspRequest::new().unwrap();
642        let der = req.to_der().unwrap();
643        let req2 = OcspRequest::from_der(&der).unwrap();
644        assert_eq!(req2.to_der().unwrap(), der);
645    }
646
647    // ── OcspResponse tests ────────────────────────────────────────────────────
648
649    #[test]
650    fn ocsp_response_status_decode() {
651        let der = OcspResponse::new_successful_der();
652        let resp = OcspResponse::from_der(&der).unwrap();
653        assert_eq!(resp.status(), OcspResponseStatus::Successful);
654    }
655
656    #[test]
657    fn ocsp_response_der_roundtrip() {
658        let der = OcspResponse::new_successful_der();
659        let resp = OcspResponse::from_der(&der).unwrap();
660        assert_eq!(resp.to_der().unwrap(), der);
661    }
662
663    #[test]
664    fn ocsp_response_basic_fails_without_body() {
665        // A response with only a status code and no responseBytes has no basic resp.
666        let der = OcspResponse::new_successful_der();
667        let resp = OcspResponse::from_der(&der).unwrap();
668        // basic() should return Err because there is no responseBytes.
669        assert!(resp.basic().is_err());
670    }
671
672    // ── OcspBasicResp / find_status tests ────────────────────────────────────
673    //
674    // Building a real OCSP_BASICRESP from scratch requires the full OCSP
675    // responder stack (OCSP_basic_sign, OCSP_basic_add1_status) which is
676    // outside the scope of unit tests. Instead we verify that find_status
677    // returns None when the cert is not in the response (requires a real
678    // OCSP response DER), and test the X509Store/X509StoreCtx path via
679    // the integration-level store tests in x509.rs.
680    //
681    // The important invariants (OcspCertId::from_cert, add_cert_id, DER
682    // round-trip) are covered by the tests above.
683    //
684    // If a real OCSP response is available (e.g. from a test OCSP responder),
685    // use OcspResponse::from_der + basic() + find_status() to validate the
686    // full stack.
687}