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}