rusmes_config/tls.rs
1//! Per-protocol TLS certificate configuration for RusMES.
2//!
3//! The `[tls]` TOML section allows a shared default TLS certificate/key pair
4//! with optional per-protocol overrides for SMTP, IMAP, POP3, and JMAP.
5//!
6//! ## Backward compatibility
7//!
8//! Existing configurations that specify a top-level `[tls]` section with
9//! `cert_path` / `key_path` directly are supported via `#[serde(alias)]`.
10//!
11//! ## Example
12//!
13//! ```toml
14//! # Shared default
15//! [tls.default]
16//! cert_path = "/etc/rusmes/tls/cert.pem"
17//! key_path = "/etc/rusmes/tls/key.pem"
18//!
19//! # IMAP uses its own certificate
20//! [tls.imap]
21//! cert_path = "/etc/rusmes/tls/imap-cert.pem"
22//! key_path = "/etc/rusmes/tls/imap-key.pem"
23//!
24//! # SMTP with mutual TLS (require client certificate)
25//! [tls.smtp]
26//! cert_path = "/etc/rusmes/tls/smtp-cert.pem"
27//! key_path = "/etc/rusmes/tls/smtp-key.pem"
28//! client_auth = "required"
29//! client_ca_path = "/etc/rusmes/tls/client-ca.pem"
30//! ```
31
32use serde::{Deserialize, Serialize};
33use std::path::PathBuf;
34
35/// Client certificate authentication mode for mutual TLS.
36///
37/// Controls whether the server requests and/or requires a client certificate
38/// during the TLS handshake. Default is [`ClientAuthMode::Disabled`] (no
39/// client certificate requested).
40///
41/// ## TOML spelling
42///
43/// ```toml
44/// client_auth = "disabled" # default — no client certificate requested
45/// client_auth = "optional" # certificate is requested but not required
46/// client_auth = "required" # handshake fails if no valid certificate is presented
47/// ```
48#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
49#[serde(rename_all = "lowercase")]
50pub enum ClientAuthMode {
51 /// No client certificate is requested. This is the default and matches
52 /// the previous behaviour (i.e. backward-compatible).
53 #[default]
54 Disabled,
55 /// Client certificate is requested but the handshake succeeds even when
56 /// the client does not present one. The certificate chain, when present,
57 /// is still verified against `client_ca_path`.
58 Optional,
59 /// Client certificate is mandatory. The TLS handshake is aborted if the
60 /// client does not present a certificate signed by the configured CA.
61 Required,
62}
63
64/// Which protocol is requesting a TLS endpoint configuration.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub enum ProtocolKind {
67 /// SMTP (port 25 / 587 / 465)
68 Smtp,
69 /// IMAP4rev1 (port 143 / 993)
70 Imap,
71 /// POP3 (port 110 / 995)
72 Pop3,
73 /// JMAP over HTTP/TLS
74 Jmap,
75}
76
77/// A single TLS endpoint: certificate chain and private key paths.
78///
79/// Both fields must be present for the endpoint to be valid.
80///
81/// ## Mutual TLS
82///
83/// Set `client_auth` to `"optional"` or `"required"` and supply a
84/// `client_ca_path` pointing to a PEM file that contains the CA certificate
85/// (or chain) used to sign client certificates. When `client_auth` is
86/// `"disabled"` (the default) both `client_auth` and `client_ca_path` are
87/// ignored.
88#[derive(Debug, Clone, Deserialize, Serialize)]
89pub struct TlsEndpointConfig {
90 /// Default: none. Path to the PEM-encoded certificate chain file.
91 /// Use `#[serde(alias = "cert_path")]` to accept the legacy spelling.
92 #[serde(alias = "cert_path")]
93 pub cert_path: PathBuf,
94
95 /// Default: none. Path to the PEM-encoded private key file.
96 /// Use `#[serde(alias = "key_path")]` to accept the legacy spelling.
97 #[serde(alias = "key_path")]
98 pub key_path: PathBuf,
99
100 /// Default: [`ClientAuthMode::Disabled`]. Controls whether a client
101 /// certificate is requested / required during the TLS handshake.
102 #[serde(default)]
103 pub client_auth: ClientAuthMode,
104
105 /// Default: `None`. Path to a PEM-encoded CA certificate file used to
106 /// verify the client certificate chain. Required when `client_auth` is
107 /// `"optional"` or `"required"`.
108 #[serde(default)]
109 pub client_ca_path: Option<PathBuf>,
110}
111
112impl TlsEndpointConfig {
113 /// Validate that both paths are non-empty strings.
114 ///
115 /// Existence on disk is not checked here — that is deferred to server
116 /// startup so that config validation can succeed in CI without real certs.
117 ///
118 /// Also validates that `client_ca_path` is set when `client_auth` is not
119 /// `Disabled`.
120 pub fn validate(&self) -> anyhow::Result<()> {
121 if self.cert_path.as_os_str().is_empty() {
122 anyhow::bail!("TLS cert_path cannot be empty");
123 }
124 if self.key_path.as_os_str().is_empty() {
125 anyhow::bail!("TLS key_path cannot be empty");
126 }
127 if self.client_auth != ClientAuthMode::Disabled && self.client_ca_path.is_none() {
128 anyhow::bail!(
129 "TLS client_ca_path must be set when client_auth is '{}' (not 'disabled')",
130 match self.client_auth {
131 ClientAuthMode::Optional => "optional",
132 ClientAuthMode::Required => "required",
133 ClientAuthMode::Disabled => unreachable!(),
134 }
135 );
136 }
137 Ok(())
138 }
139}
140
141/// Top-level TLS configuration block (`[tls]` in TOML).
142///
143/// `default` is the fallback used by any protocol that does not have a
144/// dedicated override. Per-protocol overrides take precedence when present.
145#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct TlsConfig {
147 /// Default certificate/key pair used as a fallback for all protocols.
148 pub default: TlsEndpointConfig,
149
150 /// Default: `None`. SMTP-specific TLS certificate override.
151 #[serde(default)]
152 pub smtp: Option<TlsEndpointConfig>,
153
154 /// Default: `None`. IMAP-specific TLS certificate override.
155 #[serde(default)]
156 pub imap: Option<TlsEndpointConfig>,
157
158 /// Default: `None`. POP3-specific TLS certificate override.
159 #[serde(default)]
160 pub pop3: Option<TlsEndpointConfig>,
161
162 /// Default: `None`. JMAP-specific TLS certificate override.
163 #[serde(default)]
164 pub jmap: Option<TlsEndpointConfig>,
165}
166
167impl TlsConfig {
168 /// Return the [`TlsEndpointConfig`] that should be used for `proto`.
169 ///
170 /// Returns the per-protocol override when present, otherwise the
171 /// `default` endpoint.
172 pub fn tls_for_protocol(&self, proto: ProtocolKind) -> &TlsEndpointConfig {
173 let override_cfg = match proto {
174 ProtocolKind::Smtp => self.smtp.as_ref(),
175 ProtocolKind::Imap => self.imap.as_ref(),
176 ProtocolKind::Pop3 => self.pop3.as_ref(),
177 ProtocolKind::Jmap => self.jmap.as_ref(),
178 };
179 override_cfg.unwrap_or(&self.default)
180 }
181
182 /// Validate all configured TLS endpoints.
183 pub fn validate(&self) -> anyhow::Result<()> {
184 self.default
185 .validate()
186 .map_err(|e| anyhow::anyhow!("tls.default: {}", e))?;
187 if let Some(ref s) = self.smtp {
188 s.validate()
189 .map_err(|e| anyhow::anyhow!("tls.smtp: {}", e))?;
190 }
191 if let Some(ref i) = self.imap {
192 i.validate()
193 .map_err(|e| anyhow::anyhow!("tls.imap: {}", e))?;
194 }
195 if let Some(ref p) = self.pop3 {
196 p.validate()
197 .map_err(|e| anyhow::anyhow!("tls.pop3: {}", e))?;
198 }
199 if let Some(ref j) = self.jmap {
200 j.validate()
201 .map_err(|e| anyhow::anyhow!("tls.jmap: {}", e))?;
202 }
203 Ok(())
204 }
205}