1use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33use dashmap::DashMap;
34use serde::Deserialize;
35use sha2::{Digest, Sha256};
36use synta::{Decoder, Encoding, ToDer};
37use synta_certificate::SignatureVerifier;
38use thiserror::Error;
39use tracing::{debug, info, warn};
40
41#[derive(Debug, Error)]
43pub enum OcspError {
44 #[error("OCSP request build error: {0}")]
46 RequestBuild(String),
47
48 #[error("OCSP transport error: {0}")]
50 Transport(String),
51
52 #[error("OCSP response status: {0}")]
55 ResponseStatus(String),
56
57 #[error("OCSP response signature verification failed: {0}")]
59 SignatureVerification(String),
60
61 #[error("OCSP response missing status for queried certificate")]
63 MissingCertStatus,
64
65 #[error("OCSP nonce mismatch: replay attack possible")]
67 NonceMismatch,
68
69 #[error("OCSP response parse error: {0}")]
71 Parse(String),
72
73 #[error("OCSP responder timeout after {0}s")]
75 Timeout(u64),
76}
77
78pub type OcspResult<T> = Result<T, OcspError>;
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum OcspStatus {
84 Good,
86
87 Revoked {
92 reason: String,
94 revocation_time: String,
96 },
97
98 Unknown,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub struct CertId {
116 pub hash_algorithm: String,
118 pub issuer_name_hash: Vec<u8>,
120 pub issuer_key_hash: Vec<u8>,
122 pub serial_number: Vec<u8>,
124}
125
126#[derive(Debug, Clone)]
128struct CachedOcspResponse {
129 status: OcspStatus,
131 cached_at: Instant,
133 ttl: Duration,
135}
136
137impl CachedOcspResponse {
138 fn is_expired(&self) -> bool {
139 self.cached_at.elapsed() > self.ttl
140 }
141}
142
143#[derive(Debug, Clone, Deserialize)]
147#[serde(default)]
148pub struct OcspConfig {
149 pub enabled: bool,
151
152 pub responder_url: Option<String>,
155
156 pub cache_ttl_secs: u64,
158
159 pub timeout_secs: u64,
161
162 pub require_nonce: bool,
165
166 pub soft_fail: bool,
169}
170
171impl Default for OcspConfig {
172 fn default() -> Self {
173 Self {
174 enabled: false,
175 responder_url: None,
176 cache_ttl_secs: 300,
177 timeout_secs: 10,
178 require_nonce: true,
179 soft_fail: false,
180 }
181 }
182}
183
184pub struct OcspClient {
189 config: OcspConfig,
190 cache: Arc<DashMap<CertId, CachedOcspResponse>>,
192}
193
194impl OcspClient {
195 pub fn new(config: OcspConfig) -> Self {
197 Self {
198 config,
199 cache: Arc::new(DashMap::new()),
200 }
201 }
202
203 pub async fn check_certificate_status(
220 &self,
221 cert_der: &[u8],
222 issuer_der: &[u8],
223 ) -> OcspResult<OcspStatus> {
224 if !self.config.enabled {
225 return Ok(OcspStatus::Good);
226 }
227
228 let cert_id = self.build_cert_id(cert_der, issuer_der)?;
230
231 if let Some(cached) = self.cache.get(&cert_id)
233 && !cached.is_expired()
234 {
235 debug!("OCSP cache hit for certificate");
236 return Ok(cached.status.clone());
237 }
238
239 let responder_url = self.resolve_responder_url(cert_der)?;
241
242 let nonce = if self.config.require_nonce {
244 use rand::Rng;
245 let mut n = vec![0; 32];
246 rand::rng().fill_bytes(&mut n);
247 Some(n)
248 } else {
249 None
250 };
251 let request_der = self.build_ocsp_request_with_nonce(&cert_id, nonce.as_deref())?;
252
253 let response_der = self.send_ocsp_request(&responder_url, &request_der).await?;
255
256 let mut valid_for = Duration::ZERO;
258 let status = self.parse_ocsp_response(
259 &response_der,
260 &cert_id,
261 issuer_der,
262 nonce.as_deref(),
263 &mut valid_for,
264 )?;
265
266 self.cache.insert(
268 cert_id,
269 CachedOcspResponse {
270 status: status.clone(),
271 cached_at: Instant::now(),
272 ttl: valid_for.min(Duration::from_secs(self.config.cache_ttl_secs)),
273 },
274 );
275
276 Ok(status)
277 }
278
279 pub async fn get_stapled_response(
295 &self,
296 server_cert_der: &[u8],
297 issuer_der: &[u8],
298 ) -> OcspResult<Vec<u8>> {
299 if !self.config.enabled {
300 return Err(OcspError::Transport("OCSP not enabled".to_string()));
301 }
302
303 let cert_id = self.build_cert_id(server_cert_der, issuer_der)?;
304 let responder_url = self.resolve_responder_url(server_cert_der)?;
305 let request_der = self.build_ocsp_request(&cert_id)?;
306
307 let response = self.send_ocsp_request(&responder_url, &request_der).await?;
308 let mut valid_for = Duration::ZERO;
309 self.parse_ocsp_response(&response, &cert_id, issuer_der, None, &mut valid_for)?;
310 Ok(response)
311 }
312
313 pub fn evict_expired(&self) {
315 self.cache.retain(|_, v| !v.is_expired());
316 }
317
318 pub fn cache_size(&self) -> usize {
320 self.cache.len()
321 }
322
323 fn build_cert_id(&self, cert_der: &[u8], issuer_der: &[u8]) -> OcspResult<CertId> {
330 if cert_der.is_empty() {
331 return Err(OcspError::RequestBuild("empty certificate DER".to_string()));
332 }
333 if issuer_der.is_empty() {
334 return Err(OcspError::RequestBuild(
335 "empty issuer certificate DER".to_string(),
336 ));
337 }
338
339 let cert = synta_certificate::Certificate::from_der(cert_der)
341 .map_err(|e| OcspError::RequestBuild(format!("certificate parse failed: {e}")))?;
342
343 let issuer_cert = synta_certificate::Certificate::from_der(issuer_der).map_err(|e| {
346 OcspError::RequestBuild(format!("issuer certificate parse failed: {e}"))
347 })?;
348
349 let issuer_name_der = issuer_cert.tbs_certificate.subject.0;
353 let issuer_name_hash = Sha256::digest(issuer_name_der).to_vec();
354
355 let issuer_spki = &issuer_cert.tbs_certificate.subject_public_key_info;
358 let issuer_key_bytes = issuer_spki.subject_public_key.as_bytes();
359 let issuer_key_hash = Sha256::digest(issuer_key_bytes).to_vec();
360
361 let serial_number = cert
363 .tbs_certificate
364 .serial_number
365 .to_der()
366 .map_err(|e| OcspError::RequestBuild(format!("serial encode failed: {e}")))?;
367 let serial_value = extract_integer_value(&serial_number)
370 .ok_or_else(|| OcspError::RequestBuild("malformed serial INTEGER".into()))?;
371
372 Ok(CertId {
373 hash_algorithm: "2.16.840.1.101.3.4.2.1".to_string(), issuer_name_hash,
375 issuer_key_hash,
376 serial_number: serial_value.to_vec(),
377 })
378 }
379
380 fn resolve_responder_url(&self, cert_der: &[u8]) -> OcspResult<String> {
382 if let Some(ref url) = self.config.responder_url {
383 return Ok(url.clone());
384 }
385
386 let cert = synta_certificate::Certificate::from_der(cert_der)
389 .map_err(|e| OcspError::RequestBuild(format!("certificate parse for AIA: {e}")))?;
390
391 if let Some(ref exts_raw) = cert.tbs_certificate.extensions
394 && let Some(aia_value) = synta_certificate::find_extension_value(
395 exts_raw.as_bytes(),
396 synta_certificate::oids::AUTHORITY_INFO_ACCESS,
397 )
398 && let Some(url) = extract_ocsp_url_from_aia(aia_value)
399 {
400 debug!(url = %url, "resolved OCSP responder URL from AIA extension");
401 return Ok(url);
402 }
403
404 Err(OcspError::RequestBuild(
405 "no OCSP responder URL configured and no AIA OCSP URI found in certificate".into(),
406 ))
407 }
408
409 fn build_ocsp_request(&self, cert_id: &CertId) -> OcspResult<Vec<u8>> {
425 let sha256_alg_der = sha256_algorithm_identifier_der();
428
429 let request_der = synta_certificate::OCSPRequestBuilder::new()
430 .add_request(synta_certificate::CertIDSpec {
431 hash_algorithm_der: &sha256_alg_der,
432 issuer_name_hash: &cert_id.issuer_name_hash,
433 issuer_key_hash: &cert_id.issuer_key_hash,
434 serial: &cert_id.serial_number,
435 })
436 .build_tbs()
437 .map_err(|e| OcspError::RequestBuild(format!("OCSPRequest build: {e}")))?;
438
439 debug!(request_len = request_der.len(), "built OCSP request DER");
440
441 Ok(request_der)
442 }
443
444 fn build_ocsp_request_with_nonce(
445 &self,
446 cert_id: &CertId,
447 nonce: Option<&[u8]>,
448 ) -> OcspResult<Vec<u8>> {
449 let raw = self.build_ocsp_request(cert_id)?;
450 let Some(nonce) = nonce else {
451 return Ok(raw);
452 };
453 let mut request = synta_certificate::ocsp_2024_88_types::OCSPRequest::from_der(&raw)
454 .map_err(|e| OcspError::RequestBuild(e.to_string()))?;
455 let encoded = synta::OctetStringRef::new(nonce)
456 .to_der()
457 .map_err(|e| OcspError::RequestBuild(e.to_string()))?;
458 request.tbs_request.request_extensions = Some(vec![synta_certificate::Extension {
459 extn_id: synta::ObjectIdentifier::new(&[1, 3, 6, 1, 5, 5, 7, 48, 1, 2])
460 .map_err(|e| OcspError::RequestBuild(e.to_string()))?,
461 critical: None,
462 extn_value: synta::OctetStringRef::new(&encoded),
463 }]);
464 request
465 .to_der()
466 .map_err(|e| OcspError::RequestBuild(e.to_string()))
467 }
468
469 async fn send_ocsp_request(
476 &self,
477 responder_url: &str,
478 request_der: &[u8],
479 ) -> OcspResult<Vec<u8>> {
480 let timeout = Duration::from_secs(self.config.timeout_secs);
481
482 let client = reqwest::Client::builder()
483 .timeout(timeout)
484 .build()
485 .map_err(|e| OcspError::Transport(format!("HTTP client build: {e}")))?;
486
487 info!(
488 url = %responder_url,
489 request_len = request_der.len(),
490 timeout_secs = self.config.timeout_secs,
491 "sending OCSP request"
492 );
493
494 let mut response = client
495 .post(responder_url)
496 .header("Content-Type", "application/ocsp-request")
497 .header("Accept", "application/ocsp-response")
498 .body(request_der.to_vec())
499 .send()
500 .await
501 .map_err(|e| {
502 if e.is_timeout() {
503 OcspError::Timeout(self.config.timeout_secs)
504 } else {
505 OcspError::Transport(format!("HTTP POST failed: {e}"))
506 }
507 })?;
508
509 let status = response.status();
510 if !status.is_success() {
511 return Err(OcspError::Transport(format!(
512 "OCSP responder returned HTTP {status}"
513 )));
514 }
515
516 let mut response_bytes = Vec::new();
517 while let Some(chunk) = response
518 .chunk()
519 .await
520 .map_err(|e| OcspError::Transport(e.to_string()))?
521 {
522 if chunk.len() > 1_048_576usize.saturating_sub(response_bytes.len()) {
523 return Err(OcspError::Transport("OCSP body exceeds 1 MiB".into()));
524 }
525 response_bytes.extend_from_slice(&chunk);
526 }
527
528 debug!(
529 response_len = response_bytes.len(),
530 "received OCSP response"
531 );
532
533 Ok(response_bytes.to_vec())
534 }
535
536 fn parse_ocsp_response(
546 &self,
547 response_der: &[u8],
548 cert_id: &CertId,
549 issuer_der: &[u8],
550 nonce: Option<&[u8]>,
551 valid_for: &mut Duration,
552 ) -> OcspResult<OcspStatus> {
553 let ocsp_response: synta_certificate::ocsp::OCSPResponse<'_> =
555 Decoder::new(response_der, Encoding::Der)
556 .decode()
557 .map_err(|e| OcspError::Parse(format!("OCSPResponse decode: {e}")))?;
558
559 match &ocsp_response.response_status {
561 synta_certificate::ocsp::OCSPResponseStatus::Successful => {}
562 status => {
563 return Err(OcspError::ResponseStatus(format!("{status:?}")));
564 }
565 }
566
567 let response_bytes = ocsp_response
569 .response_bytes
570 .as_ref()
571 .ok_or(OcspError::Parse(
572 "successful OCSPResponse has no responseBytes".into(),
573 ))?;
574
575 if response_bytes.response_type.components() != synta_certificate::ocsp::ID_PKIX_OCSP_BASIC
577 {
578 return Err(OcspError::Parse(format!(
579 "unexpected responseType: {:?}",
580 response_bytes.response_type.components()
581 )));
582 }
583
584 let basic_response: synta_certificate::ocsp::BasicOCSPResponse<'_> =
586 Decoder::new(response_bytes.response.as_bytes(), Encoding::Der)
587 .decode()
588 .map_err(|e| OcspError::Parse(format!("BasicOCSPResponse decode: {e}")))?;
589
590 let invalid = |e: String| OcspError::SignatureVerification(e);
592 let mut candidates = vec![issuer_der];
593 if let Some(certs) = &basic_response.certs {
594 candidates.extend(certs.iter().map(|c| c.as_bytes()));
595 }
596 let tbs = extract_basic_ocsp_tbs_bytes(response_bytes.response.as_bytes())
597 .ok_or_else(|| invalid("invalid response TBS".into()))?;
598 let alg = basic_response
599 .signature_algorithm
600 .to_der()
601 .map_err(|e| invalid(e.to_string()))?;
602 let verifier = synta_certificate::default_signature_verifier();
603 let trusted = candidates.into_iter().any(|der| {
604 let check = || -> Result<(), String> {
605 crate::auth::certificate::valid_now(der)?;
606 let cert =
607 synta_certificate::Certificate::from_der(der).map_err(|e| e.to_string())?;
608 if der != issuer_der {
609 if !crate::auth::certificate::issued_by(der, issuer_der) {
610 return Err("untrusted OCSP signer".into());
611 }
612 let extensions = cert
613 .tbs_certificate
614 .extensions
615 .as_ref()
616 .ok_or("missing OCSP signing EKU")?;
617 let eku = synta_certificate::find_extension_value(
618 extensions.as_bytes(),
619 &[2, 5, 29, 37],
620 )
621 .ok_or("missing OCSP signing EKU")?;
622 let oids: Vec<synta::ObjectIdentifier> = Decoder::new(eku, Encoding::Der)
623 .decode()
624 .map_err(|e| e.to_string())?;
625 if !oids
626 .iter()
627 .any(|o| o.components() == [1, 3, 6, 1, 5, 5, 7, 3, 9])
628 {
629 return Err("OCSP signing EKU required".into());
630 }
631 }
632 let matches = match &basic_response.tbs_response_data.responder_id {
633 synta_certificate::ocsp::ResponderID::ByName(name) => {
634 name.to_der().map_err(|e| e.to_string())?
635 == cert.tbs_certificate.subject.as_bytes()
636 }
637 synta_certificate::ocsp::ResponderID::ByKey(hash) => {
638 use synta_certificate::DataHasher;
639 let hash_bytes = synta_certificate::default_data_hasher()
640 .hash_data(
641 "sha1",
642 cert.tbs_certificate
643 .subject_public_key_info
644 .subject_public_key
645 .as_bytes(),
646 )
647 .map_err(|e| e.to_string())?;
648 hash.as_bytes() == hash_bytes
649 }
650 };
651 if !matches {
652 return Err("responder ID mismatch".into());
653 }
654 let ranges = synta_certificate::cert_byte_ranges(der)
655 .ok_or("invalid responder certificate")?;
656 verifier
657 .verify_certificate_signature(
658 tbs,
659 &alg,
660 basic_response.signature.as_bytes(),
661 &der[ranges.subject_public_key_info],
662 )
663 .map_err(|e| e.to_string())
664 };
665 check().is_ok()
666 });
667 if !trusted {
668 return Err(invalid("no authorized OCSP signature".into()));
669 }
670 let now = chrono::Utc::now();
671 let convert = |t: &synta::GeneralizedTime| {
672 crate::auth::certificate::time(&synta_certificate::Time::GeneralTime(t.clone()))
673 .map_err(OcspError::Parse)
674 };
675 let produced = convert(&basic_response.tbs_response_data.produced_at)?;
676 if produced > now + chrono::Duration::seconds(300) {
677 return Err(OcspError::Parse("future OCSP producedAt".into()));
678 }
679 if let Some(expected) = nonce {
680 let extensions = basic_response
681 .tbs_response_data
682 .response_extensions
683 .as_ref()
684 .ok_or(OcspError::NonceMismatch)?;
685 let ext = extensions
686 .iter()
687 .find(|e| e.extn_id.components() == [1, 3, 6, 1, 5, 5, 7, 48, 1, 2])
688 .ok_or(OcspError::NonceMismatch)?;
689 let got: synta::OctetStringRef<'_> =
690 Decoder::new(ext.extn_value.as_bytes(), Encoding::Der)
691 .decode()
692 .map_err(|_| OcspError::NonceMismatch)?;
693 if got.as_bytes() != expected {
694 return Err(OcspError::NonceMismatch);
695 }
696 }
697
698 for single in &basic_response.tbs_response_data.responses {
701 let resp_name_hash = single.cert_id.issuer_name_hash.as_bytes();
702 let resp_key_hash = single.cert_id.issuer_key_hash.as_bytes();
703
704 if resp_name_hash == cert_id.issuer_name_hash.as_slice()
705 && resp_key_hash == cert_id.issuer_key_hash.as_slice()
706 && single.cert_id.serial_number
707 == synta::Integer::from_bytes(&cert_id.serial_number)
708 && single.cert_id.hash_algorithm.algorithm.to_string() == cert_id.hash_algorithm
709 {
710 let updated = convert(&single.this_update)?;
711 let expires = if let Some(next) = &single.next_update {
712 convert(next)?
713 } else {
714 updated + chrono::Duration::seconds(3600)
715 };
716 if updated > now + chrono::Duration::seconds(300)
717 || expires <= now
718 || expires <= updated
719 || produced < updated - chrono::Duration::seconds(300)
720 {
721 return Err(OcspError::Parse(
722 "stale or invalid OCSP update interval".into(),
723 ));
724 }
725 *valid_for = (expires - now)
726 .to_std()
727 .map_err(|e| OcspError::Parse(e.to_string()))?;
728 return match &single.cert_status {
730 synta_certificate::ocsp::CertStatus::Good(_) => {
731 debug!("OCSP status: good");
732 Ok(OcspStatus::Good)
733 }
734 synta_certificate::ocsp::CertStatus::Revoked(info) => {
735 let reason = info
736 .revocation_reason
737 .as_ref()
738 .map(|r| format!("{r:?}"))
739 .unwrap_or_else(|| "unspecified".into());
740 let revocation_time = format!(
741 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
742 info.revocation_time.year,
743 info.revocation_time.month,
744 info.revocation_time.day,
745 info.revocation_time.hour,
746 info.revocation_time.minute,
747 info.revocation_time.second,
748 );
749 warn!(
750 reason = %reason,
751 revocation_time = %revocation_time,
752 "OCSP status: revoked"
753 );
754 Ok(OcspStatus::Revoked {
755 reason,
756 revocation_time,
757 })
758 }
759 synta_certificate::ocsp::CertStatus::Unknown(_) => {
760 debug!("OCSP status: unknown");
761 Ok(OcspStatus::Unknown)
762 }
763 };
764 }
765 }
766
767 Err(OcspError::MissingCertStatus)
768 }
769}
770
771fn extract_basic_ocsp_tbs_bytes(basic_response_der: &[u8]) -> Option<&[u8]> {
791 if basic_response_der.is_empty() || basic_response_der[0] != 0x30 {
793 return None;
794 }
795
796 let (outer_len, outer_len_bytes) = read_der_length_ocsp(basic_response_der, 1)?;
798 let content_start = 1 + outer_len_bytes;
799 let content_end = content_start + outer_len;
800 if content_end > basic_response_der.len() {
801 return None;
802 }
803
804 let tbs_start = content_start;
807 if tbs_start >= basic_response_der.len() {
808 return None;
809 }
810 let tbs_tag = basic_response_der[tbs_start];
811 if tbs_tag != 0x30 {
813 return None;
814 }
815
816 let (tbs_value_len, tbs_len_bytes) = read_der_length_ocsp(basic_response_der, tbs_start + 1)?;
817 let tbs_end = tbs_start + 1 + tbs_len_bytes + tbs_value_len;
818 if tbs_end > basic_response_der.len() {
819 return None;
820 }
821
822 Some(&basic_response_der[tbs_start..tbs_end])
823}
824
825fn read_der_length_ocsp(bytes: &[u8], offset: usize) -> Option<(usize, usize)> {
828 let first = *bytes.get(offset)?;
829 if first < 0x80 {
830 Some((first as usize, 1))
831 } else {
832 let n = (first & 0x7f) as usize;
833 if n == 0 || n > 4 || offset + 1 + n > bytes.len() {
834 return None;
835 }
836 let mut val = 0usize;
837 for i in 0..n {
838 val = (val << 8) | bytes[offset + 1 + i] as usize;
839 }
840 Some((val, 1 + n))
841 }
842}
843
844fn sha256_algorithm_identifier_der() -> Vec<u8> {
852 vec![
853 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00,
854 ]
855}
856
857fn extract_integer_value(der: &[u8]) -> Option<&[u8]> {
861 if der.len() < 2 || der[0] != 0x02 {
862 return None;
863 }
864 let len_byte = der[1];
865 if len_byte < 0x80 {
866 let value_start = 2;
868 let value_len = len_byte as usize;
869 der.get(value_start..value_start + value_len)
870 } else {
871 let num_len_bytes = (len_byte & 0x7f) as usize;
873 if der.len() < 2 + num_len_bytes {
874 return None;
875 }
876 let mut value_len: usize = 0;
877 for &b in &der[2..2 + num_len_bytes] {
878 value_len = (value_len << 8) | b as usize;
879 }
880 let value_start = 2 + num_len_bytes;
881 der.get(value_start..value_start + value_len)
882 }
883}
884
885fn extract_ocsp_url_from_aia(aia_value: &[u8]) -> Option<String> {
890 let mut decoder = Decoder::new(aia_value, Encoding::Der);
898 let seq_tag = synta::Tag::universal_constructed(synta::tag::TAG_SEQUENCE);
899
900 let mut outer = decoder.enter_constructed(seq_tag).ok()?;
901 while !outer.is_empty() {
902 let mut access_desc = outer.enter_constructed(seq_tag).ok()?;
903
904 let method: synta::ObjectIdentifier = access_desc.decode().ok()?;
905 if method.components() == synta_certificate::oids::AD_OCSP {
906 let tag = access_desc.peek_tag().ok()?;
909 if tag.number() == 6 {
910 let raw: synta::RawDer<'_> = access_desc.decode().ok()?;
911 let raw_bytes = raw.as_bytes();
913 let url_bytes = extract_context_tagged_value(raw_bytes)?;
914 return String::from_utf8(url_bytes.to_vec()).ok();
915 }
916 }
917 }
919 None
920}
921
922fn extract_context_tagged_value(tlv: &[u8]) -> Option<&[u8]> {
924 if tlv.len() < 2 {
925 return None;
926 }
927 let len_byte = tlv[1];
928 if len_byte < 0x80 {
929 Some(&tlv[2..2 + len_byte as usize])
930 } else {
931 let num = (len_byte & 0x7f) as usize;
932 if tlv.len() < 2 + num {
933 return None;
934 }
935 let mut vlen: usize = 0;
936 for &b in &tlv[2..2 + num] {
937 vlen = (vlen << 8) | b as usize;
938 }
939 let start = 2 + num;
940 tlv.get(start..start + vlen)
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn test_ocsp_config_defaults() {
950 let config = OcspConfig::default();
951 assert!(!config.enabled);
952 assert!(config.responder_url.is_none());
953 assert_eq!(config.cache_ttl_secs, 300);
954 assert_eq!(config.timeout_secs, 10);
955 assert!(config.require_nonce);
956 assert!(!config.soft_fail);
957 }
958
959 #[test]
960 fn test_ocsp_status_variants() {
961 let good = OcspStatus::Good;
962 assert_eq!(good, OcspStatus::Good);
963
964 let revoked = OcspStatus::Revoked {
965 reason: "keyCompromise".to_string(),
966 revocation_time: "2026-01-15T10:00:00Z".to_string(),
967 };
968 assert!(matches!(revoked, OcspStatus::Revoked { .. }));
969
970 let unknown = OcspStatus::Unknown;
971 assert_eq!(unknown, OcspStatus::Unknown);
972 }
973
974 #[test]
975 fn test_cert_id_equality() {
976 let id1 = CertId {
977 hash_algorithm: "2.16.840.1.101.3.4.2.1".to_string(),
978 issuer_name_hash: vec![0x01, 0x02],
979 issuer_key_hash: vec![0x03, 0x04],
980 serial_number: vec![0x05],
981 };
982 let id2 = id1.clone();
983 assert_eq!(id1, id2);
984 }
985
986 #[tokio::test]
987 async fn test_ocsp_disabled_returns_good() {
988 let config = OcspConfig::default(); let client = OcspClient::new(config);
990 let status = client
991 .check_certificate_status(&[0x30, 0x00], &[0x30, 0x00])
992 .await
993 .unwrap();
994 assert_eq!(status, OcspStatus::Good);
995 }
996
997 #[test]
998 fn test_build_cert_id_empty_cert() {
999 let config = OcspConfig::default();
1000 let client = OcspClient::new(config);
1001 let result = client.build_cert_id(&[], &[0x30, 0x00]);
1002 assert!(matches!(result, Err(OcspError::RequestBuild(_))));
1003 }
1004
1005 #[test]
1006 fn test_build_cert_id_empty_issuer() {
1007 let config = OcspConfig::default();
1008 let client = OcspClient::new(config);
1009 let result = client.build_cert_id(&[0x30, 0x00], &[]);
1010 assert!(matches!(result, Err(OcspError::RequestBuild(_))));
1011 }
1012
1013 #[test]
1014 fn test_resolve_responder_url_from_config() {
1015 let config = OcspConfig {
1016 responder_url: Some("http://ocsp.example.com".to_string()),
1017 ..Default::default()
1018 };
1019 let client = OcspClient::new(config);
1020 let url = client.resolve_responder_url(&[0x30, 0x00]).unwrap();
1021 assert_eq!(url, "http://ocsp.example.com");
1022 }
1023
1024 #[test]
1025 fn test_resolve_responder_url_no_config() {
1026 let config = OcspConfig::default();
1027 let client = OcspClient::new(config);
1028 let result = client.resolve_responder_url(&[0x30, 0x00]);
1029 assert!(matches!(result, Err(OcspError::RequestBuild(_))));
1030 }
1031
1032 #[test]
1033 fn test_cache_operations() {
1034 let config = OcspConfig::default();
1035 let client = OcspClient::new(config);
1036 assert_eq!(client.cache_size(), 0);
1037
1038 let cert_id = CertId {
1040 hash_algorithm: "2.16.840.1.101.3.4.2.1".to_string(),
1041 issuer_name_hash: vec![0x01],
1042 issuer_key_hash: vec![0x02],
1043 serial_number: vec![0x03],
1044 };
1045 client.cache.insert(
1046 cert_id,
1047 CachedOcspResponse {
1048 status: OcspStatus::Good,
1049 cached_at: Instant::now(),
1050 ttl: Duration::from_secs(300),
1051 },
1052 );
1053 assert_eq!(client.cache_size(), 1);
1054
1055 client.evict_expired();
1056 assert_eq!(client.cache_size(), 1); }
1058}
1059
1060#[cfg(test)]
1061#[path = "regression_tests.rs"]
1062mod regression_tests;