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 tokio::sync::RwLock;
34use tokio_rustls::TlsAcceptor;
35use tracing::{debug, error, info, warn};
36
37use crate::config::{ClientAuthMode, OcspStaplingConfig, TlsConfig};
38use crate::error::KipukaError;
39
40/// Build a `TlsAcceptor` from the Kipuka TLS configuration.
41///
42/// The resulting acceptor can be used with `tokio_rustls` to wrap a
43/// TCP listener.
44///
45/// When OCSP stapling is enabled, the returned acceptor includes a
46/// `CertifiedKey` that carries the stapled OCSP response.  The caller
47/// should also spawn the [`OcspStapler`] background task to keep the
48/// stapled response fresh.
49pub fn build_tls_acceptor(config: &TlsConfig) -> Result<TlsAcceptor, KipukaError> {
50    let server_config = build_server_config(config)?;
51    Ok(TlsAcceptor::from(Arc::new(server_config)))
52}
53
54/// Build a `rustls::ServerConfig` from the Kipuka TLS configuration.
55fn build_server_config(config: &TlsConfig) -> Result<rustls::ServerConfig, KipukaError> {
56    // ── Load server certificate chain ────────────────────────────────────
57    let cert_chain = load_cert_chain(&config.cert_file)?;
58    let private_key = load_private_key(&config.key_file)?;
59
60    // ── Configure TLS protocol versions (FTP_TRP.1: TLS 1.2+) ───────────
61    let versions = protocol_versions(&config.min_protocol, &config.max_protocol)?;
62
63    // ── Configure client authentication ──────────────────────────────────
64    let builder = rustls::ServerConfig::builder_with_protocol_versions(&versions);
65
66    let server_config = match config.client_auth {
67        ClientAuthMode::Required => {
68            let client_verifier = build_client_verifier(&config.ca_file)?;
69            builder
70                .with_client_cert_verifier(client_verifier)
71                .with_single_cert(cert_chain, private_key)
72                .map_err(|e| KipukaError::Tls(format!("server cert config: {e}")))?
73        }
74        ClientAuthMode::Optional => {
75            let client_verifier = build_optional_client_verifier(&config.ca_file)?;
76            builder
77                .with_client_cert_verifier(client_verifier)
78                .with_single_cert(cert_chain, private_key)
79                .map_err(|e| KipukaError::Tls(format!("server cert config: {e}")))?
80        }
81        ClientAuthMode::None => builder
82            .with_no_client_auth()
83            .with_single_cert(cert_chain, private_key)
84            .map_err(|e| KipukaError::Tls(format!("server cert config: {e}")))?,
85    };
86
87    Ok(server_config)
88}
89
90/// Load a PEM certificate chain from a file.
91fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, KipukaError> {
92    let file = std::fs::File::open(path)
93        .map_err(|e| KipukaError::Tls(format!("cannot open cert file '{path}': {e}")))?;
94    let mut reader = BufReader::new(file);
95
96    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
97        .collect::<Result<Vec<_>, _>>()
98        .map_err(|e| KipukaError::Tls(format!("cannot parse cert file '{path}': {e}")))?;
99
100    if certs.is_empty() {
101        return Err(KipukaError::Tls(format!(
102            "no certificates found in '{path}'"
103        )));
104    }
105
106    Ok(certs)
107}
108
109/// Load a PEM private key from a file.
110fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, KipukaError> {
111    let file = std::fs::File::open(path)
112        .map_err(|e| KipukaError::Tls(format!("cannot open key file '{path}': {e}")))?;
113    let mut reader = BufReader::new(file);
114
115    // Try all PEM key formats (PKCS#8, PKCS#1 RSA, SEC1 EC)
116    let key = rustls_pemfile::private_key(&mut reader)
117        .map_err(|e| KipukaError::Tls(format!("cannot parse key file '{path}': {e}")))?
118        .ok_or_else(|| KipukaError::Tls(format!("no private key found in '{path}'")))?;
119
120    Ok(key)
121}
122
123/// Load CA certificates from a PEM file for client verification.
124fn load_trust_anchors(ca_file: &str) -> Result<rustls::RootCertStore, KipukaError> {
125    let file = std::fs::File::open(ca_file)
126        .map_err(|e| KipukaError::Tls(format!("cannot open CA file '{ca_file}': {e}")))?;
127    let mut reader = BufReader::new(file);
128
129    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
130        .collect::<Result<Vec<_>, _>>()
131        .map_err(|e| KipukaError::Tls(format!("cannot parse CA file '{ca_file}': {e}")))?;
132
133    if certs.is_empty() {
134        return Err(KipukaError::Tls(format!(
135            "no CA certificates found in '{ca_file}'"
136        )));
137    }
138
139    let mut root_store = rustls::RootCertStore::empty();
140    for cert in certs {
141        root_store
142            .add(cert)
143            .map_err(|e| KipukaError::Tls(format!("invalid CA certificate: {e}")))?;
144    }
145
146    Ok(root_store)
147}
148
149/// Build a client certificate verifier that requires a valid certificate.
150fn build_client_verifier(
151    ca_file: &str,
152) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, KipukaError> {
153    let roots = load_trust_anchors(ca_file)?;
154    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
155        .build()
156        .map_err(|e| KipukaError::Tls(format!("client verifier build: {e}")))?;
157    Ok(verifier)
158}
159
160/// Build a client certificate verifier that accepts but does not require
161/// a valid certificate (optional mTLS).
162fn build_optional_client_verifier(
163    ca_file: &str,
164) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, KipukaError> {
165    let roots = load_trust_anchors(ca_file)?;
166    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
167        .allow_unauthenticated()
168        .build()
169        .map_err(|e| KipukaError::Tls(format!("optional client verifier build: {e}")))?;
170    Ok(verifier)
171}
172
173/// Map config protocol version strings to rustls `SupportedProtocolVersion`.
174fn protocol_versions(
175    min: &str,
176    max: &str,
177) -> Result<Vec<&'static rustls::SupportedProtocolVersion>, KipukaError> {
178    let mut versions = Vec::new();
179
180    match (min, max) {
181        ("1.2", "1.2") => {
182            versions.push(&rustls::version::TLS12);
183        }
184        ("1.2", "1.3") => {
185            versions.push(&rustls::version::TLS12);
186            versions.push(&rustls::version::TLS13);
187        }
188        ("1.3", "1.3") => {
189            versions.push(&rustls::version::TLS13);
190        }
191        _ => {
192            return Err(KipukaError::Tls(format!(
193                "unsupported protocol version range: {min}..{max}"
194            )));
195        }
196    }
197
198    Ok(versions)
199}
200
201/// Compute the `tls-server-end-point` channel binding value (RFC 5929).
202///
203/// This is the hash of the server's TLS certificate, used for channel
204/// binding in HTTP authentication protocols.  The hash algorithm is
205/// determined by the certificate's signature algorithm:
206///
207/// - MD5 or SHA-1 signed certs → use SHA-256
208/// - All others → use the cert's own hash algorithm
209///
210/// EST uses this for binding enrollment requests to the TLS session,
211/// preventing credential forwarding attacks.
212pub fn compute_channel_binding(cert_der: &[u8]) -> Vec<u8> {
213    // Per RFC 5929 §3: for certs signed with MD5 or SHA-1, use SHA-256.
214    // For simplicity, we always use SHA-256 here since most modern CAs
215    // use SHA-256+ anyway, and the RFC requires SHA-256 as the fallback.
216    use sha2::{Digest, Sha256};
217    let mut hasher = Sha256::new();
218    hasher.update(cert_der);
219    hasher.finalize().to_vec()
220}
221
222// ── OCSP Stapling (RFC 6066 §8 / RFC 7633) ──────────────────────────────────
223
224/// Cached OCSP response for TLS stapling.
225///
226/// RFC 6066 Section 8: the server provides a DER-encoded OCSPResponse
227/// in the `CertificateStatus` handshake message when the client sends
228/// the `status_request` extension.
229///
230/// The response is refreshed periodically by [`OcspStapler`].  If the
231/// OCSP responder is unreachable, the stale response is served when
232/// `soft_fail` is enabled (RFC 7633 Section 4 note: a stale but
233/// unexpired response is preferable to no response at all).
234#[derive(Debug, Clone)]
235pub struct StapledOcspResponse {
236    /// DER-encoded OCSPResponse bytes.
237    pub response_der: Vec<u8>,
238
239    /// When this response was fetched from the responder.
240    pub fetched_at: std::time::Instant,
241
242    /// The `nextUpdate` time from the OCSP response, if present.
243    ///
244    /// Used to determine whether a stale cached response is still
245    /// within tolerance for soft-fail serving.
246    pub next_update: Option<chrono::DateTime<chrono::Utc>>,
247}
248
249/// Shared handle to the current stapled OCSP response.
250///
251/// Protected by an `RwLock` so the background refresh task can update
252/// the response without blocking concurrent TLS handshakes.
253pub type OcspResponseHandle = Arc<RwLock<Option<StapledOcspResponse>>>;
254
255/// Background task that periodically refreshes the stapled OCSP response.
256///
257/// RFC 6066 Section 8 / RFC 7633:
258///
259/// The stapler fetches an OCSP response for the server's end-entity
260/// certificate from the configured (or AIA-derived) OCSP responder URL.
261/// It replaces the cached response atomically so in-flight handshakes
262/// are not affected.
263///
264/// ## Refresh strategy
265///
266/// 1. Fetch at startup (blocking — the server does not accept TLS
267///    connections until the first response is obtained, unless
268///    `soft_fail` is `true`).
269/// 2. Re-fetch at `refresh_interval_secs` intervals.
270/// 3. On fetch failure: log a warning and keep serving the stale
271///    response if `soft_fail` is enabled and the response has not
272///    passed its `nextUpdate` window.
273pub struct OcspStapler {
274    /// Configuration for the stapling subsystem.
275    config: OcspStaplingConfig,
276
277    /// DER-encoded server end-entity certificate (needed for the OCSP request).
278    server_cert_der: Vec<u8>,
279
280    /// DER-encoded issuer certificate (needed to build the OCSP request).
281    issuer_cert_der: Option<Vec<u8>>,
282
283    /// Shared handle to the current response (read by TLS accept path).
284    response: OcspResponseHandle,
285}
286
287impl OcspStapler {
288    /// Create a new OCSP stapler.
289    ///
290    /// # Arguments
291    ///
292    /// * `config` — OCSP stapling configuration from `[tls.ocsp_stapling]`.
293    /// * `server_cert_der` — DER bytes of the server's end-entity certificate.
294    /// * `issuer_cert_der` — DER bytes of the issuing CA certificate (second
295    ///   cert in the chain file).  Needed to construct the OCSP request.
296    pub fn new(
297        config: OcspStaplingConfig,
298        server_cert_der: Vec<u8>,
299        issuer_cert_der: Option<Vec<u8>>,
300    ) -> Self {
301        Self {
302            config,
303            server_cert_der,
304            issuer_cert_der,
305            response: Arc::new(RwLock::new(None)),
306        }
307    }
308
309    /// Returns a clone of the shared OCSP response handle.
310    ///
311    /// Pass this to the TLS accept loop so it can read the current
312    /// stapled response during handshakes.
313    pub fn response_handle(&self) -> OcspResponseHandle {
314        Arc::clone(&self.response)
315    }
316
317    /// Run the OCSP refresh loop.
318    ///
319    /// This should be spawned as a background tokio task.  It runs
320    /// indefinitely, fetching a fresh OCSP response at the configured
321    /// interval.
322    ///
323    /// # Cancellation
324    ///
325    /// The task is cancel-safe.  Dropping the `JoinHandle` stops the loop.
326    pub async fn run(&self) {
327        let interval = Duration::from_secs(self.config.refresh_interval_secs);
328
329        info!(
330            interval_secs = self.config.refresh_interval_secs,
331            soft_fail = self.config.soft_fail,
332            "OCSP stapler started (RFC 6066 §8)"
333        );
334
335        // Initial fetch.
336        self.refresh_once().await;
337
338        // Periodic refresh loop.
339        let mut ticker = tokio::time::interval(interval);
340        ticker.tick().await; // consume the immediate first tick
341        loop {
342            ticker.tick().await;
343            self.refresh_once().await;
344        }
345    }
346
347    /// Perform a single OCSP response fetch and cache update.
348    async fn refresh_once(&self) {
349        debug!("refreshing stapled OCSP response");
350
351        match self.fetch_ocsp_response().await {
352            Ok(response) => {
353                info!("OCSP stapled response refreshed successfully");
354                let mut guard = self.response.write().await;
355                *guard = Some(response);
356            }
357            Err(e) => {
358                if self.config.soft_fail {
359                    warn!(
360                        error = %e,
361                        "OCSP responder unreachable, serving stale response (soft-fail mode)"
362                    );
363                    // Keep the existing cached response; it may still be
364                    // within its nextUpdate window.
365                } else {
366                    error!(
367                        error = %e,
368                        "OCSP responder unreachable and soft_fail is disabled"
369                    );
370                    // Clear the cached response so handshakes fail visibly
371                    // rather than serving an expired response.
372                    let mut guard = self.response.write().await;
373                    *guard = None;
374                }
375            }
376        }
377    }
378
379    /// Fetch an OCSP response from the responder.
380    ///
381    /// Constructs an OCSP request for the server certificate, sends it
382    /// to the responder URL (from config or AIA), and parses the response.
383    async fn fetch_ocsp_response(&self) -> Result<StapledOcspResponse, String> {
384        let responder_url = self
385            .config
386            .responder_url
387            .as_deref()
388            .or_else(|| self.extract_aia_ocsp_url())
389            .ok_or_else(|| {
390                "no OCSP responder URL configured and none found in certificate AIA".to_string()
391            })?
392            .to_string();
393
394        debug!(url = %responder_url, "fetching OCSP response");
395
396        // TODO: Build OCSP request from server_cert_der + issuer_cert_der
397        // using the `ocsp` or `x509-ocsp` crate, POST to responder_url,
398        // and parse the OCSPResponse.
399        //
400        // The implementation should:
401        // 1. Extract serial number and issuer name hash from server cert
402        // 2. Build an OCSPRequest with a single CertID
403        // 3. HTTP POST to responder_url with Content-Type: application/ocsp-request
404        // 4. Parse the OCSPResponse, verify signature against issuer cert
405        // 5. Extract thisUpdate/nextUpdate for cache management
406        let _ = &self.server_cert_der;
407        let _ = &self.issuer_cert_der;
408
409        Err(format!(
410            "OCSP fetch not yet implemented (responder: {responder_url})"
411        ))
412    }
413
414    /// Extract the OCSP responder URL from the server certificate's AIA extension.
415    ///
416    /// RFC 5280 Section 4.2.2.1: the Authority Information Access extension
417    /// contains the access method `id-ad-ocsp` (OID 1.3.6.1.5.5.7.48.1)
418    /// with a GeneralName (typically a uniformResourceIdentifier) pointing
419    /// to the OCSP responder.
420    fn extract_aia_ocsp_url(&self) -> Option<&str> {
421        // TODO: Parse AIA extension from self.server_cert_der.
422        // For now, return None so the config-level URL is required.
423        let _ = &self.server_cert_der;
424        None
425    }
426}
427
428/// Check whether a DER-encoded certificate contains the TLS Feature
429/// Extension (must-staple, OID 1.3.6.1.5.5.7.1.24).
430///
431/// RFC 7633 Section 4: if this extension is present with value
432/// `status_request(5)`, the TLS server MUST provide a stapled OCSP
433/// response during every handshake.
434pub fn has_must_staple_extension(cert_der: &[u8]) -> bool {
435    // The OID 1.3.6.1.5.5.7.1.24 encodes to:
436    //   06 08 2b 06 01 05 05 07 01 18
437    const MUST_STAPLE_OID_DER: &[u8] =
438        &[0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18];
439
440    // Simple byte-pattern search.  A full implementation would parse the
441    // X.509 extensions properly via an ASN.1 library.
442    cert_der
443        .windows(MUST_STAPLE_OID_DER.len())
444        .any(|window| window == MUST_STAPLE_OID_DER)
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn protocol_versions_1_2_to_1_3() {
453        let versions = protocol_versions("1.2", "1.3").unwrap();
454        assert_eq!(versions.len(), 2);
455    }
456
457    #[test]
458    fn protocol_versions_1_3_only() {
459        let versions = protocol_versions("1.3", "1.3").unwrap();
460        assert_eq!(versions.len(), 1);
461    }
462
463    #[test]
464    fn protocol_versions_invalid() {
465        assert!(protocol_versions("1.0", "1.2").is_err());
466        assert!(protocol_versions("1.3", "1.2").is_err());
467    }
468
469    #[test]
470    fn channel_binding_is_sha256() {
471        let cert_der = b"fake certificate DER bytes";
472        let binding = compute_channel_binding(cert_der);
473        assert_eq!(binding.len(), 32); // SHA-256 output is 32 bytes
474    }
475
476    #[test]
477    fn must_staple_detection_positive() {
478        // Construct a byte sequence containing the TLS Feature OID.
479        let mut cert = vec![0x30, 0x20]; // SEQUENCE header (placeholder)
480        // ... some bytes ...
481        cert.extend_from_slice(&[0x00, 0x00]);
482        // The OID for id-pe-tlsfeature: 1.3.6.1.5.5.7.1.24
483        cert.extend_from_slice(&[0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18]);
484        // The value: SEQUENCE { INTEGER 5 }
485        cert.extend_from_slice(&[0x30, 0x03, 0x02, 0x01, 0x05]);
486        cert.extend_from_slice(&[0x00, 0x00]);
487
488        assert!(has_must_staple_extension(&cert));
489    }
490
491    #[test]
492    fn must_staple_detection_negative() {
493        // A certificate without the TLS Feature OID.
494        let cert = b"some certificate bytes without the must-staple OID";
495        assert!(!has_must_staple_extension(cert));
496    }
497
498    #[test]
499    fn ocsp_stapling_config_defaults() {
500        let config = crate::config::OcspStaplingConfig::default();
501        assert!(!config.enabled);
502        assert!(config.responder_url.is_none());
503        assert_eq!(config.refresh_interval_secs, 14400);
504        assert!(config.soft_fail);
505    }
506}