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}