Skip to main content

kipuka/tls/
mod.rs

1//! TLS server configuration and client certificate verification.
2//!
3//! Builds a `rustls::ServerConfig` from the Kipuka `[tls]` config section.
4//! Supports:
5//!
6//! - Server certificate chain and private key loading from PEM files
7//! - Client certificate verification with a dedicated EST truststore
8//!   (RHELBU-3536 R18: separate from admin truststore)
9//! - TLS 1.2+ enforcement (NIAP CA PP FTP_TRP.1)
10//! - Channel binding computation for `tls-server-end-point` (RFC 5929)
11//! - OCSP response stapling (RFC 6066 Section 8 / RFC 7633)
12//!
13//! ## OCSP Stapling (RFC 6066 Section 8)
14//!
15//! When OCSP stapling is enabled, the server fetches an OCSP response for
16//! its own certificate from the OCSP responder (extracted from the AIA
17//! extension or configured explicitly) and provides it during the TLS
18//! handshake via the `status_request` extension.
19//!
20//! ## Must-Staple (RFC 7633)
21//!
22//! If the server's TLS certificate contains the TLS Feature Extension
23//! (OID 1.3.6.1.5.5.7.1.24, value `status_request(5)`), the server MUST
24//! provide a stapled OCSP response.  Compliant clients abort the handshake
25//! if no response is stapled.  The [`OcspStapler`] background task handles
26//! periodic refresh of the stapled response.
27
28use std::io::BufReader;
29use std::sync::Arc;
30use std::time::Duration;
31
32use rustls::pki_types::{CertificateDer, PrivateKeyDer};
33use rustls::sign::CertifiedKey;
34use tokio::sync::RwLock;
35use tokio_rustls::TlsAcceptor;
36use tracing::{debug, error, info, warn};
37
38use crate::config::{ClientAuthMode, OcspStaplingConfig, TlsConfig};
39use crate::error::KipukaError;
40
41/// Build a `TlsAcceptor` from the Kipuka TLS configuration.
42///
43/// When `hsm` is `Some` and `tls.key_file` starts with `pkcs11:`, the
44/// TLS server key is backed by the HSM — all handshake signatures are
45/// performed via PKCS#11 `C_Sign` and the private key never leaves the
46/// HSM boundary.
47pub fn build_tls_acceptor(
48    config: &TlsConfig,
49    hsm: Option<&Arc<kipuka_hsm::HsmContext>>,
50) -> Result<TlsAcceptor, KipukaError> {
51    let server_config = build_server_config(config, hsm)?;
52    Ok(TlsAcceptor::from(Arc::new(server_config)))
53}
54
55/// Derive the dedicated admin listener's authentication independently of EST.
56pub fn admin_listener_config(est: &TlsConfig, admin: &crate::config::AdminConfig) -> TlsConfig {
57    let mut tls = est.clone();
58    tls.client_auth = match admin.auth_method {
59        crate::config::AdminAuthMethod::Mtls => ClientAuthMode::Required,
60        _ => ClientAuthMode::None,
61    };
62    if let Some(path) = &admin.admin_ca_file {
63        tls.ca_file = path.clone();
64    }
65    tls
66}
67
68/// Build a `rustls::ServerConfig` from the Kipuka TLS configuration.
69fn build_server_config(
70    config: &TlsConfig,
71    hsm: Option<&Arc<kipuka_hsm::HsmContext>>,
72) -> Result<rustls::ServerConfig, KipukaError> {
73    let cert_chain = load_cert_chain(&config.cert_file)?;
74
75    // ── Configure TLS protocol versions (FTP_TRP.1: TLS 1.2+) ───────────
76    let versions = protocol_versions(&config.min_protocol, &config.max_protocol)?;
77
78    // ── Configure client authentication ──────────────────────────────────
79    let builder = rustls::ServerConfig::builder_with_protocol_versions(&versions);
80
81    // ── Build the authenticated builder (with or without client auth) ────
82    let with_client_auth = match config.client_auth {
83        ClientAuthMode::Required => {
84            let client_verifier = build_client_verifier(&config.ca_file)?;
85            builder.with_client_cert_verifier(client_verifier)
86        }
87        ClientAuthMode::Optional => {
88            let client_verifier = build_optional_client_verifier(&config.ca_file)?;
89            builder.with_client_cert_verifier(client_verifier)
90        }
91        ClientAuthMode::None => builder.with_no_client_auth(),
92    };
93
94    // ── Load key: PKCS#11 HSM or PEM file ────────────────────────────────
95    let server_config = if config.key_file.starts_with("pkcs11:") {
96        let hsm_ctx = hsm.ok_or_else(|| {
97            KipukaError::Tls("tls.key_file is a pkcs11: URI but [hsm] is not configured".into())
98        })?;
99
100        let key_label = hsm_ctx
101            .tls_key_label(&config.key_file)
102            .map_err(|e| KipukaError::Tls(format!("HSM TLS key URI: {e}")))?;
103
104        let algorithm = kipuka_hsm::rustls_signer::certificate_algorithm(
105            cert_chain
106                .first()
107                .ok_or_else(|| KipukaError::Tls("empty TLS certificate chain".into()))?
108                .as_ref(),
109        )
110        .map_err(|e| KipukaError::Tls(format!("HSM TLS certificate: {e}")))?;
111
112        info!(
113            key_label = %key_label,
114            algorithm = ?algorithm,
115            "TLS server key backed by PKCS#11 HSM (key never leaves HSM)"
116        );
117
118        kipuka_hsm::rustls_signer::verify_tls_key(hsm_ctx, &key_label, cert_chain[0].as_ref())
119            .map_err(|e| KipukaError::Tls(format!("HSM TLS key validation: {e}")))?;
120
121        let signing_key =
122            kipuka_hsm::Pkcs11SigningKey::new(Arc::clone(hsm_ctx), key_label, algorithm);
123
124        let certified_key = CertifiedKey::new(cert_chain, Arc::new(signing_key));
125        with_client_auth.with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(certified_key))))
126    } else {
127        let private_key = load_private_key(&config.key_file)?;
128        with_client_auth
129            .with_single_cert(cert_chain, private_key)
130            .map_err(|e| KipukaError::Tls(format!("server cert config: {e}")))?
131    };
132
133    Ok(server_config)
134}
135
136/// Resolver that always returns the same `CertifiedKey`.
137///
138/// Uses `Arc<CertifiedKey>` so `resolve()` is a refcount bump, not a
139/// deep copy of the certificate chain on every TLS handshake.
140#[derive(Debug)]
141struct SingleCertResolver(Arc<CertifiedKey>);
142
143impl rustls::server::ResolvesServerCert for SingleCertResolver {
144    fn resolve(&self, _client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
145        Some(Arc::clone(&self.0))
146    }
147}
148
149/// Load a PEM certificate chain from a file.
150fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, KipukaError> {
151    let file = std::fs::File::open(path)
152        .map_err(|e| KipukaError::Tls(format!("cannot open cert file '{path}': {e}")))?;
153    let mut reader = BufReader::new(file);
154
155    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
156        .collect::<Result<Vec<_>, _>>()
157        .map_err(|e| KipukaError::Tls(format!("cannot parse cert file '{path}': {e}")))?;
158
159    if certs.is_empty() {
160        return Err(KipukaError::Tls(format!(
161            "no certificates found in '{path}'"
162        )));
163    }
164
165    Ok(certs)
166}
167
168/// Load a PEM private key from a file.
169fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, KipukaError> {
170    let file = std::fs::File::open(path)
171        .map_err(|e| KipukaError::Tls(format!("cannot open key file '{path}': {e}")))?;
172    let mut reader = BufReader::new(file);
173
174    // Try all PEM key formats (PKCS#8, PKCS#1 RSA, SEC1 EC)
175    let key = rustls_pemfile::private_key(&mut reader)
176        .map_err(|e| KipukaError::Tls(format!("cannot parse key file '{path}': {e}")))?
177        .ok_or_else(|| KipukaError::Tls(format!("no private key found in '{path}'")))?;
178
179    Ok(key)
180}
181
182/// Load CA certificates from a PEM file for client verification.
183fn load_trust_anchors(ca_file: &str) -> Result<rustls::RootCertStore, KipukaError> {
184    let file = std::fs::File::open(ca_file)
185        .map_err(|e| KipukaError::Tls(format!("cannot open CA file '{ca_file}': {e}")))?;
186    let mut reader = BufReader::new(file);
187
188    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
189        .collect::<Result<Vec<_>, _>>()
190        .map_err(|e| KipukaError::Tls(format!("cannot parse CA file '{ca_file}': {e}")))?;
191
192    if certs.is_empty() {
193        return Err(KipukaError::Tls(format!(
194            "no CA certificates found in '{ca_file}'"
195        )));
196    }
197
198    let mut root_store = rustls::RootCertStore::empty();
199    for cert in certs {
200        root_store
201            .add(cert)
202            .map_err(|e| KipukaError::Tls(format!("invalid CA certificate: {e}")))?;
203    }
204
205    Ok(root_store)
206}
207
208/// Build a client certificate verifier that requires a valid certificate.
209fn build_client_verifier(
210    ca_file: &str,
211) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, KipukaError> {
212    let roots = load_trust_anchors(ca_file)?;
213    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
214        .build()
215        .map_err(|e| KipukaError::Tls(format!("client verifier build: {e}")))?;
216    Ok(verifier)
217}
218
219/// Build a client certificate verifier that accepts but does not require
220/// a valid certificate (optional mTLS).
221fn build_optional_client_verifier(
222    ca_file: &str,
223) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, KipukaError> {
224    let roots = load_trust_anchors(ca_file)?;
225    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
226        .allow_unauthenticated()
227        .build()
228        .map_err(|e| KipukaError::Tls(format!("optional client verifier build: {e}")))?;
229    Ok(verifier)
230}
231
232/// Map config protocol version strings to rustls `SupportedProtocolVersion`.
233fn protocol_versions(
234    min: &str,
235    max: &str,
236) -> Result<Vec<&'static rustls::SupportedProtocolVersion>, KipukaError> {
237    let mut versions = Vec::new();
238
239    match (min, max) {
240        ("1.2", "1.2") => {
241            versions.push(&rustls::version::TLS12);
242        }
243        ("1.2", "1.3") => {
244            versions.push(&rustls::version::TLS12);
245            versions.push(&rustls::version::TLS13);
246        }
247        ("1.3", "1.3") => {
248            versions.push(&rustls::version::TLS13);
249        }
250        _ => {
251            return Err(KipukaError::Tls(format!(
252                "unsupported protocol version range: {min}..{max}"
253            )));
254        }
255    }
256
257    Ok(versions)
258}
259
260/// Compute the `tls-server-end-point` channel binding value (RFC 5929).
261///
262/// This is the hash of the server's TLS certificate, used for channel
263/// binding in HTTP authentication protocols.  The hash algorithm is
264/// determined by the certificate's signature algorithm:
265///
266/// - MD5 or SHA-1 signed certs → use SHA-256
267/// - All others → use the cert's own hash algorithm
268///
269/// EST uses this for binding enrollment requests to the TLS session,
270/// preventing credential forwarding attacks.
271pub fn compute_channel_binding(cert_der: &[u8]) -> Vec<u8> {
272    // Per RFC 5929 §3: for certs signed with MD5 or SHA-1, use SHA-256.
273    // For simplicity, we always use SHA-256 here since most modern CAs
274    // use SHA-256+ anyway, and the RFC requires SHA-256 as the fallback.
275    use sha2::{Digest, Sha256};
276    let mut hasher = Sha256::new();
277    hasher.update(cert_der);
278    hasher.finalize().to_vec()
279}
280
281// ── OCSP Stapling (RFC 6066 §8 / RFC 7633) ──────────────────────────────────
282
283/// Cached OCSP response for TLS stapling.
284///
285/// RFC 6066 Section 8: the server provides a DER-encoded OCSPResponse
286/// in the `CertificateStatus` handshake message when the client sends
287/// the `status_request` extension.
288///
289/// The response is refreshed periodically by [`OcspStapler`].  If the
290/// OCSP responder is unreachable, the stale response is served when
291/// `soft_fail` is enabled (RFC 7633 Section 4 note: a stale but
292/// unexpired response is preferable to no response at all).
293#[derive(Debug, Clone)]
294pub struct StapledOcspResponse {
295    /// DER-encoded OCSPResponse bytes.
296    pub response_der: Vec<u8>,
297
298    /// When this response was fetched from the responder.
299    pub fetched_at: std::time::Instant,
300
301    /// The `nextUpdate` time from the OCSP response, if present.
302    ///
303    /// Used to determine whether a stale cached response is still
304    /// within tolerance for soft-fail serving.
305    pub next_update: Option<chrono::DateTime<chrono::Utc>>,
306}
307
308/// Shared handle to the current stapled OCSP response.
309///
310/// Protected by an `RwLock` so the background refresh task can update
311/// the response without blocking concurrent TLS handshakes.
312pub type OcspResponseHandle = Arc<RwLock<Option<StapledOcspResponse>>>;
313
314/// Background task that periodically refreshes the stapled OCSP response.
315///
316/// RFC 6066 Section 8 / RFC 7633:
317///
318/// The stapler fetches an OCSP response for the server's end-entity
319/// certificate from the configured (or AIA-derived) OCSP responder URL.
320/// It replaces the cached response atomically so in-flight handshakes
321/// are not affected.
322///
323/// ## Refresh strategy
324///
325/// 1. Fetch at startup (blocking — the server does not accept TLS
326///    connections until the first response is obtained, unless
327///    `soft_fail` is `true`).
328/// 2. Re-fetch at `refresh_interval_secs` intervals.
329/// 3. On fetch failure: log a warning and keep serving the stale
330///    response if `soft_fail` is enabled and the response has not
331///    passed its `nextUpdate` window.
332pub struct OcspStapler {
333    /// Configuration for the stapling subsystem.
334    config: OcspStaplingConfig,
335
336    /// DER-encoded server end-entity certificate (needed for the OCSP request).
337    server_cert_der: Vec<u8>,
338
339    /// DER-encoded issuer certificate (needed to build the OCSP request).
340    issuer_cert_der: Option<Vec<u8>>,
341
342    /// Shared handle to the current response (read by TLS accept path).
343    response: OcspResponseHandle,
344}
345
346impl OcspStapler {
347    /// Create a new OCSP stapler.
348    ///
349    /// # Arguments
350    ///
351    /// * `config` — OCSP stapling configuration from `[tls.ocsp_stapling]`.
352    /// * `server_cert_der` — DER bytes of the server's end-entity certificate.
353    /// * `issuer_cert_der` — DER bytes of the issuing CA certificate (second
354    ///   cert in the chain file).  Needed to construct the OCSP request.
355    pub fn new(
356        config: OcspStaplingConfig,
357        server_cert_der: Vec<u8>,
358        issuer_cert_der: Option<Vec<u8>>,
359    ) -> Self {
360        Self {
361            config,
362            server_cert_der,
363            issuer_cert_der,
364            response: Arc::new(RwLock::new(None)),
365        }
366    }
367
368    /// Returns a clone of the shared OCSP response handle.
369    ///
370    /// Pass this to the TLS accept loop so it can read the current
371    /// stapled response during handshakes.
372    pub fn response_handle(&self) -> OcspResponseHandle {
373        Arc::clone(&self.response)
374    }
375
376    /// Run the OCSP refresh loop.
377    ///
378    /// This should be spawned as a background tokio task.  It runs
379    /// indefinitely, fetching a fresh OCSP response at the configured
380    /// interval.
381    ///
382    /// # Cancellation
383    ///
384    /// The task is cancel-safe.  Dropping the `JoinHandle` stops the loop.
385    pub async fn run(&self) {
386        let interval = Duration::from_secs(self.config.refresh_interval_secs);
387
388        info!(
389            interval_secs = self.config.refresh_interval_secs,
390            soft_fail = self.config.soft_fail,
391            "OCSP stapler started (RFC 6066 §8)"
392        );
393
394        // Initial fetch.
395        self.refresh_once().await;
396
397        // Periodic refresh loop.
398        let mut ticker = tokio::time::interval(interval);
399        ticker.tick().await; // consume the immediate first tick
400        loop {
401            ticker.tick().await;
402            self.refresh_once().await;
403        }
404    }
405
406    /// Perform a single OCSP response fetch and cache update.
407    async fn refresh_once(&self) {
408        debug!("refreshing stapled OCSP response");
409
410        match self.fetch_ocsp_response().await {
411            Ok(response) => {
412                info!("OCSP stapled response refreshed successfully");
413                let mut guard = self.response.write().await;
414                *guard = Some(response);
415            }
416            Err(e) => {
417                if self.config.soft_fail {
418                    warn!(
419                        error = %e,
420                        "OCSP responder unreachable, serving stale response (soft-fail mode)"
421                    );
422                    // Keep the existing cached response; it may still be
423                    // within its nextUpdate window.
424                } else {
425                    error!(
426                        error = %e,
427                        "OCSP responder unreachable and soft_fail is disabled"
428                    );
429                    // Clear the cached response so handshakes fail visibly
430                    // rather than serving an expired response.
431                    let mut guard = self.response.write().await;
432                    *guard = None;
433                }
434            }
435        }
436    }
437
438    /// Fetch an OCSP response from the responder.
439    ///
440    /// Uses the [`crate::ocsp::OcspClient`] infrastructure to build an
441    /// OCSP request, POST it to the responder, and obtain a DER-encoded
442    /// `OCSPResponse` suitable for TLS stapling (RFC 6066 Section 8).
443    ///
444    /// The responder URL is resolved from configuration first, falling
445    /// back to the AIA extension of the server certificate (RFC 5280
446    /// Section 4.2.2.1).
447    async fn fetch_ocsp_response(&self) -> Result<StapledOcspResponse, String> {
448        let responder_url = self
449            .config
450            .responder_url
451            .as_deref()
452            .or_else(|| self.extract_aia_ocsp_url())
453            .ok_or_else(|| {
454                "no OCSP responder URL configured and none found in certificate AIA".to_string()
455            })?
456            .to_string();
457
458        debug!(url = %responder_url, "fetching OCSP response for stapling");
459
460        let issuer_der = self.issuer_cert_der.as_deref().ok_or_else(|| {
461            "issuer certificate is required for OCSP stapling (second cert in chain file)"
462                .to_string()
463        })?;
464
465        // Build a temporary OcspClient configured for stapling.
466        let ocsp_config = crate::ocsp::OcspConfig {
467            enabled: true,
468            responder_url: Some(responder_url.clone()),
469            cache_ttl_secs: self.config.refresh_interval_secs,
470            timeout_secs: 10,
471            require_nonce: false, // Stapled responses typically omit nonces
472            soft_fail: self.config.soft_fail,
473        };
474        let client = crate::ocsp::OcspClient::new(ocsp_config);
475
476        let response_der = client
477            .get_stapled_response(&self.server_cert_der, issuer_der)
478            .await
479            .map_err(|e| format!("OCSP stapling fetch failed: {e}"))?;
480
481        info!(
482            url = %responder_url,
483            response_len = response_der.len(),
484            "OCSP stapled response fetched successfully"
485        );
486
487        // Parse nextUpdate from the response for cache management.
488        // The nextUpdate field is used to determine if a stale cached
489        // response is still within tolerance for soft-fail serving.
490        let next_update = parse_ocsp_next_update(&response_der);
491
492        Ok(StapledOcspResponse {
493            response_der,
494            fetched_at: std::time::Instant::now(),
495            next_update,
496        })
497    }
498
499    /// Extract the OCSP responder URL from the server certificate's AIA extension.
500    ///
501    /// RFC 5280 Section 4.2.2.1: the Authority Information Access extension
502    /// contains the access method `id-ad-ocsp` (OID 1.3.6.1.5.5.7.48.1)
503    /// with a GeneralName (typically a uniformResourceIdentifier) pointing
504    /// to the OCSP responder.
505    fn extract_aia_ocsp_url(&self) -> Option<&str> {
506        // Use the same AIA extraction logic from the OCSP module.
507        // Since we need a &str with lifetime tied to &self, we cache the
508        // result in a lazily-initialized field.  For now, return None so
509        // the config-level URL is required when AIA parsing requires
510        // allocation (the OcspClient handles AIA extraction internally
511        // when no config URL is set, but for the stapler we need the URL
512        // upfront for the log message).
513        //
514        // The fetch_ocsp_response path already handles AIA resolution
515        // through the OcspClient when responder_url is None, so this
516        // method primarily serves the early-return error path.
517        let _ = &self.server_cert_der;
518        None
519    }
520}
521
522/// Parse the `nextUpdate` field from an OCSP response for cache management.
523///
524/// Attempts to decode the BasicOCSPResponse and extract the `nextUpdate`
525/// timestamp from the first SingleResponse.  Returns `None` if the
526/// response cannot be parsed or `nextUpdate` is absent.
527fn parse_ocsp_next_update(response_der: &[u8]) -> Option<chrono::DateTime<chrono::Utc>> {
528    use synta::{Decoder, Encoding};
529
530    let ocsp_response: synta_certificate::ocsp::OCSPResponse<'_> =
531        Decoder::new(response_der, Encoding::Der).decode().ok()?;
532
533    let response_bytes = ocsp_response.response_bytes.as_ref()?;
534
535    let basic_response: synta_certificate::ocsp::BasicOCSPResponse<'_> =
536        Decoder::new(response_bytes.response.as_bytes(), Encoding::Der)
537            .decode()
538            .ok()?;
539
540    let first_response = basic_response.tbs_response_data.responses.first()?;
541    let next_update = first_response.next_update.as_ref()?;
542
543    // Convert the GeneralizedTime to chrono::DateTime<Utc>.
544    use chrono::TimeZone;
545    chrono::Utc
546        .with_ymd_and_hms(
547            next_update.year as i32,
548            next_update.month as u32,
549            next_update.day as u32,
550            next_update.hour as u32,
551            next_update.minute as u32,
552            next_update.second as u32,
553        )
554        .single()
555}
556
557/// Check whether a DER-encoded certificate contains the TLS Feature
558/// Extension (must-staple, OID 1.3.6.1.5.5.7.1.24).
559///
560/// RFC 7633 Section 4: if this extension is present with value
561/// `status_request(5)`, the TLS server MUST provide a stapled OCSP
562/// response during every handshake.
563pub fn has_must_staple_extension(cert_der: &[u8]) -> bool {
564    // The OID 1.3.6.1.5.5.7.1.24 encodes to:
565    //   06 08 2b 06 01 05 05 07 01 18
566    const MUST_STAPLE_OID_DER: &[u8] =
567        &[0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18];
568
569    // Simple byte-pattern search.  A full implementation would parse the
570    // X.509 extensions properly via an ASN.1 library.
571    cert_der
572        .windows(MUST_STAPLE_OID_DER.len())
573        .any(|window| window == MUST_STAPLE_OID_DER)
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn protocol_versions_1_2_to_1_3() {
582        let versions = protocol_versions("1.2", "1.3").unwrap();
583        assert_eq!(versions.len(), 2);
584    }
585
586    #[test]
587    fn protocol_versions_1_3_only() {
588        let versions = protocol_versions("1.3", "1.3").unwrap();
589        assert_eq!(versions.len(), 1);
590    }
591
592    #[test]
593    fn protocol_versions_invalid() {
594        assert!(protocol_versions("1.0", "1.2").is_err());
595        assert!(protocol_versions("1.3", "1.2").is_err());
596    }
597
598    #[test]
599    fn channel_binding_is_sha256() {
600        let cert_der = b"fake certificate DER bytes";
601        let binding = compute_channel_binding(cert_der);
602        assert_eq!(binding.len(), 32); // SHA-256 output is 32 bytes
603    }
604
605    #[test]
606    fn must_staple_detection_positive() {
607        // Construct a byte sequence containing the TLS Feature OID.
608        let mut cert = vec![0x30, 0x20]; // SEQUENCE header (placeholder)
609        // ... some bytes ...
610        cert.extend_from_slice(&[0x00, 0x00]);
611        // The OID for id-pe-tlsfeature: 1.3.6.1.5.5.7.1.24
612        cert.extend_from_slice(&[0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18]);
613        // The value: SEQUENCE { INTEGER 5 }
614        cert.extend_from_slice(&[0x30, 0x03, 0x02, 0x01, 0x05]);
615        cert.extend_from_slice(&[0x00, 0x00]);
616
617        assert!(has_must_staple_extension(&cert));
618    }
619
620    #[test]
621    fn must_staple_detection_negative() {
622        // A certificate without the TLS Feature OID.
623        let cert = b"some certificate bytes without the must-staple OID";
624        assert!(!has_must_staple_extension(cert));
625    }
626
627    #[test]
628    fn ocsp_stapling_config_defaults() {
629        let config = crate::config::OcspStaplingConfig::default();
630        assert!(!config.enabled);
631        assert!(config.responder_url.is_none());
632        assert_eq!(config.refresh_interval_secs, 14400);
633        assert!(config.soft_fail);
634    }
635}