Skip to main content

amaters_net/
ocsp.rs

1//! OCSP (Online Certificate Status Protocol) revocation checking
2//!
3//! Implements RFC 6960 OCSP request building, HTTP transport, and response parsing.
4//! Uses pure Rust DER encoding/decoding with `x509-parser`.
5//!
6//! # Architecture
7//!
8//! - `OcspRevocationChecker`: Main entry point with caching
9//! - `build_ocsp_request()`: Constructs DER-encoded OCSP request
10//! - `send_ocsp_request()`: HTTP/1.1 POST to OCSP responder
11//! - `parse_ocsp_response()`: Parses DER-encoded OCSP response
12//!
13//! # Fail-open Design
14//!
15//! On any error (network, parsing, timeout), the checker returns
16//! `RevocationStatus::Unknown` rather than blocking the connection.
17
18use std::collections::HashMap;
19use std::fmt::Write;
20use std::sync::Arc;
21use std::time::{Duration, SystemTime};
22
23use parking_lot::RwLock;
24use tracing::warn;
25use x509_parser::prelude::*;
26
27use crate::error::{NetError, NetResult};
28use crate::mtls::RevocationStatus;
29
30// ── ASN.1 DER tag constants ──────────────────────────────────────────
31
32/// ASN.1 SEQUENCE tag
33const TAG_SEQUENCE: u8 = 0x30;
34/// ASN.1 OCTET STRING tag
35const TAG_OCTET_STRING: u8 = 0x04;
36/// ASN.1 INTEGER tag
37const TAG_INTEGER: u8 = 0x02;
38/// ASN.1 OID tag
39const TAG_OID: u8 = 0x06;
40/// ASN.1 ENUMERATED tag
41const TAG_ENUMERATED: u8 = 0x0A;
42/// ASN.1 context-specific constructed [0]
43const TAG_CONTEXT_0: u8 = 0xA0;
44/// ASN.1 context-specific constructed [1]
45const TAG_CONTEXT_1: u8 = 0xA1;
46/// ASN.1 context-specific primitive [1] (for revoked implicit tag)
47const TAG_CONTEXT_PRIM_1: u8 = 0x81;
48
49/// SHA-256 OID: 2.16.840.1.101.3.4.2.1
50const SHA256_OID_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
51
52/// id-pkix-ocsp-basic OID: 1.3.6.1.5.5.7.48.1.1
53const OCSP_BASIC_OID_BYTES: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01];
54
55/// id-ad-ocsp OID bytes: 1.3.6.1.5.5.7.48.1
56const AIA_OCSP_OID_BYTES: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01];
57
58/// AIA extension OID bytes: 1.3.6.1.5.5.7.1.1
59const AIA_EXT_OID_BYTES: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01];
60
61/// Default timeout for OCSP requests
62const DEFAULT_OCSP_TIMEOUT: Duration = Duration::from_secs(5);
63
64/// Default OCSP cache TTL
65const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(3600);
66
67// ── DER encoding helpers ─────────────────────────────────────────────
68
69/// Encode a DER length field (supports short and long forms)
70fn der_encode_length(len: usize) -> Vec<u8> {
71    if len < 0x80 {
72        vec![len as u8]
73    } else if len < 0x100 {
74        vec![0x81, len as u8]
75    } else if len < 0x10000 {
76        vec![0x82, (len >> 8) as u8, len as u8]
77    } else if len < 0x100_0000 {
78        vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
79    } else {
80        vec![
81            0x84,
82            (len >> 24) as u8,
83            (len >> 16) as u8,
84            (len >> 8) as u8,
85            len as u8,
86        ]
87    }
88}
89
90/// Wrap content bytes in a TLV (tag-length-value) structure
91fn der_tlv(tag: u8, content: &[u8]) -> Vec<u8> {
92    let mut out = vec![tag];
93    out.extend(der_encode_length(content.len()));
94    out.extend(content);
95    out
96}
97
98/// Encode a DER OID value (tag + length + oid bytes)
99fn der_oid(oid_bytes: &[u8]) -> Vec<u8> {
100    der_tlv(TAG_OID, oid_bytes)
101}
102
103/// Encode an algorithm identifier SEQUENCE (OID + NULL parameters)
104fn der_algorithm_identifier(oid_bytes: &[u8]) -> Vec<u8> {
105    let mut content = der_oid(oid_bytes);
106    // NULL parameters: 05 00
107    content.extend(&[0x05, 0x00]);
108    der_tlv(TAG_SEQUENCE, &content)
109}
110
111/// Encode an OCTET STRING
112fn der_octet_string(data: &[u8]) -> Vec<u8> {
113    der_tlv(TAG_OCTET_STRING, data)
114}
115
116/// Encode a positive INTEGER (with leading zero if MSB is set)
117fn der_integer_from_bytes(data: &[u8]) -> Vec<u8> {
118    // Strip leading zeros but keep at least one byte
119    let stripped = data
120        .iter()
121        .position(|&b| b != 0)
122        .map_or(&data[data.len().saturating_sub(1)..], |pos| &data[pos..]);
123
124    // If MSB is set, prepend a 0x00 byte to keep it positive
125    if stripped.first().is_some_and(|&b| b & 0x80 != 0) {
126        let mut content = vec![0x00];
127        content.extend(stripped);
128        der_tlv(TAG_INTEGER, &content)
129    } else {
130        der_tlv(TAG_INTEGER, stripped)
131    }
132}
133
134// ── DER parsing helpers ──────────────────────────────────────────────
135
136/// Read a DER length from a byte slice, returning (length_value, bytes_consumed)
137fn der_read_length(data: &[u8]) -> NetResult<(usize, usize)> {
138    if data.is_empty() {
139        return Err(NetError::InvalidCertificate(
140            "OCSP: unexpected end of DER data reading length".to_string(),
141        ));
142    }
143    let first = data[0];
144    if first < 0x80 {
145        Ok((first as usize, 1))
146    } else {
147        let num_bytes = (first & 0x7F) as usize;
148        if num_bytes == 0 || num_bytes > 4 {
149            return Err(NetError::InvalidCertificate(format!(
150                "OCSP: unsupported DER length encoding ({num_bytes} bytes)"
151            )));
152        }
153        if data.len() < 1 + num_bytes {
154            return Err(NetError::InvalidCertificate(
155                "OCSP: truncated DER length".to_string(),
156            ));
157        }
158        let mut val: usize = 0;
159        for i in 0..num_bytes {
160            val = (val << 8) | (data[1 + i] as usize);
161        }
162        Ok((val, 1 + num_bytes))
163    }
164}
165
166/// Read a TLV element, returning (tag, content_slice, total_bytes_consumed)
167fn der_read_tlv(data: &[u8]) -> NetResult<(u8, &[u8], usize)> {
168    if data.is_empty() {
169        return Err(NetError::InvalidCertificate(
170            "OCSP: unexpected end of DER data reading TLV".to_string(),
171        ));
172    }
173    let tag = data[0];
174    let (len, len_bytes) = der_read_length(&data[1..])?;
175    let header_len = 1 + len_bytes;
176    let total = header_len + len;
177    if data.len() < total {
178        return Err(NetError::InvalidCertificate(format!(
179            "OCSP: DER content truncated (need {total}, have {})",
180            data.len()
181        )));
182    }
183    Ok((tag, &data[header_len..total], total))
184}
185
186/// Iterate over children of a SEQUENCE (or other constructed type)
187fn der_children(data: &[u8]) -> NetResult<Vec<(u8, Vec<u8>)>> {
188    let mut children = Vec::new();
189    let mut pos = 0;
190    while pos < data.len() {
191        let (tag, content, consumed) = der_read_tlv(&data[pos..])?;
192        children.push((tag, content.to_vec()));
193        pos += consumed;
194    }
195    Ok(children)
196}
197
198// ── Certificate fingerprint helper ───────────────────────────────────
199
200/// Compute a hex fingerprint from the first 32 bytes of a cert (matches mtls.rs)
201fn cert_fingerprint(cert_der: &[u8]) -> String {
202    cert_der.iter().take(32).fold(String::new(), |mut s, b| {
203        let _ = write!(&mut s, "{b:02x}");
204        s
205    })
206}
207
208// ── OCSP URL extraction ─────────────────────────────────────────────
209
210/// Extract OCSP responder URL from a certificate's Authority Information Access extension
211pub fn extract_ocsp_url(cert_der: &[u8]) -> NetResult<Option<String>> {
212    let (_, parsed) = X509Certificate::from_der(cert_der).map_err(|e| {
213        NetError::InvalidCertificate(format!("OCSP: failed to parse certificate: {e}"))
214    })?;
215
216    // Look for AIA extension (OID 1.3.6.1.5.5.7.1.1)
217    let aia_oid = asn1_rs::Oid::new(std::borrow::Cow::Borrowed(AIA_EXT_OID_BYTES));
218    let aia = parsed.extensions().iter().find(|ext| ext.oid == aia_oid);
219
220    let aia_ext = match aia {
221        Some(ext) => ext,
222        None => return Ok(None),
223    };
224
225    // Parse the AIA extension value as a SEQUENCE of AccessDescription
226    // Each AccessDescription = SEQUENCE { accessMethod OID, accessLocation GeneralName }
227    let children = der_children(aia_ext.value)?;
228
229    for (tag, child_data) in &children {
230        // Each child should be a SEQUENCE (AccessDescription)
231        if *tag != TAG_SEQUENCE {
232            continue;
233        }
234        let inner = der_children(child_data)?;
235        if inner.len() < 2 {
236            continue;
237        }
238
239        // First element is the OID (accessMethod)
240        let (oid_tag, oid_data) = &inner[0];
241        if *oid_tag != TAG_OID {
242            continue;
243        }
244
245        // Check if this is id-ad-ocsp (1.3.6.1.5.5.7.48.1)
246        if oid_data.as_slice() != AIA_OCSP_OID_BYTES {
247            continue;
248        }
249
250        // Second element is the GeneralName — context [6] uniformResourceIdentifier
251        let (name_tag, name_data) = &inner[1];
252        // Context-specific primitive [6] = 0x86
253        if *name_tag == 0x86 {
254            let url = String::from_utf8(name_data.clone()).map_err(|e| {
255                NetError::InvalidCertificate(format!("OCSP: invalid URL encoding: {e}"))
256            })?;
257            return Ok(Some(url));
258        }
259    }
260
261    Ok(None)
262}
263
264// ── OCSP request building ────────────────────────────────────────────
265
266/// Build a DER-encoded OCSP request for the given certificate.
267///
268/// The request follows RFC 6960 structure:
269/// ```text
270/// OCSPRequest ::= SEQUENCE {
271///     tbsRequest TBSRequest
272/// }
273/// TBSRequest ::= SEQUENCE {
274///     version [0] EXPLICIT Version DEFAULT v1,  (omitted for v1)
275///     requestList SEQUENCE OF Request
276/// }
277/// Request ::= SEQUENCE {
278///     reqCert CertID
279/// }
280/// CertID ::= SEQUENCE {
281///     hashAlgorithm AlgorithmIdentifier,
282///     issuerNameHash OCTET STRING,
283///     issuerKeyHash OCTET STRING,
284///     serialNumber CertificateSerialNumber
285/// }
286/// ```
287pub fn build_ocsp_request(cert_der: &[u8]) -> NetResult<Vec<u8>> {
288    let (_, parsed) = X509Certificate::from_der(cert_der).map_err(|e| {
289        NetError::InvalidCertificate(format!("OCSP: failed to parse certificate: {e}"))
290    })?;
291
292    // Hash the issuer distinguished name with SHA-256
293    let issuer_name_der = parsed.issuer().as_raw();
294    let issuer_name_hash = blake3::hash(issuer_name_der);
295
296    // For a self-signed cert, the issuer key is in the cert itself.
297    // For issued certs, ideally we'd have the issuer cert. We use the
298    // subject public key info from the cert's own SPKI as a fallback
299    // (which is correct for self-signed, and a best-effort for others).
300    let spki_der = parsed.public_key().raw;
301    let issuer_key_hash = blake3::hash(spki_der);
302
303    // Serial number bytes
304    let serial_bytes = parsed.serial.to_bytes_be();
305
306    // Build CertID
307    let algo_id = der_algorithm_identifier(SHA256_OID_BYTES);
308    let name_hash = der_octet_string(issuer_name_hash.as_bytes());
309    let key_hash = der_octet_string(issuer_key_hash.as_bytes());
310    let serial_int = der_integer_from_bytes(&serial_bytes);
311
312    let mut cert_id_content = Vec::new();
313    cert_id_content.extend(&algo_id);
314    cert_id_content.extend(&name_hash);
315    cert_id_content.extend(&key_hash);
316    cert_id_content.extend(&serial_int);
317    let cert_id = der_tlv(TAG_SEQUENCE, &cert_id_content);
318
319    // Build Request
320    let request = der_tlv(TAG_SEQUENCE, &cert_id);
321
322    // Build requestList (SEQUENCE OF Request)
323    let request_list = der_tlv(TAG_SEQUENCE, &request);
324
325    // Build TBSRequest (version omitted for v1)
326    let tbs_request = der_tlv(TAG_SEQUENCE, &request_list);
327
328    // Build OCSPRequest
329    let ocsp_request = der_tlv(TAG_SEQUENCE, &tbs_request);
330
331    Ok(ocsp_request)
332}
333
334// ── HTTP transport ───────────────────────────────────────────────────
335
336/// Parse a URL into (host, port, path) components
337fn parse_url(url: &str) -> NetResult<(String, u16, String)> {
338    // Strip scheme
339    let without_scheme = if let Some(rest) = url.strip_prefix("http://") {
340        rest
341    } else if let Some(rest) = url.strip_prefix("https://") {
342        // OCSP typically uses HTTP, but handle https prefix gracefully
343        rest
344    } else {
345        url
346    };
347
348    // Split host+port from path
349    let (host_port, path) = match without_scheme.find('/') {
350        Some(idx) => (&without_scheme[..idx], &without_scheme[idx..]),
351        None => (without_scheme, "/"),
352    };
353
354    // Split host from port
355    let (host, port) = match host_port.rfind(':') {
356        Some(idx) => {
357            let port_str = &host_port[idx + 1..];
358            let port: u16 = port_str.parse().map_err(|e| {
359                NetError::InvalidCertificate(format!("OCSP: invalid port in URL: {e}"))
360            })?;
361            (host_port[..idx].to_string(), port)
362        }
363        None => (host_port.to_string(), 80),
364    };
365
366    Ok((host, port, path.to_string()))
367}
368
369/// Send an OCSP request via HTTP/1.1 POST and return the response body.
370///
371/// Uses raw TCP for pure-Rust operation (no reqwest dependency).
372pub async fn send_ocsp_request(
373    url: &str,
374    request_der: &[u8],
375    timeout: Duration,
376) -> NetResult<Vec<u8>> {
377    use tokio::io::{AsyncReadExt, AsyncWriteExt};
378    use tokio::net::TcpStream;
379
380    let (host, port, path) = parse_url(url)?;
381
382    // Build HTTP/1.1 POST request
383    let http_request = format!(
384        "POST {path} HTTP/1.1\r\n\
385         Host: {host}\r\n\
386         Content-Type: application/ocsp-request\r\n\
387         Content-Length: {}\r\n\
388         Connection: close\r\n\
389         \r\n",
390        request_der.len()
391    );
392
393    // Connect with timeout
394    let addr = format!("{host}:{port}");
395    let stream = tokio::time::timeout(timeout, TcpStream::connect(&addr))
396        .await
397        .map_err(|_| NetError::Timeout(format!("OCSP: connection to {addr} timed out")))?
398        .map_err(|e| {
399            NetError::ConnectionRefused(format!("OCSP: failed to connect to {addr}: {e}"))
400        })?;
401
402    let mut stream = stream;
403
404    // Send request with timeout
405    tokio::time::timeout(timeout, async {
406        stream
407            .write_all(http_request.as_bytes())
408            .await
409            .map_err(|e| NetError::ConnectionReset(format!("OCSP: failed to send request: {e}")))?;
410        stream.write_all(request_der).await.map_err(|e| {
411            NetError::ConnectionReset(format!("OCSP: failed to send request body: {e}"))
412        })?;
413        stream
414            .flush()
415            .await
416            .map_err(|e| NetError::ConnectionReset(format!("OCSP: failed to flush: {e}")))?;
417        Ok::<(), NetError>(())
418    })
419    .await
420    .map_err(|_| NetError::Timeout("OCSP: send timed out".to_string()))??;
421
422    // Read response with timeout
423    let response_bytes = tokio::time::timeout(timeout, async {
424        let mut buf = Vec::with_capacity(8192);
425        stream.read_to_end(&mut buf).await.map_err(|e| {
426            NetError::ConnectionReset(format!("OCSP: failed to read response: {e}"))
427        })?;
428        Ok::<Vec<u8>, NetError>(buf)
429    })
430    .await
431    .map_err(|_| NetError::Timeout("OCSP: read timed out".to_string()))??;
432
433    // Parse HTTP response: find end of headers (\r\n\r\n)
434    let header_end = response_bytes
435        .windows(4)
436        .position(|w| w == b"\r\n\r\n")
437        .ok_or_else(|| {
438            NetError::InvalidCertificate(
439                "OCSP: malformed HTTP response (no header end)".to_string(),
440            )
441        })?;
442
443    let header_str = String::from_utf8_lossy(&response_bytes[..header_end]);
444
445    // Check HTTP status
446    let status_line = header_str
447        .lines()
448        .next()
449        .ok_or_else(|| NetError::InvalidCertificate("OCSP: empty HTTP response".to_string()))?;
450
451    // Expect "HTTP/1.x 200 ..."
452    let parts: Vec<&str> = status_line.splitn(3, ' ').collect();
453    if parts.len() < 2 {
454        return Err(NetError::InvalidCertificate(format!(
455            "OCSP: malformed HTTP status line: {status_line}"
456        )));
457    }
458    let status_code: u16 = parts[1].parse().map_err(|e| {
459        NetError::InvalidCertificate(format!("OCSP: invalid HTTP status code: {e}"))
460    })?;
461
462    if status_code != 200 {
463        return Err(NetError::InvalidCertificate(format!(
464            "OCSP: HTTP error {status_code}"
465        )));
466    }
467
468    let body_start = header_end + 4;
469    if body_start >= response_bytes.len() {
470        return Err(NetError::InvalidCertificate(
471            "OCSP: empty HTTP response body".to_string(),
472        ));
473    }
474
475    Ok(response_bytes[body_start..].to_vec())
476}
477
478// ── OCSP response parsing ────────────────────────────────────────────
479
480/// Parse a DER-encoded OCSP response and extract the revocation status.
481///
482/// OCSP response structure (RFC 6960):
483/// ```text
484/// OCSPResponse ::= SEQUENCE {
485///     responseStatus ENUMERATED { successful(0), ... },
486///     responseBytes [0] EXPLICIT SEQUENCE {
487///         responseType OID,
488///         response OCTET STRING  -- contains BasicOCSPResponse DER
489///     } OPTIONAL
490/// }
491///
492/// BasicOCSPResponse ::= SEQUENCE {
493///     tbsResponseData ResponseData,
494///     signatureAlgorithm AlgorithmIdentifier,
495///     signature BIT STRING,
496///     certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL
497/// }
498///
499/// ResponseData ::= SEQUENCE {
500///     version [0] EXPLICIT Version DEFAULT v1,
501///     responderID ResponderID,
502///     producedAt GeneralizedTime,
503///     responses SEQUENCE OF SingleResponse,
504///     ...
505/// }
506///
507/// SingleResponse ::= SEQUENCE {
508///     certID CertID,
509///     certStatus CertStatus,
510///     ...
511/// }
512///
513/// CertStatus ::= CHOICE {
514///     good    [0] IMPLICIT NULL,
515///     revoked [1] IMPLICIT RevokedInfo,
516///     unknown [2] IMPLICIT UnknownInfo
517/// }
518/// ```
519pub fn parse_ocsp_response(response_der: &[u8]) -> NetResult<RevocationStatus> {
520    // Outer SEQUENCE: OCSPResponse
521    let (tag, ocsp_resp_content, _) = der_read_tlv(response_der)?;
522    if tag != TAG_SEQUENCE {
523        return Err(NetError::InvalidCertificate(format!(
524            "OCSP: expected SEQUENCE, got 0x{tag:02x}"
525        )));
526    }
527
528    let children = der_children(ocsp_resp_content)?;
529    if children.is_empty() {
530        return Err(NetError::InvalidCertificate(
531            "OCSP: empty OCSPResponse".to_string(),
532        ));
533    }
534
535    // First child: responseStatus ENUMERATED
536    let (status_tag, status_data) = &children[0];
537    if *status_tag != TAG_ENUMERATED {
538        return Err(NetError::InvalidCertificate(format!(
539            "OCSP: expected ENUMERATED for responseStatus, got 0x{status_tag:02x}"
540        )));
541    }
542    let response_status = status_data
543        .first()
544        .copied()
545        .ok_or_else(|| NetError::InvalidCertificate("OCSP: empty responseStatus".to_string()))?;
546
547    // responseStatus: 0=successful, 1=malformedRequest, 2=internalError,
548    //                 3=tryLater, 5=sigRequired, 6=unauthorized
549    if response_status != 0 {
550        return Err(NetError::InvalidCertificate(format!(
551            "OCSP: non-successful responseStatus: {response_status}"
552        )));
553    }
554
555    // Second child: responseBytes [0] EXPLICIT
556    if children.len() < 2 {
557        return Err(NetError::InvalidCertificate(
558            "OCSP: missing responseBytes".to_string(),
559        ));
560    }
561
562    let (rb_tag, rb_data) = &children[1];
563    if *rb_tag != TAG_CONTEXT_0 {
564        return Err(NetError::InvalidCertificate(format!(
565            "OCSP: expected [0] for responseBytes, got 0x{rb_tag:02x}"
566        )));
567    }
568
569    // responseBytes is a SEQUENCE { responseType OID, response OCTET STRING }
570    let (inner_tag, inner_content, _) = der_read_tlv(rb_data)?;
571    if inner_tag != TAG_SEQUENCE {
572        return Err(NetError::InvalidCertificate(
573            "OCSP: responseBytes inner not SEQUENCE".to_string(),
574        ));
575    }
576
577    let rb_children = der_children(inner_content)?;
578    if rb_children.len() < 2 {
579        return Err(NetError::InvalidCertificate(
580            "OCSP: responseBytes SEQUENCE too short".to_string(),
581        ));
582    }
583
584    // Verify responseType is id-pkix-ocsp-basic
585    let (oid_tag, oid_data) = &rb_children[0];
586    if *oid_tag != TAG_OID {
587        return Err(NetError::InvalidCertificate(
588            "OCSP: responseType not OID".to_string(),
589        ));
590    }
591    if oid_data.as_slice() != OCSP_BASIC_OID_BYTES {
592        return Err(NetError::InvalidCertificate(
593            "OCSP: responseType is not id-pkix-ocsp-basic".to_string(),
594        ));
595    }
596
597    // response OCTET STRING contains BasicOCSPResponse
598    let (oct_tag, oct_data) = &rb_children[1];
599    if *oct_tag != TAG_OCTET_STRING {
600        return Err(NetError::InvalidCertificate(
601            "OCSP: response not OCTET STRING".to_string(),
602        ));
603    }
604
605    // Parse BasicOCSPResponse
606    parse_basic_ocsp_response(oct_data)
607}
608
609/// Parse a BasicOCSPResponse and extract status from the first SingleResponse
610fn parse_basic_ocsp_response(data: &[u8]) -> NetResult<RevocationStatus> {
611    // BasicOCSPResponse ::= SEQUENCE { tbsResponseData, sigAlgo, sig, [0] certs }
612    let (tag, content, _) = der_read_tlv(data)?;
613    if tag != TAG_SEQUENCE {
614        return Err(NetError::InvalidCertificate(
615            "OCSP: BasicOCSPResponse not SEQUENCE".to_string(),
616        ));
617    }
618
619    let children = der_children(content)?;
620    if children.is_empty() {
621        return Err(NetError::InvalidCertificate(
622            "OCSP: empty BasicOCSPResponse".to_string(),
623        ));
624    }
625
626    // First child is tbsResponseData (SEQUENCE)
627    let (tbs_tag, tbs_data) = &children[0];
628    if *tbs_tag != TAG_SEQUENCE {
629        return Err(NetError::InvalidCertificate(
630            "OCSP: tbsResponseData not SEQUENCE".to_string(),
631        ));
632    }
633
634    parse_tbs_response_data(tbs_data)
635}
636
637/// Parse ResponseData and extract certStatus from the first SingleResponse
638fn parse_tbs_response_data(data: &[u8]) -> NetResult<RevocationStatus> {
639    let children = der_children(data)?;
640
641    // ResponseData fields:
642    //   [0] version (optional), responderID, producedAt, responses SEQUENCE, [1] extensions
643    // We need to find the SEQUENCE OF SingleResponse (the "responses" field).
644    // The responderID can be [1] or [2] (byName or byKey), producedAt is GeneralizedTime (0x18).
645    // We iterate and find the first SEQUENCE that contains SingleResponse elements.
646
647    // Find the responses field: it's a SEQUENCE OF SingleResponse.
648    // Walk the children looking for SEQUENCE tags after the responderID and producedAt.
649    let mut response_seq: Option<&Vec<u8>> = None;
650    let mut found_time = false;
651
652    for (tag, child_data) in &children {
653        // Skip version [0]
654        if *tag == TAG_CONTEXT_0 {
655            continue;
656        }
657        // Skip responderID [1] byName or [2] byKey
658        if *tag == TAG_CONTEXT_1 || *tag == 0xA2 {
659            continue;
660        }
661        // GeneralizedTime (0x18) = producedAt
662        if *tag == 0x18 {
663            found_time = true;
664            continue;
665        }
666        // First SEQUENCE after producedAt is the responses field
667        if *tag == TAG_SEQUENCE && found_time {
668            response_seq = Some(child_data);
669            break;
670        }
671        // If we haven't seen a time yet and this is a SEQUENCE, it might be responses
672        // in a simplified encoding (some responders omit optional fields differently)
673        if *tag == TAG_SEQUENCE && !found_time {
674            // Check if this looks like it contains SingleResponse children
675            if let Ok(inner) = der_children(child_data) {
676                if !inner.is_empty() && inner[0].0 == TAG_SEQUENCE {
677                    response_seq = Some(child_data);
678                    break;
679                }
680            }
681        }
682    }
683
684    let responses_data = response_seq.ok_or_else(|| {
685        NetError::InvalidCertificate(
686            "OCSP: could not find responses SEQUENCE in ResponseData".to_string(),
687        )
688    })?;
689
690    // Parse SEQUENCE OF SingleResponse — take the first one
691    let single_responses = der_children(responses_data)?;
692    if single_responses.is_empty() {
693        return Err(NetError::InvalidCertificate(
694            "OCSP: no SingleResponse found".to_string(),
695        ));
696    }
697
698    let (sr_tag, sr_data) = &single_responses[0];
699    if *sr_tag != TAG_SEQUENCE {
700        return Err(NetError::InvalidCertificate(
701            "OCSP: SingleResponse not SEQUENCE".to_string(),
702        ));
703    }
704
705    parse_single_response(sr_data)
706}
707
708/// Parse a SingleResponse and extract certStatus
709fn parse_single_response(data: &[u8]) -> NetResult<RevocationStatus> {
710    let children = der_children(data)?;
711
712    // SingleResponse ::= SEQUENCE {
713    //   certID CertID (SEQUENCE),
714    //   certStatus CertStatus,
715    //   thisUpdate GeneralizedTime,
716    //   ...
717    // }
718    if children.len() < 2 {
719        return Err(NetError::InvalidCertificate(
720            "OCSP: SingleResponse too short".to_string(),
721        ));
722    }
723
724    // certStatus is the second element (index 1)
725    let (status_tag, _status_data) = &children[1];
726
727    // CertStatus ::= CHOICE {
728    //   good    [0] IMPLICIT NULL     -> tag 0x80 (context primitive 0)
729    //   revoked [1] IMPLICIT ...      -> tag 0xA1 (context constructed 1)
730    //   unknown [2] IMPLICIT NULL     -> tag 0x82 (context primitive 2)
731    // }
732    match *status_tag {
733        0x80 => Ok(RevocationStatus::Good), // good [0]
734        0xA1 | TAG_CONTEXT_PRIM_1 => Ok(RevocationStatus::Revoked), // revoked [1]
735        0x82 => Ok(RevocationStatus::Unknown), // unknown [2]
736        other => {
737            warn!("OCSP: unexpected certStatus tag 0x{other:02x}");
738            Ok(RevocationStatus::Unknown)
739        }
740    }
741}
742
743// ── OcspRevocationChecker ────────────────────────────────────────────
744
745/// OCSP-based certificate revocation checker with caching
746///
747/// Implements the `RevocationChecker` trait with real OCSP protocol support:
748/// - Builds DER-encoded OCSP requests per RFC 6960
749/// - Sends requests via HTTP/1.1 POST to OCSP responders
750/// - Parses DER-encoded OCSP responses
751/// - Caches results with configurable TTL
752/// - Fail-open: returns `Unknown` on any error
753#[derive(Debug)]
754pub struct OcspRevocationChecker {
755    /// OCSP responder URL (overrides URL from certificate AIA extension)
756    responder_url: Option<String>,
757    /// Cache of OCSP responses (fingerprint -> (status, timestamp))
758    response_cache: Arc<RwLock<HashMap<String, (RevocationStatus, SystemTime)>>>,
759    /// Cache TTL
760    cache_ttl: Duration,
761    /// Request timeout
762    timeout: Duration,
763}
764
765impl Default for OcspRevocationChecker {
766    fn default() -> Self {
767        Self::new()
768    }
769}
770
771impl OcspRevocationChecker {
772    /// Create a new OCSP revocation checker with default settings
773    pub fn new() -> Self {
774        Self {
775            responder_url: None,
776            response_cache: Arc::new(RwLock::new(HashMap::new())),
777            cache_ttl: DEFAULT_CACHE_TTL,
778            timeout: DEFAULT_OCSP_TIMEOUT,
779        }
780    }
781
782    /// Set the OCSP responder URL (overrides certificate AIA extension)
783    pub fn with_responder_url(mut self, url: impl Into<String>) -> Self {
784        self.responder_url = Some(url.into());
785        self
786    }
787
788    /// Set cache TTL
789    pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
790        self.cache_ttl = ttl;
791        self
792    }
793
794    /// Set request timeout
795    pub fn with_timeout(mut self, timeout: Duration) -> Self {
796        self.timeout = timeout;
797        self
798    }
799
800    /// Get cached revocation status for a certificate fingerprint
801    pub fn get_cached(&self, fingerprint: &str) -> Option<RevocationStatus> {
802        let cache = self.response_cache.read();
803        if let Some((status, timestamp)) = cache.get(fingerprint) {
804            if timestamp.elapsed().unwrap_or(Duration::MAX) < self.cache_ttl {
805                return Some(*status);
806            }
807        }
808        None
809    }
810
811    /// Cache a revocation status for a certificate fingerprint
812    pub fn cache_status(&self, fingerprint: String, status: RevocationStatus) {
813        let mut cache = self.response_cache.write();
814        cache.insert(fingerprint, (status, SystemTime::now()));
815    }
816
817    /// Clear the entire cache
818    pub fn clear_cache(&self) {
819        self.response_cache.write().clear();
820    }
821
822    /// Get the number of cached entries
823    pub fn cache_size(&self) -> usize {
824        self.response_cache.read().len()
825    }
826
827    /// Determine the OCSP responder URL to use for a certificate
828    fn resolve_responder_url(&self, cert_der: &[u8]) -> NetResult<Option<String>> {
829        // Custom URL takes priority
830        if let Some(ref url) = self.responder_url {
831            return Ok(Some(url.clone()));
832        }
833        // Try to extract from certificate AIA extension
834        extract_ocsp_url(cert_der)
835    }
836
837    /// Synchronous check: cache-only, no network I/O
838    pub fn check_revocation(
839        &self,
840        cert: &rustls::pki_types::CertificateDer<'_>,
841    ) -> NetResult<RevocationStatus> {
842        let fingerprint = cert_fingerprint(cert.as_ref());
843
844        // Cache-only in sync mode
845        if let Some(status) = self.get_cached(&fingerprint) {
846            return Ok(status);
847        }
848
849        // No network in sync mode
850        Ok(RevocationStatus::Unknown)
851    }
852
853    /// Asynchronous check: cache first, then network OCSP query
854    pub fn check_revocation_async<'a>(
855        &'a self,
856        cert: &'a rustls::pki_types::CertificateDer<'_>,
857    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NetResult<RevocationStatus>> + Send + 'a>>
858    {
859        let cert_bytes = cert.as_ref().to_vec();
860        let fingerprint = cert_fingerprint(&cert_bytes);
861
862        // Check cache first
863        if let Some(status) = self.get_cached(&fingerprint) {
864            return Box::pin(async move { Ok(status) });
865        }
866
867        let timeout = self.timeout;
868
869        Box::pin(async move {
870            // Resolve responder URL
871            let url = match self.resolve_responder_url(&cert_bytes) {
872                Ok(Some(url)) => url,
873                Ok(None) => {
874                    warn!("OCSP: no responder URL available for certificate");
875                    return Ok(RevocationStatus::Unknown);
876                }
877                Err(e) => {
878                    warn!("OCSP: failed to resolve responder URL: {e}");
879                    return Ok(RevocationStatus::Unknown);
880                }
881            };
882
883            // Build OCSP request
884            let request_der = match build_ocsp_request(&cert_bytes) {
885                Ok(req) => req,
886                Err(e) => {
887                    warn!("OCSP: failed to build request: {e}");
888                    return Ok(RevocationStatus::Unknown);
889                }
890            };
891
892            // Send OCSP request
893            let response_der = match send_ocsp_request(&url, &request_der, timeout).await {
894                Ok(resp) => resp,
895                Err(e) => {
896                    warn!("OCSP: network error: {e}");
897                    return Ok(RevocationStatus::Unknown);
898                }
899            };
900
901            // Parse OCSP response
902            let status = match parse_ocsp_response(&response_der) {
903                Ok(s) => s,
904                Err(e) => {
905                    warn!("OCSP: failed to parse response: {e}");
906                    RevocationStatus::Unknown
907                }
908            };
909
910            // Cache the result
911            self.cache_status(fingerprint, status);
912
913            Ok(status)
914        })
915    }
916}
917
918// ── RevocationChecker trait impl (delegates to the methods above) ────
919
920impl crate::mtls::RevocationChecker for OcspRevocationChecker {
921    fn check_revocation(
922        &self,
923        cert: &rustls::pki_types::CertificateDer<'_>,
924    ) -> NetResult<RevocationStatus> {
925        OcspRevocationChecker::check_revocation(self, cert)
926    }
927
928    fn check_revocation_async(
929        &self,
930        cert: &rustls::pki_types::CertificateDer<'_>,
931    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NetResult<RevocationStatus>> + Send + '_>>
932    {
933        let cert_bytes = cert.as_ref().to_vec();
934        let fingerprint = cert_fingerprint(&cert_bytes);
935
936        // Check cache first
937        if let Some(status) = self.get_cached(&fingerprint) {
938            return Box::pin(async move { Ok(status) });
939        }
940
941        let timeout = self.timeout;
942
943        Box::pin(async move {
944            // Resolve responder URL
945            let url = match self.resolve_responder_url(&cert_bytes) {
946                Ok(Some(url)) => url,
947                Ok(None) => {
948                    warn!("OCSP: no responder URL available for certificate");
949                    return Ok(RevocationStatus::Unknown);
950                }
951                Err(e) => {
952                    warn!("OCSP: failed to resolve responder URL: {e}");
953                    return Ok(RevocationStatus::Unknown);
954                }
955            };
956
957            // Build OCSP request
958            let request_der = match build_ocsp_request(&cert_bytes) {
959                Ok(req) => req,
960                Err(e) => {
961                    warn!("OCSP: failed to build request: {e}");
962                    return Ok(RevocationStatus::Unknown);
963                }
964            };
965
966            // Send OCSP request
967            let response_der = match send_ocsp_request(&url, &request_der, timeout).await {
968                Ok(resp) => resp,
969                Err(e) => {
970                    warn!("OCSP: network error: {e}");
971                    return Ok(RevocationStatus::Unknown);
972                }
973            };
974
975            // Parse OCSP response
976            let status = match parse_ocsp_response(&response_der) {
977                Ok(s) => s,
978                Err(e) => {
979                    warn!("OCSP: failed to parse response: {e}");
980                    RevocationStatus::Unknown
981                }
982            };
983
984            // Cache the result
985            self.cache_status(fingerprint, status);
986
987            Ok(status)
988        })
989    }
990}
991
992// ── Tests ────────────────────────────────────────────────────────────
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997    use crate::tls::SelfSignedGenerator;
998
999    /// Helper: generate a self-signed certificate for testing
1000    fn gen_test_cert() -> (rustls::pki_types::CertificateDer<'static>, Vec<u8>) {
1001        let generator = SelfSignedGenerator::new("test-ocsp");
1002        let (cert, _key) = generator.generate().expect("should generate cert");
1003        let der = cert.as_ref().to_vec();
1004        (cert, der)
1005    }
1006
1007    // ── 1. test_build_ocsp_request_structure ──
1008
1009    #[test]
1010    fn test_build_ocsp_request_structure() {
1011        let (_cert, der) = gen_test_cert();
1012        let req = build_ocsp_request(&der).expect("should build OCSP request");
1013
1014        // Verify it starts with SEQUENCE tag
1015        assert_eq!(
1016            req[0], TAG_SEQUENCE,
1017            "OCSP request must start with SEQUENCE tag"
1018        );
1019
1020        // Verify we can parse the outer SEQUENCE
1021        let (tag, content, total) = der_read_tlv(&req).expect("should parse outer SEQUENCE");
1022        assert_eq!(tag, TAG_SEQUENCE);
1023        assert_eq!(total, req.len(), "entire request should be consumed");
1024
1025        // Parse tbsRequest (inner SEQUENCE)
1026        let (tbs_tag, tbs_content, _) = der_read_tlv(content).expect("should parse TBSRequest");
1027        assert_eq!(tbs_tag, TAG_SEQUENCE);
1028
1029        // Parse requestList (inner SEQUENCE)
1030        let (rl_tag, rl_content, _) = der_read_tlv(tbs_content).expect("should parse requestList");
1031        assert_eq!(rl_tag, TAG_SEQUENCE);
1032
1033        // Parse first Request (inner SEQUENCE)
1034        let (r_tag, r_content, _) = der_read_tlv(rl_content).expect("should parse Request");
1035        assert_eq!(r_tag, TAG_SEQUENCE);
1036
1037        // Request contains CertID (a SEQUENCE), parse into it
1038        let (cid_tag, cid_content, _) = der_read_tlv(r_content).expect("should parse CertID");
1039        assert_eq!(cid_tag, TAG_SEQUENCE);
1040
1041        // Parse CertID children — should have 4: algo, nameHash, keyHash, serial
1042        let cert_id_children = der_children(cid_content).expect("should parse CertID fields");
1043        assert_eq!(
1044            cert_id_children.len(),
1045            4,
1046            "CertID must have 4 fields (algo, nameHash, keyHash, serial)"
1047        );
1048
1049        // First child: AlgorithmIdentifier SEQUENCE
1050        assert_eq!(cert_id_children[0].0, TAG_SEQUENCE, "algo must be SEQUENCE");
1051        // Second: OCTET STRING (issuerNameHash)
1052        assert_eq!(cert_id_children[1].0, TAG_OCTET_STRING);
1053        // Third: OCTET STRING (issuerKeyHash)
1054        assert_eq!(cert_id_children[2].0, TAG_OCTET_STRING);
1055        // Fourth: INTEGER (serialNumber)
1056        assert_eq!(cert_id_children[3].0, TAG_INTEGER);
1057    }
1058
1059    // ── 2. test_extract_ocsp_url_from_aia ──
1060
1061    #[test]
1062    fn test_extract_ocsp_url_from_aia() {
1063        // Self-signed certs from rcgen typically don't have AIA, so expect None
1064        let (_cert, der) = gen_test_cert();
1065        let url = extract_ocsp_url(&der).expect("should not error");
1066        // Self-signed cert has no AIA extension
1067        assert_eq!(url, None);
1068    }
1069
1070    // ── 3. test_parse_ocsp_response_good ──
1071
1072    #[test]
1073    fn test_parse_ocsp_response_good() {
1074        // Build a synthetic OCSP response with certStatus = good [0]
1075        let response = build_test_ocsp_response(0x80, &[0x00]); // good = [0] IMPLICIT NULL
1076        let status = parse_ocsp_response(&response).expect("should parse good response");
1077        assert_eq!(status, RevocationStatus::Good);
1078    }
1079
1080    // ── 4. test_parse_ocsp_response_revoked ──
1081
1082    #[test]
1083    fn test_parse_ocsp_response_revoked() {
1084        // Build a synthetic OCSP response with certStatus = revoked [1]
1085        // revoked [1] IMPLICIT RevokedInfo — minimal: just a GeneralizedTime
1086        let revoked_info = der_tlv(0x18, b"20250101000000Z"); // revocationTime
1087        let response = build_test_ocsp_response(0xA1, &revoked_info);
1088        let status = parse_ocsp_response(&response).expect("should parse revoked response");
1089        assert_eq!(status, RevocationStatus::Revoked);
1090    }
1091
1092    // ── 5. test_parse_ocsp_response_unknown ──
1093
1094    #[test]
1095    fn test_parse_ocsp_response_unknown() {
1096        // Build a synthetic OCSP response with certStatus = unknown [2]
1097        let response = build_test_ocsp_response(0x82, &[0x00]); // unknown = [2] IMPLICIT NULL
1098        let status = parse_ocsp_response(&response).expect("should parse unknown response");
1099        assert_eq!(status, RevocationStatus::Unknown);
1100    }
1101
1102    // ── 6. test_parse_ocsp_response_malformed ──
1103
1104    #[test]
1105    fn test_parse_ocsp_response_malformed() {
1106        let garbage = vec![0xFF, 0x01, 0x02, 0x03, 0xDE, 0xAD, 0xBE, 0xEF];
1107        let result = parse_ocsp_response(&garbage);
1108        assert!(result.is_err(), "garbage bytes should return error");
1109    }
1110
1111    // ── 7. test_ocsp_cache_hit ──
1112
1113    #[test]
1114    fn test_ocsp_cache_hit() {
1115        let checker = OcspRevocationChecker::new().with_cache_ttl(Duration::from_secs(3600));
1116        let (cert, _der) = gen_test_cert();
1117
1118        let fingerprint = cert_fingerprint(cert.as_ref());
1119        checker.cache_status(fingerprint, RevocationStatus::Good);
1120
1121        let status = checker
1122            .check_revocation(&cert)
1123            .expect("should check revocation");
1124        assert_eq!(status, RevocationStatus::Good);
1125    }
1126
1127    // ── 8. test_ocsp_cache_miss_and_populate ──
1128
1129    #[tokio::test]
1130    async fn test_ocsp_cache_miss_and_populate() {
1131        let checker = OcspRevocationChecker::new().with_cache_ttl(Duration::from_secs(3600));
1132        let (cert, _der) = gen_test_cert();
1133
1134        let fingerprint = cert_fingerprint(cert.as_ref());
1135
1136        // Cache is empty, sync check should return Unknown
1137        assert!(checker.get_cached(&fingerprint).is_none());
1138
1139        let status = checker
1140            .check_revocation(&cert)
1141            .expect("should check revocation");
1142        assert_eq!(status, RevocationStatus::Unknown);
1143
1144        // Manually populate cache
1145        checker.cache_status(fingerprint.clone(), RevocationStatus::Good);
1146        assert_eq!(
1147            checker.get_cached(&fingerprint),
1148            Some(RevocationStatus::Good)
1149        );
1150        assert_eq!(checker.cache_size(), 1);
1151    }
1152
1153    // ── 9. test_ocsp_cache_expiry ──
1154
1155    #[test]
1156    fn test_ocsp_cache_expiry() {
1157        // Set very short TTL
1158        let checker = OcspRevocationChecker::new().with_cache_ttl(Duration::from_millis(1));
1159        let (cert, _der) = gen_test_cert();
1160
1161        let fingerprint = cert_fingerprint(cert.as_ref());
1162        checker.cache_status(fingerprint.clone(), RevocationStatus::Good);
1163
1164        // Sleep to let TTL expire
1165        std::thread::sleep(Duration::from_millis(10));
1166
1167        // Cache entry should be expired
1168        assert!(
1169            checker.get_cached(&fingerprint).is_none(),
1170            "expired cache entry should not be returned"
1171        );
1172
1173        // Check should return Unknown (no network in sync)
1174        let status = checker
1175            .check_revocation(&cert)
1176            .expect("should check revocation");
1177        assert_eq!(status, RevocationStatus::Unknown);
1178    }
1179
1180    // ── 10. test_ocsp_sync_cache_only ──
1181
1182    #[test]
1183    fn test_ocsp_sync_cache_only() {
1184        // Sync check should never block on network, even with a responder URL
1185        let checker = OcspRevocationChecker::new()
1186            .with_responder_url("http://ocsp.example.com")
1187            .with_cache_ttl(Duration::from_secs(3600));
1188
1189        let (cert, _der) = gen_test_cert();
1190
1191        // Sync check with no cache entry returns Unknown (no network)
1192        let status = checker
1193            .check_revocation(&cert)
1194            .expect("should check revocation");
1195        assert_eq!(status, RevocationStatus::Unknown);
1196    }
1197
1198    // ── 11. test_ocsp_fallback_on_error ──
1199
1200    #[tokio::test]
1201    async fn test_ocsp_fallback_on_error() {
1202        // Point to a non-existent responder — should fail-open
1203        let checker = OcspRevocationChecker::new()
1204            .with_responder_url("http://127.0.0.1:1")
1205            .with_timeout(Duration::from_millis(100));
1206
1207        let (cert, _der) = gen_test_cert();
1208
1209        let status = checker
1210            .check_revocation_async(&cert)
1211            .await
1212            .expect("should not error even on network failure");
1213        assert_eq!(status, RevocationStatus::Unknown);
1214    }
1215
1216    // ── 12. test_ocsp_with_custom_responder ──
1217
1218    #[test]
1219    fn test_ocsp_with_custom_responder() {
1220        let checker =
1221            OcspRevocationChecker::new().with_responder_url("http://custom-ocsp.example.com/ocsp");
1222
1223        let (_cert, der) = gen_test_cert();
1224
1225        // The custom URL should override any AIA URL
1226        let url = checker
1227            .resolve_responder_url(&der)
1228            .expect("should resolve URL");
1229        assert_eq!(url, Some("http://custom-ocsp.example.com/ocsp".to_string()));
1230    }
1231
1232    // ── Additional: test_parse_url ──
1233
1234    #[test]
1235    fn test_parse_url_variants() {
1236        let (host, port, path) =
1237            parse_url("http://ocsp.example.com:8080/ocsp").expect("should parse");
1238        assert_eq!(host, "ocsp.example.com");
1239        assert_eq!(port, 8080);
1240        assert_eq!(path, "/ocsp");
1241
1242        let (host, port, path) = parse_url("http://ocsp.example.com/check").expect("should parse");
1243        assert_eq!(host, "ocsp.example.com");
1244        assert_eq!(port, 80);
1245        assert_eq!(path, "/check");
1246
1247        let (host, port, path) = parse_url("http://ocsp.example.com").expect("should parse");
1248        assert_eq!(host, "ocsp.example.com");
1249        assert_eq!(port, 80);
1250        assert_eq!(path, "/");
1251    }
1252
1253    // ── Additional: test_der_encoding_helpers ──
1254
1255    #[test]
1256    fn test_der_integer_from_bytes() {
1257        // Small positive integer
1258        let encoded = der_integer_from_bytes(&[0x05]);
1259        assert_eq!(encoded, vec![TAG_INTEGER, 0x01, 0x05]);
1260
1261        // Integer with MSB set needs leading zero
1262        let encoded = der_integer_from_bytes(&[0x80]);
1263        assert_eq!(encoded, vec![TAG_INTEGER, 0x02, 0x00, 0x80]);
1264
1265        // Multi-byte integer
1266        let encoded = der_integer_from_bytes(&[0x01, 0x00]);
1267        assert_eq!(encoded, vec![TAG_INTEGER, 0x02, 0x01, 0x00]);
1268
1269        // Leading zeros stripped
1270        let encoded = der_integer_from_bytes(&[0x00, 0x00, 0x42]);
1271        assert_eq!(encoded, vec![TAG_INTEGER, 0x01, 0x42]);
1272    }
1273
1274    #[test]
1275    fn test_der_encode_length() {
1276        assert_eq!(der_encode_length(0), vec![0x00]);
1277        assert_eq!(der_encode_length(127), vec![0x7F]);
1278        assert_eq!(der_encode_length(128), vec![0x81, 0x80]);
1279        assert_eq!(der_encode_length(256), vec![0x82, 0x01, 0x00]);
1280    }
1281
1282    // ── Test helper: build a synthetic OCSP response DER ──
1283
1284    /// Build a minimal valid OCSP response with the given certStatus tag and data
1285    fn build_test_ocsp_response(cert_status_tag: u8, cert_status_data: &[u8]) -> Vec<u8> {
1286        // CertID (minimal: algo + two hashes + serial)
1287        let algo = der_algorithm_identifier(SHA256_OID_BYTES);
1288        let name_hash = der_octet_string(&[0u8; 32]);
1289        let key_hash = der_octet_string(&[0u8; 32]);
1290        let serial = der_integer_from_bytes(&[0x01]);
1291        let mut cert_id_content = Vec::new();
1292        cert_id_content.extend(&algo);
1293        cert_id_content.extend(&name_hash);
1294        cert_id_content.extend(&key_hash);
1295        cert_id_content.extend(&serial);
1296        let cert_id = der_tlv(TAG_SEQUENCE, &cert_id_content);
1297
1298        // certStatus
1299        let cert_status = der_tlv(cert_status_tag, cert_status_data);
1300
1301        // thisUpdate (GeneralizedTime)
1302        let this_update = der_tlv(0x18, b"20250101000000Z");
1303
1304        // SingleResponse
1305        let mut sr_content = Vec::new();
1306        sr_content.extend(&cert_id);
1307        sr_content.extend(&cert_status);
1308        sr_content.extend(&this_update);
1309        let single_response = der_tlv(TAG_SEQUENCE, &sr_content);
1310
1311        // responses (SEQUENCE OF SingleResponse)
1312        let responses = der_tlv(TAG_SEQUENCE, &single_response);
1313
1314        // responderID [1] byKey (minimal)
1315        let responder_id = der_tlv(0xA1, &der_octet_string(&[0u8; 20]));
1316
1317        // producedAt
1318        let produced_at = der_tlv(0x18, b"20250101000000Z");
1319
1320        // tbsResponseData
1321        let mut tbs_content = Vec::new();
1322        tbs_content.extend(&responder_id);
1323        tbs_content.extend(&produced_at);
1324        tbs_content.extend(&responses);
1325        let tbs_response_data = der_tlv(TAG_SEQUENCE, &tbs_content);
1326
1327        // signatureAlgorithm (sha256WithRSAEncryption is fine as placeholder)
1328        let sig_algo = der_algorithm_identifier(SHA256_OID_BYTES);
1329
1330        // signature BIT STRING (minimal placeholder)
1331        let signature = der_tlv(0x03, &[0x00, 0x00]); // 0 unused bits + 1 byte
1332
1333        // BasicOCSPResponse
1334        let mut basic_content = Vec::new();
1335        basic_content.extend(&tbs_response_data);
1336        basic_content.extend(&sig_algo);
1337        basic_content.extend(&signature);
1338        let basic_ocsp_response = der_tlv(TAG_SEQUENCE, &basic_content);
1339
1340        // responseBytes: SEQUENCE { responseType OID, response OCTET STRING }
1341        let response_type = der_oid(OCSP_BASIC_OID_BYTES);
1342        let response_octet = der_octet_string(&basic_ocsp_response);
1343        let mut rb_content = Vec::new();
1344        rb_content.extend(&response_type);
1345        rb_content.extend(&response_octet);
1346        let response_bytes_seq = der_tlv(TAG_SEQUENCE, &rb_content);
1347        let response_bytes = der_tlv(TAG_CONTEXT_0, &response_bytes_seq);
1348
1349        // responseStatus ENUMERATED = 0 (successful)
1350        let response_status = der_tlv(TAG_ENUMERATED, &[0x00]);
1351
1352        // OCSPResponse
1353        let mut ocsp_resp_content = Vec::new();
1354        ocsp_resp_content.extend(&response_status);
1355        ocsp_resp_content.extend(&response_bytes);
1356        der_tlv(TAG_SEQUENCE, &ocsp_resp_content)
1357    }
1358}