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`], and
87/// also by [`BorrowedOcspSingleResp::status`].
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum OcspCertStatus {
90    /// Certificate is currently valid (`V_OCSP_CERTSTATUS_GOOD = 0`).
91    Good,
92    /// Certificate has been revoked (`V_OCSP_CERTSTATUS_REVOKED = 1`).
93    Revoked,
94    /// Responder does not know this certificate (`V_OCSP_CERTSTATUS_UNKNOWN = 2`).
95    Unknown,
96}
97
98impl OcspCertStatus {
99    fn from_raw(status: i32) -> Self {
100        match status {
101            0 => Self::Good,
102            1 => Self::Revoked,
103            _ => Self::Unknown,
104        }
105    }
106}
107
108// ── OcspRevokeReason ──────────────────────────────────────────────────────────
109
110/// CRL revocation reason codes (RFC 5280 §5.3.1).
111///
112/// Carried inside [`OcspSingleStatus`] and [`SingleRespStatus`] when the
113/// certificate status is [`OcspCertStatus::Revoked`].  A value of `None` means
114/// no reason extension was present in the response (OpenSSL returns
115/// `OCSP_REVOKED_STATUS_NOSTATUS`, i.e. `-1`).
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum OcspRevokeReason {
118    /// `unspecified` (0).
119    Unspecified,
120    /// `keyCompromise` (1).
121    KeyCompromise,
122    /// `cACompromise` (2).
123    CaCompromise,
124    /// `affiliationChanged` (3).
125    AffiliationChanged,
126    /// `superseded` (4).
127    Superseded,
128    /// `cessationOfOperation` (5).
129    CessationOfOperation,
130    /// `certificateHold` (6).
131    CertificateHold,
132    /// `removeFromCRL` (8).
133    RemoveFromCrl,
134    /// `privilegeWithdrawn` (9).
135    PrivilegeWithdrawn,
136    /// `aACompromise` (10).
137    AaCompromise,
138    /// Any other numeric reason code (forward-compatibility guard).
139    Other(i32),
140}
141
142impl OcspRevokeReason {
143    fn from_raw(reason: i32) -> Option<Self> {
144        // -1 means "no reason given" (OCSP_REVOKED_STATUS_NOSTATUS).
145        if reason < 0 {
146            return None;
147        }
148        Some(match reason {
149            0 => Self::Unspecified,
150            1 => Self::KeyCompromise,
151            2 => Self::CaCompromise,
152            3 => Self::AffiliationChanged,
153            4 => Self::Superseded,
154            5 => Self::CessationOfOperation,
155            6 => Self::CertificateHold,
156            8 => Self::RemoveFromCrl,
157            9 => Self::PrivilegeWithdrawn,
158            10 => Self::AaCompromise,
159            n => Self::Other(n),
160        })
161    }
162}
163
164// ── OcspSingleStatus ──────────────────────────────────────────────────────────
165
166/// Status of a single certificate, returned by [`OcspBasicResp::find_status`].
167#[derive(Debug, Clone)]
168pub struct OcspSingleStatus {
169    /// Per-certificate status.
170    pub cert_status: OcspCertStatus,
171    /// Revocation reason code (RFC 5280 §5.3.1).
172    ///
173    /// Set only when `cert_status == Revoked` and the responder included a
174    /// reason code; `None` when the reason was absent or the cert is not revoked.
175    pub reason: Option<OcspRevokeReason>,
176    /// `thisUpdate` time as a human-readable UTC string, if present.
177    pub this_update: Option<String>,
178    /// `nextUpdate` time as a human-readable UTC string, if present.
179    pub next_update: Option<String>,
180    /// Revocation time as a human-readable UTC string (`cert_status == Revoked` only).
181    pub revocation_time: Option<String>,
182}
183
184// ── OcspCertId ───────────────────────────────────────────────────────────────
185
186/// Certificate identifier for OCSP (`OCSP_CERTID*`).
187///
188/// Created from a subject certificate and its issuer with [`OcspCertId::from_cert`].
189/// Add to a request with [`OcspRequest::add_cert_id`], or use to look up a status
190/// in a response with [`OcspBasicResp::find_status`].
191///
192/// `Clone` is implemented via `OCSP_CERTID_dup`.
193pub struct OcspCertId {
194    ptr: *mut sys::OCSP_CERTID,
195}
196
197unsafe impl Send for OcspCertId {}
198
199impl Clone for OcspCertId {
200    fn clone(&self) -> Self {
201        let ptr = unsafe { sys::OCSP_CERTID_dup(self.ptr) };
202        // OCSP_CERTID_dup returns null only on allocation failure; treat as abort.
203        assert!(!ptr.is_null(), "OCSP_CERTID_dup: allocation failure");
204        OcspCertId { ptr }
205    }
206}
207
208impl Drop for OcspCertId {
209    fn drop(&mut self) {
210        unsafe { sys::OCSP_CERTID_free(self.ptr) };
211    }
212}
213
214impl OcspCertId {
215    /// Allocate a new, empty `OCSP_CERTID`.
216    ///
217    /// The returned object has all fields zeroed.  Use [`OcspCertId::from_cert`]
218    /// to build a fully populated cert ID from a subject certificate and its
219    /// issuer instead.
220    ///
221    /// # Errors
222    ///
223    /// Returns `Err` if OpenSSL fails to allocate memory.
224    #[must_use = "returns a new OcspCertId that must be used or stored"]
225    pub fn new() -> Result<Self, ErrorStack> {
226        // SAFETY: OCSP_CERTID_new() is a plain allocator; it takes no arguments
227        // and returns null only on allocation failure.
228        let ptr = unsafe { sys::OCSP_CERTID_new() };
229        if ptr.is_null() {
230            return Err(ErrorStack::drain());
231        }
232        Ok(OcspCertId { ptr })
233    }
234
235    /// Build a cert ID from a subject certificate and its direct issuer.
236    ///
237    /// `digest` is the hash algorithm used to hash the issuer name and key
238    /// (default: SHA-1 when `None`, per RFC 6960).  SHA-1 is required by most
239    /// deployed OCSP responders; pass `Some(sha256_alg)` only when the responder
240    /// is known to support it.
241    ///
242    /// # Errors
243    pub fn from_cert(
244        digest: Option<&crate::digest::DigestAlg>,
245        subject: &crate::x509::X509,
246        issuer: &crate::x509::X509,
247    ) -> Result<Self, ErrorStack> {
248        let dgst_ptr = digest.map_or(std::ptr::null(), crate::digest::DigestAlg::as_ptr);
249        let ptr = unsafe { sys::OCSP_cert_to_id(dgst_ptr, subject.as_ptr(), issuer.as_ptr()) };
250        if ptr.is_null() {
251            return Err(ErrorStack::drain());
252        }
253        Ok(OcspCertId { ptr })
254    }
255}
256
257// ── OcspRequest ───────────────────────────────────────────────────────────────
258
259/// An OCSP request (`OCSP_REQUEST*`).
260///
261/// Build with [`OcspRequest::new`], populate with [`OcspRequest::add_cert_id`],
262/// then encode with [`OcspRequest::to_der`] and send to the OCSP responder.
263pub struct OcspRequest {
264    ptr: *mut sys::OCSP_REQUEST,
265}
266
267unsafe impl Send for OcspRequest {}
268
269impl Drop for OcspRequest {
270    fn drop(&mut self) {
271        unsafe { sys::OCSP_REQUEST_free(self.ptr) };
272    }
273}
274
275impl OcspRequest {
276    /// Create a new, empty OCSP request.
277    ///
278    /// # Errors
279    pub fn new() -> Result<Self, ErrorStack> {
280        let ptr = unsafe { sys::OCSP_REQUEST_new() };
281        if ptr.is_null() {
282            return Err(ErrorStack::drain());
283        }
284        Ok(OcspRequest { ptr })
285    }
286
287    /// Decode an OCSP request from DER bytes.
288    ///
289    /// # Errors
290    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
291        let mut ptr = std::ptr::null_mut::<sys::OCSP_REQUEST>();
292        let mut p = der.as_ptr();
293        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
294        let result = unsafe {
295            sys::d2i_OCSP_REQUEST(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
296        };
297        if result.is_null() {
298            return Err(ErrorStack::drain());
299        }
300        Ok(OcspRequest { ptr })
301    }
302
303    /// Add a certificate identifier to the request.
304    ///
305    /// `cert_id` ownership is transferred to the request (`add0` semantics);
306    /// the `OcspCertId` is consumed.
307    ///
308    /// # Errors
309    pub fn add_cert_id(&mut self, cert_id: OcspCertId) -> Result<(), ErrorStack> {
310        // OCSP_request_add0_id transfers ownership of the CERTID on success only.
311        // Do not forget cert_id until after the null check — on failure the CERTID
312        // is NOT consumed by OpenSSL and must still be freed by our Drop impl.
313        let rc = unsafe { sys::OCSP_request_add0_id(self.ptr, cert_id.ptr) };
314        if rc.is_null() {
315            return Err(ErrorStack::drain());
316        }
317        // Success: ownership transferred; suppress Drop.
318        std::mem::forget(cert_id);
319        Ok(())
320    }
321
322    /// Encode the OCSP request to DER bytes.
323    ///
324    /// # Errors
325    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
326        let len = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::null_mut()) };
327        if len < 0 {
328            return Err(ErrorStack::drain());
329        }
330        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
331        let mut buf = vec![0u8; len as usize];
332        let mut out_ptr = buf.as_mut_ptr();
333        let written = unsafe { sys::i2d_OCSP_REQUEST(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
334        if written < 0 {
335            return Err(ErrorStack::drain());
336        }
337        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
338        buf.truncate(written as usize);
339        Ok(buf)
340    }
341}
342
343// ── OcspSingleResp / BorrowedOcspSingleResp ───────────────────────────────────
344
345/// An individual `SingleResponse` entry inside an `OCSP_BASICRESP`
346/// (`OCSP_SINGLERESP*`).
347///
348/// Not usually constructed directly; obtain a borrowed view via
349/// [`OcspBasicResp::get_response`].
350pub struct OcspSingleResp {
351    ptr: *mut sys::OCSP_SINGLERESP,
352}
353
354unsafe impl Send for OcspSingleResp {}
355
356impl Drop for OcspSingleResp {
357    fn drop(&mut self) {
358        // OCSP_SINGLERESP objects are owned by their parent OCSP_BASICRESP.
359        // They must NOT be freed independently; this Drop is intentionally a
360        // no-op.  The owning OcspBasicResp will free them via OCSP_BASICRESP_free.
361    }
362}
363
364/// A borrowed `OCSP_SINGLERESP*` whose lifetime is tied to its parent
365/// [`OcspBasicResp`].
366///
367/// Obtained from [`OcspBasicResp::get_response`].  Does **not** free the
368/// underlying pointer on drop — the pointer is owned by the `OCSP_BASICRESP`
369/// and freed when the [`OcspBasicResp`] is dropped.
370pub struct BorrowedOcspSingleResp<'a> {
371    inner: std::mem::ManuallyDrop<OcspSingleResp>,
372    _lifetime: std::marker::PhantomData<&'a OcspBasicResp>,
373}
374
375/// Status of a single certificate entry, returned by
376/// [`BorrowedOcspSingleResp::status`].
377#[derive(Debug, Clone)]
378pub struct SingleRespStatus {
379    /// Per-certificate revocation status.
380    pub cert_status: OcspCertStatus,
381    /// Revocation reason, set only when `cert_status == Revoked` and the
382    /// responder included a reason code.
383    pub reason: Option<OcspRevokeReason>,
384    /// `thisUpdate` time as a human-readable UTC string, if present.
385    pub this_update: Option<String>,
386    /// `nextUpdate` time as a human-readable UTC string, if present.
387    pub next_update: Option<String>,
388    /// Revocation time as a human-readable UTC string; `Some` only when
389    /// `cert_status == Revoked` and the time was provided.
390    pub revocation_time: Option<String>,
391}
392
393impl BorrowedOcspSingleResp<'_> {
394    /// Extract the revocation status from this `SingleResponse` entry.
395    ///
396    /// Calls `OCSP_single_get0_status` to retrieve the certificate status,
397    /// optional revocation reason, and the `thisUpdate`/`nextUpdate`/
398    /// `revocationTime` timestamps.
399    ///
400    /// # Errors
401    ///
402    /// Returns `Err` if OpenSSL returns an unrecognised status code (< 0).
403    pub fn status(&self) -> Result<SingleRespStatus, ErrorStack> {
404        let mut reason: i32 = -1;
405        let mut revtime: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
406        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
407        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
408
409        // SAFETY: self.inner.ptr is a valid non-null OCSP_SINGLERESP* borrowed
410        // from the parent OcspBasicResp for lifetime 'a.  The out-pointer
411        // arguments are all valid stack-allocated locals.  The returned
412        // ASN1_GENERALIZEDTIME* pointers are borrowed from the single response
413        // and are valid for the same lifetime.
414        let raw_status = unsafe {
415            sys::OCSP_single_get0_status(
416                self.inner.ptr,
417                std::ptr::addr_of_mut!(reason),
418                std::ptr::addr_of_mut!(revtime),
419                std::ptr::addr_of_mut!(thisupd),
420                std::ptr::addr_of_mut!(nextupd),
421            )
422        };
423
424        if raw_status < 0 {
425            return Err(ErrorStack::drain());
426        }
427
428        let cert_status = OcspCertStatus::from_raw(raw_status);
429        let revoke_reason = if cert_status == OcspCertStatus::Revoked {
430            OcspRevokeReason::from_raw(reason)
431        } else {
432            None
433        };
434
435        Ok(SingleRespStatus {
436            cert_status,
437            reason: revoke_reason,
438            this_update: generalizedtime_to_str(thisupd),
439            next_update: generalizedtime_to_str(nextupd),
440            revocation_time: generalizedtime_to_str(revtime),
441        })
442    }
443}
444
445// ── OcspBasicResp ─────────────────────────────────────────────────────────────
446
447/// The signed inner OCSP response (`OCSP_BASICRESP*`).
448///
449/// Extracted from an [`OcspResponse`] via [`OcspResponse::basic`], or
450/// allocated fresh with [`OcspBasicResp::new`] for responder-side use.
451/// Provides signature verification and per-certificate status lookup.
452pub struct OcspBasicResp {
453    ptr: *mut sys::OCSP_BASICRESP,
454}
455
456unsafe impl Send for OcspBasicResp {}
457
458impl Drop for OcspBasicResp {
459    fn drop(&mut self) {
460        unsafe { sys::OCSP_BASICRESP_free(self.ptr) };
461    }
462}
463
464impl OcspBasicResp {
465    /// Allocate a new, empty `OCSP_BASICRESP`.
466    ///
467    /// Useful on the responder side when building a basic response from scratch.
468    ///
469    /// # Errors
470    ///
471    /// Returns `Err` if OpenSSL fails to allocate memory.
472    #[must_use = "returns a new OcspBasicResp that must be used or stored"]
473    pub fn new() -> Result<Self, ErrorStack> {
474        // SAFETY: OCSP_BASICRESP_new() is a plain allocator; it takes no
475        // arguments and returns null only on allocation failure.
476        let ptr = unsafe { sys::OCSP_BASICRESP_new() };
477        if ptr.is_null() {
478            return Err(ErrorStack::drain());
479        }
480        Ok(OcspBasicResp { ptr })
481    }
482
483    /// Get the `SingleResponse` at position `idx` in this basic response.
484    ///
485    /// Returns `None` if `idx` is out of range (>= [`Self::count`]).
486    ///
487    /// The returned [`BorrowedOcspSingleResp`] is borrowed from `self` and
488    /// must not outlive it.
489    #[must_use]
490    pub fn get_response(&self, idx: usize) -> Option<BorrowedOcspSingleResp<'_>> {
491        // Convert usize → c_int; treat overflow as out-of-range.
492        let idx_c = i32::try_from(idx).ok()?;
493        // SAFETY: self.ptr is a valid non-null OCSP_BASICRESP*.
494        // OCSP_resp_get0 returns a borrowed pointer into the BASICRESP's
495        // internal stack; lifetime is tied to self via BorrowedOcspSingleResp.
496        let ptr = unsafe { sys::OCSP_resp_get0(self.ptr, idx_c) };
497        if ptr.is_null() {
498            return None;
499        }
500        Some(BorrowedOcspSingleResp {
501            inner: std::mem::ManuallyDrop::new(OcspSingleResp { ptr }),
502            _lifetime: std::marker::PhantomData,
503        })
504    }
505
506    /// Find the index of the first `SingleResponse` matching `id`.
507    ///
508    /// Searches from the beginning of the response list.  Returns `Some(idx)`
509    /// with the zero-based index of the matching entry, or `None` if no
510    /// matching entry was found.
511    ///
512    /// Use [`Self::get_response`] with the returned index to access the entry.
513    #[must_use]
514    pub fn find_response(&self, id: &OcspCertId) -> Option<usize> {
515        // SAFETY: self.ptr and id.ptr are both valid non-null pointers.
516        // OCSP_resp_find does not store the id pointer after returning.
517        // last = -1 means "start from the beginning".
518        let idx = unsafe { sys::OCSP_resp_find(self.ptr, id.ptr, -1) };
519        if idx < 0 {
520            return None;
521        }
522        // idx >= 0 is safe to cast.
523        #[allow(clippy::cast_sign_loss)]
524        Some(idx as usize)
525    }
526
527    /// Verify the response signature against `store`.
528    ///
529    /// `flags` is passed directly to `OCSP_basic_verify` (use 0 for defaults,
530    /// which verifies the signature and checks the signing certificate chain).
531    ///
532    /// Returns `Ok(true)` if the signature is valid.
533    ///
534    /// # Errors
535    pub fn verify(&self, store: &crate::x509::X509Store, flags: u64) -> Result<bool, ErrorStack> {
536        match unsafe {
537            sys::OCSP_basic_verify(self.ptr, std::ptr::null_mut(), store.as_ptr(), flags)
538        } {
539            1 => Ok(true),
540            0 => Ok(false),
541            _ => Err(ErrorStack::drain()),
542        }
543    }
544
545    /// Number of `SingleResponse` entries in this basic response.
546    #[must_use]
547    pub fn count(&self) -> usize {
548        let n = unsafe { sys::OCSP_resp_count(self.ptr) };
549        usize::try_from(n).unwrap_or(0)
550    }
551
552    /// Look up the status for a specific certificate by its [`OcspCertId`].
553    ///
554    /// Returns `Ok(Some(status))` if the responder included a `SingleResponse`
555    /// for that certificate, `Ok(None)` if not found, or `Err` on a fatal
556    /// OpenSSL error.
557    ///
558    /// The `cert_id` is passed by shared reference; its pointer is only used
559    /// for the duration of this call (`OCSP_resp_find_status` does not store it).
560    ///
561    /// # Errors
562    pub fn find_status(
563        &self,
564        cert_id: &OcspCertId,
565    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
566        let mut status: i32 = -1;
567        let mut reason: i32 = -1;
568        let mut revtime: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
569        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
570        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
571
572        let rc = unsafe {
573            sys::OCSP_resp_find_status(
574                self.ptr,
575                cert_id.ptr,
576                std::ptr::addr_of_mut!(status),
577                std::ptr::addr_of_mut!(reason),
578                std::ptr::addr_of_mut!(revtime),
579                std::ptr::addr_of_mut!(thisupd),
580                std::ptr::addr_of_mut!(nextupd),
581            )
582        };
583
584        // rc == 1 → found; rc == 0 → not found; anything else → error.
585        match rc {
586            1 => Ok(Some(OcspSingleStatus {
587                cert_status: OcspCertStatus::from_raw(status),
588                reason: OcspRevokeReason::from_raw(reason),
589                this_update: generalizedtime_to_str(thisupd),
590                next_update: generalizedtime_to_str(nextupd),
591                revocation_time: generalizedtime_to_str(revtime),
592            })),
593            0 => Ok(None),
594            _ => Err(ErrorStack::drain()),
595        }
596    }
597
598    /// Validate the `thisUpdate` / `nextUpdate` window of a `SingleResponse`.
599    ///
600    /// `sec` is the acceptable clock-skew in seconds (typically 300).
601    /// `maxsec` limits how far in the future `nextUpdate` may be (-1 = no limit).
602    ///
603    /// # Errors
604    pub fn check_validity(
605        &self,
606        cert_id: &OcspCertId,
607        sec: i64,
608        maxsec: i64,
609    ) -> Result<bool, ErrorStack> {
610        // Re-run find_status to get thisupd / nextupd pointers.
611        let mut thisupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
612        let mut nextupd: *mut sys::ASN1_GENERALIZEDTIME = std::ptr::null_mut();
613        let rc = unsafe {
614            sys::OCSP_resp_find_status(
615                self.ptr,
616                cert_id.ptr,
617                std::ptr::null_mut(), // status
618                std::ptr::null_mut(), // reason
619                std::ptr::null_mut(), // revtime
620                std::ptr::addr_of_mut!(thisupd),
621                std::ptr::addr_of_mut!(nextupd),
622            )
623        };
624        // rc == 1 → found; rc == 0 → not found; negative → fatal error.
625        match rc {
626            1 => {}
627            0 => return Ok(false),
628            _ => return Err(ErrorStack::drain()),
629        }
630        match unsafe { sys::OCSP_check_validity(thisupd, nextupd, sec, maxsec) } {
631            1 => Ok(true),
632            0 => Ok(false),
633            _ => Err(ErrorStack::drain()),
634        }
635    }
636}
637
638// ── OcspResponse ──────────────────────────────────────────────────────────────
639
640/// An OCSP response (`OCSP_RESPONSE*`).
641///
642/// Decode from DER with [`OcspResponse::from_der`].  Check the top-level
643/// [`OcspResponse::status`], then extract the signed inner response with
644/// [`OcspResponse::basic`] for per-certificate status lookup.
645pub struct OcspResponse {
646    ptr: *mut sys::OCSP_RESPONSE,
647}
648
649unsafe impl Send for OcspResponse {}
650
651impl Drop for OcspResponse {
652    fn drop(&mut self) {
653        unsafe { sys::OCSP_RESPONSE_free(self.ptr) };
654    }
655}
656
657impl OcspResponse {
658    /// Allocate a new, empty `OCSP_RESPONSE`.
659    ///
660    /// Useful on the responder side when building a response from scratch.
661    ///
662    /// # Errors
663    ///
664    /// Returns `Err` if OpenSSL fails to allocate memory.
665    #[must_use = "returns a new OcspResponse that must be used or stored"]
666    pub fn new() -> Result<Self, ErrorStack> {
667        // SAFETY: OCSP_RESPONSE_new() is a plain allocator; it takes no
668        // arguments and returns null only on allocation failure.
669        let ptr = unsafe { sys::OCSP_RESPONSE_new() };
670        if ptr.is_null() {
671            return Err(ErrorStack::drain());
672        }
673        Ok(OcspResponse { ptr })
674    }
675
676    /// Decode an OCSP response from DER bytes.
677    ///
678    /// # Errors
679    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
680        let mut ptr = std::ptr::null_mut::<sys::OCSP_RESPONSE>();
681        let mut p = der.as_ptr();
682        let len = i64::try_from(der.len()).unwrap_or(i64::MAX);
683        let result = unsafe {
684            sys::d2i_OCSP_RESPONSE(std::ptr::addr_of_mut!(ptr), std::ptr::addr_of_mut!(p), len)
685        };
686        if result.is_null() {
687            return Err(ErrorStack::drain());
688        }
689        Ok(OcspResponse { ptr })
690    }
691
692    /// Encode the OCSP response to DER bytes.
693    ///
694    /// # Errors
695    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
696        let len = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::null_mut()) };
697        if len < 0 {
698            return Err(ErrorStack::drain());
699        }
700        #[allow(clippy::cast_sign_loss)] // len > 0 checked above
701        let mut buf = vec![0u8; len as usize];
702        let mut out_ptr = buf.as_mut_ptr();
703        let written = unsafe { sys::i2d_OCSP_RESPONSE(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
704        if written < 0 {
705            return Err(ErrorStack::drain());
706        }
707        #[allow(clippy::cast_sign_loss)] // written >= 0 checked above
708        buf.truncate(written as usize);
709        Ok(buf)
710    }
711
712    /// Overall OCSP response status (top-level packet status, not cert status).
713    ///
714    /// A `Successful` value means the server processed the request; it does not
715    /// mean any individual certificate is good.  Use [`Self::basic`] and then
716    /// [`OcspBasicResp::find_status`] for per-certificate results.
717    #[must_use]
718    pub fn status(&self) -> OcspResponseStatus {
719        OcspResponseStatus::from(unsafe { sys::OCSP_response_status(self.ptr) })
720    }
721
722    /// Extract the signed inner response (`OCSP_BASICRESP*`).
723    ///
724    /// Only valid when [`Self::status`] is [`OcspResponseStatus::Successful`].
725    ///
726    /// # Errors
727    ///
728    /// Returns `Err` if the response has no basic response body (e.g. the
729    /// top-level status is not `Successful`).
730    pub fn basic(&self) -> Result<OcspBasicResp, ErrorStack> {
731        let ptr = unsafe { sys::OCSP_response_get1_basic(self.ptr) };
732        if ptr.is_null() {
733            return Err(ErrorStack::drain());
734        }
735        Ok(OcspBasicResp { ptr })
736    }
737
738    /// Convenience: verify the basic response signature and look up a cert status
739    /// in one call.
740    ///
741    /// Equivalent to `resp.basic()?.verify(store, 0)?; resp.basic()?.find_status(id)`.
742    ///
743    /// # Errors
744    pub fn verified_status(
745        &self,
746        store: &crate::x509::X509Store,
747        cert_id: &OcspCertId,
748    ) -> Result<Option<OcspSingleStatus>, ErrorStack> {
749        let basic = self.basic()?;
750        // verify() returns Ok(false) when the signature is invalid — treat that
751        // as an error to prevent certificate status from an unverified response.
752        if !basic.verify(store, 0)? {
753            return Err(ErrorStack::drain());
754        }
755        basic.find_status(cert_id)
756    }
757
758    /// Build a minimal `OCSP_RESPONSE` (status = successful, no basic response)
759    /// and return it as DER. Used for testing only.
760    #[cfg(test)]
761    fn new_successful_der() -> Vec<u8> {
762        // DER: SEQUENCE { ENUMERATED 0 }
763        // OCSPResponseStatus successful(0) with no responseBytes.
764        vec![0x30, 0x03, 0x0A, 0x01, 0x00]
765    }
766}
767
768// ── Private helpers ───────────────────────────────────────────────────────────
769
770/// Convert an `ASN1_GENERALIZEDTIME*` (which is really `ASN1_STRING*`) to a
771/// human-readable string via `ASN1_TIME_print` on a memory BIO.
772fn generalizedtime_to_str(t: *mut sys::ASN1_GENERALIZEDTIME) -> Option<String> {
773    if t.is_null() {
774        return None;
775    }
776    // ASN1_GENERALIZEDTIME is typedef'd to asn1_string_st, same as ASN1_TIME.
777    // ASN1_TIME_print handles both UTCTime and GeneralizedTime.
778    let Ok(mut bio) = MemBio::new() else {
779        // BIO allocation failed; clear the error queue so callers do not
780        // see a stale allocation error as if it came from their own call.
781        unsafe { sys::ERR_clear_error() };
782        return None;
783    };
784    let rc = unsafe { sys::ASN1_TIME_print(bio.as_ptr(), t.cast::<sys::ASN1_TIME>()) };
785    if rc != 1 {
786        unsafe { sys::ERR_clear_error() };
787        return None;
788    }
789    String::from_utf8(bio.into_vec()).ok()
790}
791
792// ── Tests ─────────────────────────────────────────────────────────────────────
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use crate::pkey::{KeygenCtx, Pkey, Private, Public};
798    use crate::x509::{X509Builder, X509NameOwned};
799
800    /// Build a minimal CA + end-entity certificate pair for testing.
801    fn make_ca_and_ee() -> (
802        crate::x509::X509,
803        Pkey<Private>,
804        crate::x509::X509,
805        Pkey<Private>,
806    ) {
807        // CA key + cert (self-signed)
808        let mut ca_kgen = KeygenCtx::new(c"ED25519").unwrap();
809        let ca_priv = ca_kgen.generate().unwrap();
810        let ca_pub = Pkey::<Public>::from(ca_priv.clone());
811
812        let mut ca_name = X509NameOwned::new().unwrap();
813        ca_name.add_entry_by_txt(c"CN", b"OCSP Test CA").unwrap();
814
815        let ca_cert = X509Builder::new()
816            .unwrap()
817            .set_version(2)
818            .unwrap()
819            .set_serial_number(1)
820            .unwrap()
821            .set_not_before_offset(0)
822            .unwrap()
823            .set_not_after_offset(365 * 86400)
824            .unwrap()
825            .set_subject_name(&ca_name)
826            .unwrap()
827            .set_issuer_name(&ca_name)
828            .unwrap()
829            .set_public_key(&ca_pub)
830            .unwrap()
831            .sign(&ca_priv, None)
832            .unwrap()
833            .build();
834
835        // EE key + cert (signed by CA)
836        let mut ee_kgen = KeygenCtx::new(c"ED25519").unwrap();
837        let ee_priv = ee_kgen.generate().unwrap();
838        let ee_pub = Pkey::<Public>::from(ee_priv.clone());
839
840        let mut ee_name = X509NameOwned::new().unwrap();
841        ee_name.add_entry_by_txt(c"CN", b"OCSP Test EE").unwrap();
842
843        let ee_cert = X509Builder::new()
844            .unwrap()
845            .set_version(2)
846            .unwrap()
847            .set_serial_number(2)
848            .unwrap()
849            .set_not_before_offset(0)
850            .unwrap()
851            .set_not_after_offset(365 * 86400)
852            .unwrap()
853            .set_subject_name(&ee_name)
854            .unwrap()
855            .set_issuer_name(&ca_name)
856            .unwrap()
857            .set_public_key(&ee_pub)
858            .unwrap()
859            .sign(&ca_priv, None)
860            .unwrap()
861            .build();
862
863        (ca_cert, ca_priv, ee_cert, ee_priv)
864    }
865
866    // ── OcspCertId tests ──────────────────────────────────────────────────────
867
868    #[test]
869    fn cert_id_from_cert() {
870        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
871        // SHA-1 is the OCSP default; pass None for the digest.
872        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
873        // Clone must not crash.
874        let _id2 = id.clone();
875    }
876
877    // ── OcspRequest tests ─────────────────────────────────────────────────────
878
879    #[test]
880    fn ocsp_request_new_and_to_der() {
881        let req = OcspRequest::new().unwrap();
882        let der = req.to_der().unwrap();
883        assert!(!der.is_empty());
884    }
885
886    #[test]
887    fn ocsp_request_with_cert_id() {
888        let (ca_cert, _, ee_cert, _) = make_ca_and_ee();
889        let id = OcspCertId::from_cert(None, &ee_cert, &ca_cert).unwrap();
890
891        let mut req = OcspRequest::new().unwrap();
892        req.add_cert_id(id).unwrap();
893        let der = req.to_der().unwrap();
894        assert!(!der.is_empty());
895        // DER with a cert ID is larger than an empty request.
896        let empty_der = OcspRequest::new().unwrap().to_der().unwrap();
897        assert!(der.len() > empty_der.len());
898    }
899
900    #[test]
901    fn ocsp_request_der_roundtrip() {
902        let req = OcspRequest::new().unwrap();
903        let der = req.to_der().unwrap();
904        let req2 = OcspRequest::from_der(&der).unwrap();
905        assert_eq!(req2.to_der().unwrap(), der);
906    }
907
908    // ── OcspResponse tests ────────────────────────────────────────────────────
909
910    #[test]
911    fn ocsp_response_status_decode() {
912        let der = OcspResponse::new_successful_der();
913        let resp = OcspResponse::from_der(&der).unwrap();
914        assert_eq!(resp.status(), OcspResponseStatus::Successful);
915    }
916
917    #[test]
918    fn ocsp_response_der_roundtrip() {
919        let der = OcspResponse::new_successful_der();
920        let resp = OcspResponse::from_der(&der).unwrap();
921        assert_eq!(resp.to_der().unwrap(), der);
922    }
923
924    #[test]
925    fn ocsp_response_basic_fails_without_body() {
926        // A response with only a status code and no responseBytes has no basic resp.
927        let der = OcspResponse::new_successful_der();
928        let resp = OcspResponse::from_der(&der).unwrap();
929        // basic() should return Err because there is no responseBytes.
930        assert!(resp.basic().is_err());
931    }
932
933    // ── OcspBasicResp / find_status tests ────────────────────────────────────
934    //
935    // Building a real OCSP_BASICRESP from scratch requires the full OCSP
936    // responder stack (OCSP_basic_sign, OCSP_basic_add1_status) which is
937    // outside the scope of unit tests. Instead we verify that find_status
938    // returns None when the cert is not in the response (requires a real
939    // OCSP response DER), and test the X509Store/X509StoreCtx path via
940    // the integration-level store tests in x509.rs.
941    //
942    // The important invariants (OcspCertId::from_cert, add_cert_id, DER
943    // round-trip) are covered by the tests above.
944    //
945    // If a real OCSP response is available (e.g. from a test OCSP responder),
946    // use OcspResponse::from_der + basic() + find_status() to validate the
947    // full stack.
948
949    // ── Constructor smoke tests ───────────────────────────────────────────────
950
951    #[test]
952    fn ocsp_basicresp_new_and_drop() {
953        // OcspBasicResp::new() must succeed and the value must drop cleanly.
954        let resp = OcspBasicResp::new().unwrap();
955        drop(resp);
956    }
957
958    #[test]
959    fn ocsp_certid_new_and_drop() {
960        // OcspCertId::new() must succeed and the value must drop cleanly.
961        let id = OcspCertId::new().unwrap();
962        drop(id);
963    }
964
965    #[test]
966    fn ocsp_response_new_and_drop() {
967        // OcspResponse::new() must succeed and the value must drop cleanly.
968        let resp = OcspResponse::new().unwrap();
969        drop(resp);
970    }
971
972    // ── get_response / find_response tests ───────────────────────────────────
973
974    #[test]
975    fn ocsp_resp_get0_out_of_range() {
976        // A fresh, empty OcspBasicResp has no entries; get_response(0) must
977        // return None rather than panicking or returning a dangling pointer.
978        let resp = OcspBasicResp::new().unwrap();
979        assert!(resp.get_response(0).is_none());
980    }
981
982    #[test]
983    fn ocsp_find_response_empty() {
984        // find_response on an empty basic response must return None.
985        let resp = OcspBasicResp::new().unwrap();
986        let id = OcspCertId::new().unwrap();
987        assert!(resp.find_response(&id).is_none());
988    }
989
990    // NOTE: Testing find_response and BorrowedOcspSingleResp::status with a
991    // real populated response requires the full responder stack
992    // (OCSP_basic_add1_status, OCSP_basic_sign).  That is integration-level
993    // work tracked as a follow-up TODO.
994}