Skip to main content

kipuka_util/
tls.rs

1//! TLS configuration with NIAP CA PP and FIPS compliance.
2//!
3//! Enforces:
4//! - TLS 1.2+ only (no SSLv3, TLS 1.0, TLS 1.1) per NIAP CA PP
5//! - FIPS-approved cipher suites only per NIAP CA PP FCS_TLSC_EXT.1
6//! - mTLS client certificate verification for EST enrollment
7//! - PKCS#11 URI detection for HSM-backed private keys
8
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use rustls::crypto::ring as ring_provider;
13use rustls::pki_types::{CertificateDer, PrivateKeyDer};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tracing::{debug, info, warn};
17
18/// Errors during TLS configuration.
19#[derive(Debug, Error)]
20pub enum TlsError {
21    /// Failed to read a PEM file.
22    #[error("failed to read PEM file {path}: {source}")]
23    PemRead {
24        path: String,
25        source: std::io::Error,
26    },
27
28    /// No certificates found in the PEM file.
29    #[error("no certificates found in {path}")]
30    NoCertificates { path: String },
31
32    /// No private key found in the PEM file.
33    #[error("no private key found in {path}")]
34    NoPrivateKey { path: String },
35
36    /// The private key references a PKCS#11 URI (needs HSM integration).
37    #[error("PKCS#11 URI detected: {uri} (use kipuka-hsm crate)")]
38    Pkcs11Uri { uri: String },
39
40    /// rustls configuration error.
41    #[error("TLS configuration error: {0}")]
42    Config(String),
43
44    /// I/O error.
45    #[error("I/O error: {0}")]
46    Io(#[from] std::io::Error),
47}
48
49/// Serializable TLS configuration from the config file.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TlsConfig {
52    /// Path to the server certificate chain (PEM).
53    pub cert_chain_path: PathBuf,
54    /// Path to the server private key (PEM, PKCS#8, or PKCS#11 URI).
55    pub private_key_path: PathBuf,
56    /// Optional path to trusted CA certificates for client verification (mTLS).
57    pub client_ca_path: Option<PathBuf>,
58    /// Whether to require client certificates (mTLS).
59    pub require_client_cert: bool,
60    /// Minimum TLS version (default: TLS 1.2, per NIAP CA PP).
61    pub min_version: Option<String>,
62}
63
64/// Builder for constructing a `rustls::ServerConfig`.
65///
66/// Enforces NIAP CA PP requirements:
67/// - FCS_TLSS_EXT.1: TLS 1.2 minimum, no deprecated protocols
68/// - FCS_TLSC_EXT.1: FIPS-approved cipher suites only
69/// - FCS_COP.1: Approved cryptographic operations
70pub struct TlsConfigBuilder {
71    cert_chain: Vec<CertificateDer<'static>>,
72    private_key: Option<PrivateKeyDer<'static>>,
73    client_verifier: Option<Arc<dyn rustls::server::danger::ClientCertVerifier>>,
74}
75
76impl TlsConfigBuilder {
77    /// Start building a TLS configuration.
78    pub fn new() -> Self {
79        Self {
80            cert_chain: Vec::new(),
81            private_key: None,
82            client_verifier: None,
83        }
84    }
85
86    /// Load the server certificate chain from a PEM file.
87    pub fn with_cert_chain(mut self, path: &Path) -> Result<Self, TlsError> {
88        let pem_data = std::fs::read(path).map_err(|e| TlsError::PemRead {
89            path: path.display().to_string(),
90            source: e,
91        })?;
92
93        let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &pem_data[..])
94            .filter_map(|r| r.ok())
95            .collect();
96
97        if certs.is_empty() {
98            return Err(TlsError::NoCertificates {
99                path: path.display().to_string(),
100            });
101        }
102
103        info!(
104            path = %path.display(),
105            count = certs.len(),
106            "loaded certificate chain"
107        );
108
109        self.cert_chain = certs;
110        Ok(self)
111    }
112
113    /// Load the server private key from a PEM or PKCS#8 file.
114    ///
115    /// If the file content starts with `pkcs11:`, returns an error
116    /// indicating that the HSM crate should be used instead.
117    pub fn with_private_key(mut self, path: &Path) -> Result<Self, TlsError> {
118        let pem_data = std::fs::read(path).map_err(|e| TlsError::PemRead {
119            path: path.display().to_string(),
120            source: e,
121        })?;
122
123        // Detect PKCS#11 URI (key managed by HSM).
124        if let Ok(text) = std::str::from_utf8(&pem_data) {
125            let trimmed = text.trim();
126            if trimmed.starts_with("pkcs11:") {
127                return Err(TlsError::Pkcs11Uri {
128                    uri: trimmed.to_owned(),
129                });
130            }
131        }
132
133        let key = rustls_pemfile::private_key(&mut &pem_data[..])
134            .map_err(|e| TlsError::Config(format!("key parse error: {e}")))?
135            .ok_or_else(|| TlsError::NoPrivateKey {
136                path: path.display().to_string(),
137            })?;
138
139        debug!(path = %path.display(), "loaded private key");
140
141        self.private_key = Some(key);
142        Ok(self)
143    }
144
145    /// Set up client certificate verification for mTLS.
146    pub fn with_client_auth(mut self, ca_path: &Path, required: bool) -> Result<Self, TlsError> {
147        let pem_data = std::fs::read(ca_path).map_err(|e| TlsError::PemRead {
148            path: ca_path.display().to_string(),
149            source: e,
150        })?;
151
152        let mut root_store = rustls::RootCertStore::empty();
153        let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &pem_data[..])
154            .filter_map(|r| r.ok())
155            .collect();
156
157        if ca_certs.is_empty() {
158            return Err(TlsError::NoCertificates {
159                path: ca_path.display().to_string(),
160            });
161        }
162
163        for cert in &ca_certs {
164            root_store.add(cert.clone()).map_err(|e| {
165                TlsError::Config(format!("failed to add CA cert to trust store: {e}"))
166            })?;
167        }
168
169        let verifier = if required {
170            rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
171                .build()
172                .map_err(|e| TlsError::Config(format!("client verifier build error: {e}")))?
173        } else {
174            rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
175                .allow_unauthenticated()
176                .build()
177                .map_err(|e| TlsError::Config(format!("client verifier build error: {e}")))?
178        };
179
180        info!(
181            ca_path = %ca_path.display(),
182            ca_count = ca_certs.len(),
183            required,
184            "configured client certificate verification"
185        );
186
187        self.client_verifier = Some(verifier);
188        Ok(self)
189    }
190
191    /// Build the `rustls::ServerConfig`.
192    ///
193    /// Enforces NIAP CA PP requirements:
194    /// - TLS 1.2+ only (FCS_TLSS_EXT.1)
195    /// - FIPS-approved cipher suites (FCS_TLSC_EXT.1)
196    pub fn build(self) -> Result<rustls::ServerConfig, TlsError> {
197        let key = self
198            .private_key
199            .ok_or_else(|| TlsError::Config("no private key loaded".into()))?;
200
201        if self.cert_chain.is_empty() {
202            return Err(TlsError::Config("no certificate chain loaded".into()));
203        }
204
205        let provider = Arc::new(ring_provider::default_provider());
206
207        let mut config = if let Some(verifier) = self.client_verifier {
208            rustls::ServerConfig::builder_with_provider(provider)
209                .with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
210                .map_err(|e| TlsError::Config(format!("protocol version error: {e}")))?
211                .with_client_cert_verifier(verifier)
212                .with_single_cert(self.cert_chain, key)
213                .map_err(|e| TlsError::Config(format!("server config error: {e}")))?
214        } else {
215            rustls::ServerConfig::builder_with_provider(provider)
216                .with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
217                .map_err(|e| TlsError::Config(format!("protocol version error: {e}")))?
218                .with_no_client_auth()
219                .with_single_cert(self.cert_chain, key)
220                .map_err(|e| TlsError::Config(format!("server config error: {e}")))?
221        };
222
223        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
224
225        info!("TLS server config built (TLS 1.2+, NIAP CA PP compliant)");
226
227        Ok(config)
228    }
229
230    /// Build from a [`TlsConfig`] (convenience wrapper).
231    pub fn from_config(config: &TlsConfig) -> Result<rustls::ServerConfig, TlsError> {
232        let mut builder = Self::new()
233            .with_cert_chain(&config.cert_chain_path)?
234            .with_private_key(&config.private_key_path)?;
235
236        if let Some(ref ca_path) = config.client_ca_path {
237            builder = builder.with_client_auth(ca_path, config.require_client_cert)?;
238        } else if config.require_client_cert {
239            warn!("require_client_cert is true but no client_ca_path configured");
240        }
241
242        builder.build()
243    }
244}
245
246impl Default for TlsConfigBuilder {
247    fn default() -> Self {
248        Self::new()
249    }
250}