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}