kipuka/config/tls.rs
1//! TLS configuration for EST and admin listeners.
2//!
3//! EST requires TLS 1.2+ (RFC 7030 §3.3.1). The NIAP CA PP FTP_TRP.1
4//! further constrains the cipher suite selection to AEAD-only suites
5//! with forward secrecy (ECDHE or DHE key exchange).
6//!
7//! Two separate truststores are supported (RHELBU-3536 R18):
8//!
9//! - **EST truststore** (`ca_file`) — validates EST client certificates
10//! for `/simpleenroll`, `/simplereenroll`, and `/serverkeygen`.
11//! - **Admin truststore** — configured in `[admin]` — validates admin
12//! operator mTLS certificates independently.
13
14use serde::Deserialize;
15
16/// Client certificate authentication mode.
17///
18/// RFC 7030 §3.3.2: EST servers SHOULD support certificate-based client
19/// authentication. The mode determines whether the TLS handshake
20/// requests and/or requires a client certificate.
21#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23#[derive(Default)]
24pub enum ClientAuthMode {
25 /// TLS handshake requires a valid client certificate.
26 Required,
27 /// TLS handshake requests but does not require a client certificate.
28 /// Authentication falls through to HTTP-layer methods (OTP, etc.).
29 #[default]
30 Optional,
31 /// No client certificate is requested.
32 None,
33}
34
35/// `[tls]` section — TLS configuration for the EST listener.
36///
37/// # NIAP CA PP FTP_TRP.1 compliance
38///
39/// - TLS 1.2 is the minimum supported version; TLS 1.0 and 1.1 are rejected.
40/// - Only AEAD cipher suites with forward secrecy (ECDHE/DHE key exchange)
41/// are permitted.
42/// - The default cipher suite list excludes CBC-mode suites and static
43/// RSA key exchange.
44#[derive(Debug, Clone, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct TlsConfig {
47 /// Enable TLS on the EST listener. Default: `false`.
48 ///
49 /// When `false`, the server listens in plain HTTP mode (intended only
50 /// for development behind a TLS-terminating reverse proxy).
51 #[serde(default)]
52 pub enabled: bool,
53
54 /// Path to the server certificate chain in PEM format.
55 ///
56 /// The file MUST contain the server's end-entity certificate first,
57 /// followed by any intermediate CA certificates.
58 #[serde(default)]
59 pub cert_file: String,
60
61 /// Path to the server private key in PEM format.
62 #[serde(default)]
63 pub key_file: String,
64
65 /// Client certificate authentication mode.
66 ///
67 /// - `required` — mTLS is mandatory; unauthenticated clients are rejected
68 /// at the TLS layer.
69 /// - `optional` (default) — the server requests a client certificate but
70 /// accepts connections without one. EST enrollment can fall back to
71 /// HTTP-layer authentication (OTP, HTTP Basic, etc.).
72 /// - `none` — no client certificate is requested.
73 #[serde(default)]
74 pub client_auth: ClientAuthMode,
75
76 /// Path to the CA certificate bundle (PEM) for validating EST client
77 /// certificates.
78 ///
79 /// RHELBU-3536 R18: this truststore is dedicated to the EST listener.
80 /// Admin operator mTLS uses a separate truststore configured in `[admin]`.
81 #[serde(default)]
82 pub ca_file: String,
83
84 /// Minimum TLS protocol version.
85 ///
86 /// NIAP CA PP FTP_TRP.1: must be `"1.2"` or `"1.3"`.
87 /// Default: `"1.2"`.
88 #[serde(default = "default_min_protocol")]
89 pub min_protocol: String,
90
91 /// Maximum TLS protocol version.
92 ///
93 /// Default: `"1.3"`.
94 #[serde(default = "default_max_protocol")]
95 pub max_protocol: String,
96
97 /// Allowed cipher suites (IANA names).
98 ///
99 /// When empty, the server uses the rustls default selection which
100 /// already satisfies FTP_TRP.1 (AEAD + forward secrecy only).
101 ///
102 /// Example:
103 /// ```toml
104 /// ciphersuites = [
105 /// "TLS_AES_256_GCM_SHA384",
106 /// "TLS_AES_128_GCM_SHA256",
107 /// "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
108 /// "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
109 /// ]
110 /// ```
111 #[serde(default)]
112 pub ciphersuites: Vec<String>,
113
114 /// OCSP stapling configuration (RFC 7633 / RFC 6066 Section 8).
115 ///
116 /// When the server's TLS certificate contains the TLS Feature Extension
117 /// (must-staple, OID 1.3.6.1.5.5.7.1.24), OCSP stapling MUST be
118 /// enabled to satisfy RFC 7633 Section 4 requirements. Clients that
119 /// understand must-staple will abort the handshake if no stapled OCSP
120 /// response is provided.
121 ///
122 /// Even without must-staple, enabling OCSP stapling improves TLS
123 /// handshake performance by eliminating the client-side OCSP lookup.
124 #[serde(default)]
125 pub ocsp_stapling: OcspStaplingConfig,
126}
127
128/// OCSP stapling configuration for the TLS listener.
129///
130/// RFC 6066 Section 8: the `status_request` TLS extension allows the
131/// server to provide a stapled OCSP response during the TLS handshake,
132/// eliminating the client's need to contact the OCSP responder directly.
133///
134/// RFC 7633 Section 4: when the server certificate contains the TLS
135/// Feature Extension (must-staple), the server MUST provide a stapled
136/// response; failure to do so causes compliant clients to abort.
137#[derive(Debug, Clone, Deserialize)]
138#[serde(deny_unknown_fields)]
139pub struct OcspStaplingConfig {
140 /// Enable OCSP stapling.
141 ///
142 /// When `true`, the server fetches an OCSP response for its own
143 /// certificate at startup and refreshes it periodically.
144 ///
145 /// Default: `false`.
146 #[serde(default)]
147 pub enabled: bool,
148
149 /// Override the OCSP responder URL.
150 ///
151 /// When `None`, the responder URL is extracted from the server
152 /// certificate's Authority Information Access (AIA) extension
153 /// (OID 1.3.6.1.5.5.7.48.1).
154 ///
155 /// Set this when the AIA URL is not reachable from the server
156 /// (e.g., behind a firewall) and a local OCSP responder proxy
157 /// is available.
158 #[serde(default)]
159 pub responder_url: Option<String>,
160
161 /// Interval in seconds between OCSP response refreshes.
162 ///
163 /// The server fetches a fresh OCSP response from the responder
164 /// at this interval, replacing the cached stapled response.
165 ///
166 /// Default: `14400` (4 hours). OCSP responses typically have a
167 /// `nextUpdate` validity of 24-48 hours, so refreshing every 4
168 /// hours provides adequate margin.
169 #[serde(default = "default_ocsp_refresh_interval")]
170 pub refresh_interval_secs: u64,
171
172 /// Allow serving TLS without a stapled OCSP response when the
173 /// OCSP responder is unreachable.
174 ///
175 /// When `true` (soft-fail mode), the server continues to accept
176 /// TLS connections without a stapled response if the OCSP
177 /// responder cannot be reached. A stale cached response is
178 /// served if still within its `nextUpdate` window. A warning
179 /// is logged on each failed refresh attempt.
180 ///
181 /// When `false` (hard-fail mode), the server refuses to start
182 /// if the initial OCSP fetch fails, and transitions to
183 /// unhealthy status if subsequent refreshes fail with no valid
184 /// cached response.
185 ///
186 /// Default: `true`.
187 #[serde(default = "default_soft_fail")]
188 pub soft_fail: bool,
189}
190
191fn default_min_protocol() -> String {
192 "1.2".to_string()
193}
194
195fn default_max_protocol() -> String {
196 "1.3".to_string()
197}
198
199fn default_ocsp_refresh_interval() -> u64 {
200 14400 // 4 hours
201}
202
203fn default_soft_fail() -> bool {
204 true
205}
206
207impl Default for OcspStaplingConfig {
208 fn default() -> Self {
209 Self {
210 enabled: false,
211 responder_url: None,
212 refresh_interval_secs: default_ocsp_refresh_interval(),
213 soft_fail: default_soft_fail(),
214 }
215 }
216}
217
218impl Default for TlsConfig {
219 fn default() -> Self {
220 Self {
221 enabled: false,
222 cert_file: String::new(),
223 key_file: String::new(),
224 client_auth: ClientAuthMode::default(),
225 ca_file: String::new(),
226 min_protocol: default_min_protocol(),
227 max_protocol: default_max_protocol(),
228 ciphersuites: Vec::new(),
229 ocsp_stapling: OcspStaplingConfig::default(),
230 }
231 }
232}
233
234impl TlsConfig {
235 /// Validate TLS configuration constraints.
236 pub fn validate(&self) -> std::result::Result<(), String> {
237 if !self.enabled {
238 return Ok(());
239 }
240
241 if self.cert_file.is_empty() {
242 return Err("[tls].cert_file must not be empty when TLS is enabled".into());
243 }
244 if self.key_file.is_empty() {
245 return Err("[tls].key_file must not be empty when TLS is enabled".into());
246 }
247
248 // NIAP CA PP FTP_TRP.1: TLS 1.2 minimum
249 match self.min_protocol.as_str() {
250 "1.2" | "1.3" => {}
251 other => {
252 return Err(format!(
253 "[tls].min_protocol must be \"1.2\" or \"1.3\", got {other:?}"
254 ));
255 }
256 }
257 match self.max_protocol.as_str() {
258 "1.2" | "1.3" => {}
259 other => {
260 return Err(format!(
261 "[tls].max_protocol must be \"1.2\" or \"1.3\", got {other:?}"
262 ));
263 }
264 }
265 if self.min_protocol == "1.3" && self.max_protocol == "1.2" {
266 return Err("[tls].min_protocol (1.3) cannot exceed max_protocol (1.2)".into());
267 }
268
269 // Client auth modes that need a CA file
270 if self.client_auth != ClientAuthMode::None && self.ca_file.is_empty() {
271 return Err(
272 "[tls].ca_file must be set when client_auth is \"required\" or \"optional\"".into(),
273 );
274 }
275
276 Ok(())
277 }
278}