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