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 {
173            sys::OCSP_cert_to_id(dgst_ptr, subject.as_ptr(), issuer.as_ptr())
174        };
175        if ptr.is_null() {
176            return Err(ErrorStack::drain());
177        }
178        Ok(OcspCertId { ptr })
179    }
180}
181
182// ── OcspRequest ───────────────────────────────────────────────────────────────
183
184/// An OCSP request (`OCSP_REQUEST*`).
185///
186/// Build with [`OcspRequest::new`], populate with [`OcspRequest::add_cert_id`],
187/// then encode with [`OcspRequest::to_der`] and send to the OCSP responder.
188pub struct OcspRequest {
189    ptr: *mut sys::OCSP_REQUEST,
190}
191
192unsafe impl Send for OcspRequest {}
193
194impl Drop for OcspRequest {
195    fn drop(&mut self) {
196        unsafe { sys::OCSP_REQUEST_free(self.ptr) };
197    }
198}
199
200impl OcspRequest {
201    /// Create a new, empty OCSP request.
202    ///
203    /// # Errors
204    pub fn new() -> Result<Self, ErrorStack> {
205        let ptr = unsafe { sys::OCSP_REQUEST_new() };
206        if ptr.is_null() {
207            return Err(ErrorStack::drain());
208        }
209        Ok(OcspRequest { ptr })
210    }
211
212    /// Decode an OCSP request from DER bytes.
213    ///
214    /// # Errors
215    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
216        let mut ptr = std::ptr::null_mut::<sys::OCSP_REQUEST>();
217        let mut p = der.as_ptr();
218        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
219        let result = unsafe {
220            sys::d2i_OCSP_REQUEST(
221                std::ptr::addr_of_mut!(ptr),
222                std::ptr::addr_of_mut!(p),
223                len,
224            )
225        };
226        if result.is_null() {
227            return Err(ErrorStack::drain());
228        }
229        Ok(OcspRequest { ptr })
230    }
231
232    /// Add a certificate identifier to the request.
233    ///
234    /// `cert_id` ownership is transferred to the request (`add0` semantics);
235    /// the `OcspCertId` is consumed.
236    ///
237    /// # Errors
238    pub fn add_cert_id(&mut self, cert_id: OcspCertId) -> Result<(), ErrorStack> {
239        // OCSP_request_add0_id transfers ownership of the CERTID on success only.
240        // Do not forget cert_id until after the null check — on failure the CERTID
241        // is NOT consumed by OpenSSL and must still be freed by our Drop impl.
242        let rc = unsafe { sys::OCSP_request_add0_id(self.ptr, cert_id.ptr) };
243        if rc.is_null() {
244            return Err(ErrorStack::drain());
245        }
246        // Success: ownership transferred; suppress Drop.
247        std::mem::forget(cert_id);
248        Ok(())
249    }
250
251    /// Encode the OCSP request to DER bytes.
252    ///
253    /// # Errors
254    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
255        let len = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::null_mut()) };
256        if len < 0 {
257            return Err(ErrorStack::drain());
258        }
259        let mut buf = vec![0u8; len as usize];
260        let mut out_ptr = buf.as_mut_ptr();
261        let written = unsafe {
262            sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::addr_of_mut!(out_ptr))
263        };
264        if written < 0 {
265            return Err(ErrorStack::drain());
266        }
267        buf.truncate(written as usize);
268        Ok(buf)
269    }
270}
271
272// ── OcspBasicResp ─────────────────────────────────────────────────────────────
273
274/// The signed inner OCSP response (`OCSP_BASICRESP*`).
275///
276/// Extracted from an [`OcspResponse`] via [`OcspResponse::basic`].
277/// Provides signature verification and per-certificate status lookup.
278pub struct OcspBasicResp {
279    ptr: *mut sys::OCSP_BASICRESP,
280}
281
282unsafe impl Send for OcspBasicResp {}
283
284impl Drop for OcspBasicResp {
285    fn drop(&mut self) {
286        unsafe { sys::OCSP_BASICRESP_free(self.ptr) };
287    }
288}
289
290impl OcspBasicResp {
291    /// Verify the response signature against `store`.
292    ///
293    /// `flags` is passed directly to `OCSP_basic_verify` (use 0 for defaults,
294    /// which verifies the signature and checks the signing certificate chain).
295    ///
296    /// Returns `Ok(true)` if the signature is valid.
297    ///
298    /// # Errors
299    pub fn verify(
300        &self,
301        store: &crate::x509::X509Store,
302        flags: u64,
303    ) -> Result<bool, ErrorStack> {
304        match unsafe {
305            sys::OCSP_basic_verify(self.ptr, std::ptr::null_mut(), store.as_ptr(), flags)
306        } {
307            1 => Ok(true),
308            0 => Ok(false),
309            _ => Err(ErrorStack::drain()),
310        }
311    }
312
313    /// Number of `SingleResponse` entries in this basic response.
314    #[must_use]
315    pub fn count(&self) -> usize {
316        let n = unsafe { sys::OCSP_resp_count(self.ptr) };
317        usize::try_from(n).unwrap_or(0)
318    }
319
320    /// Look up the status for a specific certificate by its [`OcspCertId`].
321    ///
322    /// Returns `Ok(Some(status))` if the responder included a `SingleResponse`
323    /// for that certificate, `Ok(None)` if not found, or `Err` on a fatal
324    /// OpenSSL error.
325    ///
326    /// The `cert_id` is passed by shared reference; its pointer is only used
327    /// for the duration of this call (`OCSP_resp_find_status` does not store it).
328    ///
329    /// # Errors
330    pub fn find_status(
331        &self,
332        cert_id: &OcspCertId,
333    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
334        let mut status: i32   = -1;
335        let mut reason: i32   = -1;
336        let mut revtime:  *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
337        let mut thisupd:  *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
338        let mut nextupd:  *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
339
340        let rc = unsafe {
341            sys::OCSP_resp_find_status(
342                self.ptr,
343                cert_id.ptr,
344                std::ptr::addr_of_mut!(status),
345                std::ptr::addr_of_mut!(reason),
346                std::ptr::addr_of_mut!(revtime),
347                std::ptr::addr_of_mut!(thisupd),
348                std::ptr::addr_of_mut!(nextupd),
349            )
350        };
351
352        // rc == 1 → found; rc == 0 → not found; anything else → error.
353        match rc {
354            1 => Ok(Some(OcspSingleStatus {
355                cert_status: OcspCertStatus::from_raw(status, reason),
356                this_update: generalizedtime_to_str(thisupd),
357                next_update: generalizedtime_to_str(nextupd),
358                revocation_time: generalizedtime_to_str(revtime),
359            })),
360            0 => Ok(None),
361            _ => Err(ErrorStack::drain()),
362        }
363    }
364
365    /// Validate the `thisUpdate` / `nextUpdate` window of a `SingleResponse`.
366    ///
367    /// `sec` is the acceptable clock-skew in seconds (typically 300).
368    /// `maxsec` limits how far in the future `nextUpdate` may be (-1 = no limit).
369    ///
370    /// # Errors
371    pub fn check_validity(
372        &self,
373        cert_id: &OcspCertId,
374        sec: i64,
375        maxsec: i64,
376    ) -> Result<bool, ErrorStack> {
377        // Re-run find_status to get thisupd / nextupd pointers.
378        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
379        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
380        let rc = unsafe {
381            sys::OCSP_resp_find_status(
382                self.ptr,
383                cert_id.ptr,
384                std::ptr::null_mut(), // status
385                std::ptr::null_mut(), // reason
386                std::ptr::null_mut(), // revtime
387                std::ptr::addr_of_mut!(thisupd),
388                std::ptr::addr_of_mut!(nextupd),
389            )
390        };
391        // rc == 1 → found; rc == 0 → not found; negative → fatal error.
392        match rc {
393            1 => {}
394            0 => return Ok(false),
395            _ => return Err(ErrorStack::drain()),
396        }
397        match unsafe { sys::OCSP_check_validity(thisupd, nextupd, sec as i64, maxsec as i64) } {
398            1 => Ok(true),
399            0 => Ok(false),
400            _ => Err(ErrorStack::drain()),
401        }
402    }
403}
404
405// ── OcspResponse ──────────────────────────────────────────────────────────────
406
407/// An OCSP response (`OCSP_RESPONSE*`).
408///
409/// Decode from DER with [`OcspResponse::from_der`].  Check the top-level
410/// [`OcspResponse::status`], then extract the signed inner response with
411/// [`OcspResponse::basic`] for per-certificate status lookup.
412pub struct OcspResponse {
413    ptr: *mut sys::OCSP_RESPONSE,
414}
415
416unsafe impl Send for OcspResponse {}
417
418impl Drop for OcspResponse {
419    fn drop(&mut self) {
420        unsafe { sys::OCSP_RESPONSE_free(self.ptr) };
421    }
422}
423
424impl OcspResponse {
425    /// Decode an OCSP response from DER bytes.
426    ///
427    /// # Errors
428    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
429        let mut ptr = std::ptr::null_mut::<sys::OCSP_RESPONSE>();
430        let mut p = der.as_ptr();
431        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
432        let result = unsafe {
433            sys::d2i_OCSP_RESPONSE(
434                std::ptr::addr_of_mut!(ptr),
435                std::ptr::addr_of_mut!(p),
436                len,
437            )
438        };
439        if result.is_null() {
440            return Err(ErrorStack::drain());
441        }
442        Ok(OcspResponse { ptr })
443    }
444
445    /// Encode the OCSP response to DER bytes.
446    ///
447    /// # Errors
448    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
449        let len = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::null_mut()) };
450        if len < 0 {
451            return Err(ErrorStack::drain());
452        }
453        let mut buf = vec![0u8; len as usize];
454        let mut out_ptr = buf.as_mut_ptr();
455        let written = unsafe {
456            sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::addr_of_mut!(out_ptr))
457        };
458        if written < 0 {
459            return Err(ErrorStack::drain());
460        }
461        buf.truncate(written as usize);
462        Ok(buf)
463    }
464
465    /// Overall OCSP response status (top-level packet status, not cert status).
466    ///
467    /// A `Successful` value means the server processed the request; it does not
468    /// mean any individual certificate is good.  Use [`Self::basic`] and then
469    /// [`OcspBasicResp::find_status`] for per-certificate results.
470    #[must_use]
471    pub fn status(&self) -> OcspResponseStatus {
472        OcspResponseStatus::from(unsafe { sys::OCSP_response_status(self.ptr) })
473    }
474
475    /// Extract the signed inner response (`OCSP_BASICRESP*`).
476    ///
477    /// Only valid when [`Self::status`] is [`OcspResponseStatus::Successful`].
478    ///
479    /// # Errors
480    ///
481    /// Returns `Err` if the response has no basic response body (e.g. the
482    /// top-level status is not `Successful`).
483    pub fn basic(&self) -> Result<OcspBasicResp, ErrorStack> {
484        let ptr = unsafe { sys::OCSP_response_get1_basic(self.ptr) };
485        if ptr.is_null() {
486            return Err(ErrorStack::drain());
487        }
488        Ok(OcspBasicResp { ptr })
489    }
490
491    /// Convenience: verify the basic response signature and look up a cert status
492    /// in one call.
493    ///
494    /// Equivalent to `resp.basic()?.verify(store, 0)?; resp.basic()?.find_status(id)`.
495    ///
496    /// # Errors
497    pub fn verified_status(
498        &self,
499        store: &crate::x509::X509Store,
500        cert_id: &OcspCertId,
501    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
502        let basic = self.basic()?;
503        // verify() returns Ok(false) when the signature is invalid — treat that
504        // as an error to prevent certificate status from an unverified response.
505        if !basic.verify(store, 0)? {
506            return Err(ErrorStack::drain());
507        }
508        basic.find_status(cert_id)
509    }
510
511    /// Build a minimal `OCSP_RESPONSE` (status = successful, no basic response)
512    /// and return it as DER. Used for testing only.
513    #[cfg(test)]
514    fn new_successful_der() -> Vec<u8> {
515        // DER: SEQUENCE { ENUMERATED 0 }
516        // OCSPResponseStatus successful(0) with no responseBytes.
517        vec![0x30, 0x03, 0x0A, 0x01, 0x00]
518    }
519}
520
521// ── Private helpers ───────────────────────────────────────────────────────────
522
523/// Convert an `ASN1_GENERALIZEDTIME*` (which is really `ASN1_STRING*`) to a
524/// human-readable string via `ASN1_TIME_print` on a memory BIO.
525fn generalizedtime_to_str(t: *mut sys::ASN1_GENERALIZEDTIME) -> Option<String> {
526    if t.is_null() {
527        return None;
528    }
529    // ASN1_GENERALIZEDTIME is typedef'd to asn1_string_st, same as ASN1_TIME.
530    // ASN1_TIME_print handles both UTCTime and GeneralizedTime.
531    let mut bio = match MemBio::new() {
532        Ok(b) => b,
533        Err(_) => {
534            // BIO allocation failed; clear the error queue so callers do not
535            // see a stale allocation error as if it came from their own call.
536            unsafe { sys::ERR_clear_error() };
537            return None;
538        }
539    };
540    let rc = unsafe {
541        sys::ASN1_TIME_print(bio.as_ptr(), t.cast::<sys::ASN1_TIME>())
542    };
543    if rc != 1 {
544        unsafe { sys::ERR_clear_error() };
545        return None;
546    }
547    String::from_utf8(bio.into_vec()).ok()
548}
549
550// ── Tests ─────────────────────────────────────────────────────────────────────
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::pkey::{KeygenCtx, Pkey, Private, Public};
556    use crate::x509::{X509Builder, X509NameOwned};
557
558    /// Build a minimal CA + end-entity certificate pair for testing.
559    fn make_ca_and_ee() -> (crate::x509::X509, Pkey<Private>, crate::x509::X509, Pkey<Private>) {
560        // CA key + cert (self-signed)
561        let mut ca_kgen = KeygenCtx::new(c"ED25519").unwrap();
562        let ca_priv = ca_kgen.generate().unwrap();
563        let ca_pub = Pkey::<Public>::from(ca_priv.clone());
564
565        let mut ca_name = X509NameOwned::new().unwrap();
566        ca_name.add_entry_by_txt(c"CN", b"OCSP Test CA").unwrap();
567
568        let ca_cert = X509Builder::new().unwrap()
569            .set_version(2).unwrap()
570            .set_serial_number(1).unwrap()
571            .set_not_before_offset(0).unwrap()
572            .set_not_after_offset(365 * 86400).unwrap()
573            .set_subject_name(&ca_name).unwrap()
574            .set_issuer_name(&ca_name).unwrap()
575            .set_public_key(&ca_pub).unwrap()
576            .sign(&ca_priv, None).unwrap()
577            .build();
578
579        // EE key + cert (signed by CA)
580        let mut ee_kgen = KeygenCtx::new(c"ED25519").unwrap();
581        let ee_priv = ee_kgen.generate().unwrap();
582        let ee_pub = Pkey::<Public>::from(ee_priv.clone());
583
584        let mut ee_name = X509NameOwned::new().unwrap();
585        ee_name.add_entry_by_txt(c"CN", b"OCSP Test EE").unwrap();
586
587        let ee_cert = X509Builder::new().unwrap()
588            .set_version(2).unwrap()
589            .set_serial_number(2).unwrap()
590            .set_not_before_offset(0).unwrap()
591            .set_not_after_offset(365 * 86400).unwrap()
592            .set_subject_name(&ee_name).unwrap()
593            .set_issuer_name(&ca_name).unwrap()
594            .set_public_key(&ee_pub).unwrap()
595            .sign(&ca_priv, None).unwrap()
596            .build();
597
598        (ca_cert, ca_priv, ee_cert, ee_priv)
599    }
600
601    // ── OcspCertId tests ──────────────────────────────────────────────────────
602
603    #[test]
604    fn cert_id_from_cert() {
605        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
606        // SHA-1 is the OCSP default; pass None for the digest.
607        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
608        // Clone must not crash.
609        let _id2 = id.clone();
610    }
611
612    // ── OcspRequest tests ─────────────────────────────────────────────────────
613
614    #[test]
615    fn ocsp_request_new_and_to_der() {
616        let req = OcspRequest::new().unwrap();
617        let der = req.to_der().unwrap();
618        assert!(!der.is_empty());
619    }
620
621    #[test]
622    fn ocsp_request_with_cert_id() {
623        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
624        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
625
626        let mut req = OcspRequest::new().unwrap();
627        req.add_cert_id(id).unwrap();
628        let der = req.to_der().unwrap();
629        assert!(!der.is_empty());
630        // DER with a cert ID is larger than an empty request.
631        let empty_der = OcspRequest::new().unwrap().to_der().unwrap();
632        assert!(der.len() > empty_der.len());
633    }
634
635    #[test]
636    fn ocsp_request_der_roundtrip() {
637        let req = OcspRequest::new().unwrap();
638        let der = req.to_der().unwrap();
639        let req2 = OcspRequest::from_der(&der).unwrap();
640        assert_eq!(req2.to_der().unwrap(), der);
641    }
642
643    // ── OcspResponse tests ────────────────────────────────────────────────────
644
645    #[test]
646    fn ocsp_response_status_decode() {
647        let der = OcspResponse::new_successful_der();
648        let resp = OcspResponse::from_der(&der).unwrap();
649        assert_eq!(resp.status(), OcspResponseStatus::Successful);
650    }
651
652    #[test]
653    fn ocsp_response_der_roundtrip() {
654        let der = OcspResponse::new_successful_der();
655        let resp = OcspResponse::from_der(&der).unwrap();
656        assert_eq!(resp.to_der().unwrap(), der);
657    }
658
659    #[test]
660    fn ocsp_response_basic_fails_without_body() {
661        // A response with only a status code and no responseBytes has no basic resp.
662        let der = OcspResponse::new_successful_der();
663        let resp = OcspResponse::from_der(&der).unwrap();
664        // basic() should return Err because there is no responseBytes.
665        assert!(resp.basic().is_err());
666    }
667
668    // ── OcspBasicResp / find_status tests ────────────────────────────────────
669    //
670    // Building a real OCSP_BASICRESP from scratch requires the full OCSP
671    // responder stack (OCSP_basic_sign, OCSP_basic_add1_status) which is
672    // outside the scope of unit tests. Instead we verify that find_status
673    // returns None when the cert is not in the response (requires a real
674    // OCSP response DER), and test the X509Store/X509StoreCtx path via
675    // the integration-level store tests in x509.rs.
676    //
677    // The important invariants (OcspCertId::from_cert, add_cert_id, DER
678    // round-trip) are covered by the tests above.
679    //
680    // If a real OCSP response is available (e.g. from a test OCSP responder),
681    // use OcspResponse::from_der + basic() + find_status() to validate the
682    // full stack.
683}