Skip to main content

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}