Skip to main content

tachyon_web/tls/
policy.rs

1//! A crypto/TLS policy shared across every listener a [`Server`](crate::server::Server) runs.
2
3use rustls::SupportedProtocolVersion;
4use rustls::crypto::CryptoProvider;
5#[cfg(all(feature = "cert-gen", any(feature = "tor", feature = "i2p")))]
6use rustls::pki_types::{CertificateDer, PrivateKeyDer};
7use std::sync::Arc;
8
9/// A crypto provider + protocol-version policy shared across every listener a
10/// [`Server`](crate::server::Server) runs.
11///
12/// Configure it **once** and it covers clearnet HTTPS (static cert or Let's Encrypt via
13/// [`AcmeManager`](crate::tls::acme::AcmeManager)), the onion `.onion` HTTPS termination
14/// ([`OnionConfig`](crate::server::tor::OnionConfig)), and the I2P eepsite's optional TLS layer
15/// ([`I2pConfig`](crate::server::i2p::I2pConfig)), instead of being reconstructed by hand for
16/// each one.
17///
18/// Pass it once via [`Server::tls_policy`](crate::server::Server::tls_policy); every listener
19/// that generates its own self-signed certificate (onion, I2P) will build it with this same
20/// provider, and clearnet's ACME/static `ServerConfig` uses it too.
21///
22/// # The Tor relay/channel layer is a separate concern
23///
24/// This policy governs TLS *termination* — the handshake a browser or I2P/Tor client
25/// completes with this process. It does **not** reach the TLS arti uses internally to connect
26/// *out* to Tor relays (the "channel" layer) — arti has no API to accept a custom
27/// `rustls::ClientConfig` for that. Instead, arti reads whatever `CryptoProvider` is installed
28/// as rustls's *process-wide* default. Call [`install_as_process_default`](Self::install_as_process_default)
29/// with this same policy before bootstrapping a [`TorClient`](arti_client::TorClient) (this is
30/// done for you by [`Server::serve_tor`](crate::server::Server::serve_tor)/
31/// [`serve_onion`](crate::server::Server::serve_onion)) so the relay layer uses the same
32/// AEAD/KEM choices as your HTTPS listeners.
33///
34/// Restricting this policy to post-quantum-only or a single cipher suite is safe for the
35/// termination side (you control both ends), but risks breaking Tor bootstrap connectivity if
36/// also installed process-wide: plenty of relays on the live network don't yet support hybrid
37/// PQ key-exchange groups on their TLS link layer. Prefer PQ, don't require it exclusively, if
38/// this same policy will also be installed as the process default.
39#[derive(Clone)]
40pub struct TlsPolicy {
41    provider: Arc<CryptoProvider>,
42    versions: Vec<&'static SupportedProtocolVersion>,
43}
44
45impl std::fmt::Debug for TlsPolicy {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("TlsPolicy")
48            .field("tls13", &self.versions.contains(&&rustls::version::TLS13))
49            .field("tls12", &self.versions.contains(&&rustls::version::TLS12))
50            .finish_non_exhaustive()
51    }
52}
53
54impl TlsPolicy {
55    /// Tachyon's curated default: hybrid post-quantum key-exchange groups preferred
56    /// (`X25519MLKEM768`, `SECP256R1MLKEM768`, `MLKEM1024`, `MLKEM768`), falling back to
57    /// classical ECDHE groups (`SECP384R1`, `X25519`, `SECP256R1`) for interoperability;
58    /// AES-256-GCM and ChaCha20-Poly1305 preferred over AES-128 for both TLS 1.3 and TLS 1.2
59    /// cipher suites. Both TLS 1.3 and 1.2 are offered — see [`tls13_only`](Self::tls13_only)
60    /// to pin more strictly.
61    #[must_use]
62    pub fn hardened() -> Self {
63        Self::with_provider(hardened_provider())
64    }
65
66    /// Builds a policy from a fully custom [`CryptoProvider`] — for example one pinned to
67    /// `TLS13_AES_256_GCM_SHA384` only, or built from `rustls::crypto::default_fips_provider()`
68    /// (with the `fips` feature) for FIPS-140-3-validated `aws-lc-rs` primitives.
69    ///
70    /// Defaults to offering both TLS 1.3 and TLS 1.2 — see [`tls13_only`](Self::tls13_only).
71    #[must_use]
72    pub fn with_provider(provider: Arc<CryptoProvider>) -> Self {
73        Self {
74            provider,
75            versions: vec![&rustls::version::TLS13, &rustls::version::TLS12],
76        }
77    }
78
79    /// Restricts this policy to TLS 1.3 only (default: TLS 1.3 and 1.2 both offered).
80    #[must_use]
81    pub fn tls13_only(mut self) -> Self {
82        self.versions = vec![&rustls::version::TLS13];
83        self
84    }
85
86    /// The underlying crypto provider.
87    #[must_use]
88    pub fn provider(&self) -> Arc<CryptoProvider> {
89        self.provider.clone()
90    }
91
92    /// The protocol versions this policy negotiates.
93    #[must_use]
94    pub fn versions(&self) -> &[&'static SupportedProtocolVersion] {
95        &self.versions
96    }
97
98    /// Installs this policy's crypto provider as rustls's process-wide default, via
99    /// [`CryptoProvider::install_default`].
100    ///
101    /// Idempotent and safe to call redundantly (e.g. once per listener sharing this policy):
102    /// rustls's global default can only be set once per process, so only the *first* call
103    /// actually installs anything — later calls (even with a different policy) are silently
104    /// ignored. Call this before bootstrapping a [`TorClient`](arti_client::TorClient) if you
105    /// want arti's relay/channel TLS connections to use this policy's provider too — see the
106    /// [type docs](Self) for why that's a separate concern from HTTPS termination.
107    pub fn install_as_process_default(&self) {
108        let _ = (*self.provider).clone().install_default();
109    }
110
111    /// Builds a `rustls::ServerConfig` from a PEM cert chain + key, using this policy's
112    /// provider and protocol versions.
113    ///
114    /// Only used by the onion/i2p self-signed-cert paths today (see `server/tor.rs` and
115    /// `server/i2p.rs`, both of which require `cert-gen` — not just `tls` — to reach the
116    /// self-signed-cert branch) — gated the same way so a plain `tls`-only build doesn't trip
117    /// `-D dead-code`.
118    #[cfg(all(feature = "cert-gen", any(feature = "tor", feature = "i2p")))]
119    pub(crate) fn server_config_from_pem(
120        &self,
121        cert: &[u8],
122        key: &[u8],
123    ) -> Result<rustls::ServerConfig, std::io::Error> {
124        use rustls_pemfile::{certs, private_key};
125
126        let mut cert_reader = std::io::BufReader::new(cert);
127        let cert_chain: Vec<CertificateDer<'static>> = certs(&mut cert_reader)
128            .filter_map(std::result::Result::ok)
129            .collect();
130
131        let mut key_reader = std::io::BufReader::new(key);
132        let key_der: PrivateKeyDer<'static> = private_key(&mut key_reader)
133            .map_err(|e| {
134                std::io::Error::new(
135                    std::io::ErrorKind::InvalidData,
136                    format!("Failed to read private key: {e}"),
137                )
138            })?
139            .ok_or_else(|| {
140                std::io::Error::new(std::io::ErrorKind::NotFound, "No private key found in PEM")
141            })?;
142
143        let mut server_config = rustls::ServerConfig::builder_with_provider(self.provider())
144            .with_protocol_versions(&self.versions)
145            .map_err(|e| {
146                std::io::Error::new(
147                    std::io::ErrorKind::InvalidData,
148                    format!("TLS version configuration failed: {e}"),
149                )
150            })?
151            .with_no_client_auth()
152            .with_single_cert(cert_chain, key_der)
153            .map_err(|e| {
154                std::io::Error::new(
155                    std::io::ErrorKind::InvalidInput,
156                    format!("Invalid certificate or key: {e}"),
157                )
158            })?;
159
160        server_config.alpn_protocols = crate::server::alpn_protocols(false);
161        Ok(server_config)
162    }
163}
164
165impl Default for TlsPolicy {
166    fn default() -> Self {
167        Self::hardened()
168    }
169}
170
171/// Tachyon's curated default `CryptoProvider`: hybrid post-quantum key-exchange groups
172/// preferred, AES-256-GCM/ChaCha20-Poly1305 preferred over AES-128, computed once and shared.
173fn hardened_provider() -> Arc<CryptoProvider> {
174    static DEFAULT_PROVIDER: std::sync::OnceLock<Arc<CryptoProvider>> = std::sync::OnceLock::new();
175    DEFAULT_PROVIDER
176        .get_or_init(|| {
177            let kx_groups = vec![
178                rustls::crypto::aws_lc_rs::kx_group::X25519MLKEM768,
179                rustls::crypto::aws_lc_rs::kx_group::SECP256R1MLKEM768,
180                rustls::crypto::aws_lc_rs::kx_group::MLKEM1024,
181                rustls::crypto::aws_lc_rs::kx_group::MLKEM768,
182                rustls::crypto::aws_lc_rs::kx_group::SECP384R1,
183                rustls::crypto::aws_lc_rs::kx_group::X25519,
184                rustls::crypto::aws_lc_rs::kx_group::SECP256R1,
185            ];
186
187            // Both TLS 1.3 and 1.2 suites are always offered — call `TlsPolicy::tls13_only()`
188            // or pass a fully custom provider (`TlsPolicy::with_provider`) for a narrower set.
189            let cipher_suites = vec![
190                // TLS 1.3
191                rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384,
192                rustls::crypto::aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
193                rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256,
194                // TLS 1.2
195                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
196                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
197                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
198                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
199                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
200                rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
201            ];
202
203            Arc::new(CryptoProvider {
204                cipher_suites,
205                kx_groups,
206                ..rustls::crypto::aws_lc_rs::default_provider()
207            })
208        })
209        .clone()
210}
211
212#[cfg(test)]
213#[allow(clippy::expect_used)]
214mod tests {
215    use super::TlsPolicy;
216
217    #[test]
218    fn hardened_offers_both_tls_versions_by_default() {
219        let policy = TlsPolicy::hardened();
220        assert_eq!(policy.versions().len(), 2);
221    }
222
223    #[test]
224    fn tls13_only_restricts_to_a_single_version() {
225        let policy = TlsPolicy::hardened().tls13_only();
226        assert_eq!(policy.versions(), &[&rustls::version::TLS13]);
227    }
228
229    #[test]
230    fn default_matches_hardened() {
231        let default_versions = TlsPolicy::default().versions().len();
232        let hardened_versions = TlsPolicy::hardened().versions().len();
233        assert_eq!(default_versions, hardened_versions);
234    }
235
236    #[test]
237    fn debug_format_reports_negotiated_versions() {
238        let both = format!("{:?}", TlsPolicy::hardened());
239        assert!(both.contains("tls13: true"));
240        assert!(both.contains("tls12: true"));
241
242        let tls13_only = format!("{:?}", TlsPolicy::hardened().tls13_only());
243        assert!(tls13_only.contains("tls13: true"));
244        assert!(tls13_only.contains("tls12: false"));
245    }
246
247    /// Idempotent by design (rustls's process-wide default can only be installed once) — this
248    /// just proves calling it repeatedly, including after another policy already raced to
249    /// install first, never panics.
250    #[test]
251    fn install_as_process_default_is_idempotent() {
252        TlsPolicy::hardened().install_as_process_default();
253        TlsPolicy::hardened()
254            .tls13_only()
255            .install_as_process_default();
256    }
257
258    #[cfg(all(feature = "cert-gen", any(feature = "tor", feature = "i2p")))]
259    #[test]
260    fn server_config_from_pem_builds_a_working_config_from_a_self_signed_cert() {
261        let cert = crate::tls::generate_self_signed_cert(vec!["localhost".to_string()])
262            .expect("generate self-signed cert");
263
264        let config = TlsPolicy::hardened()
265            .server_config_from_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())
266            .expect("build server config from valid PEM");
267
268        assert_eq!(config.alpn_protocols, crate::server::alpn_protocols(false));
269    }
270
271    #[cfg(all(feature = "cert-gen", any(feature = "tor", feature = "i2p")))]
272    #[test]
273    fn server_config_from_pem_rejects_garbage_input() {
274        let err = TlsPolicy::hardened()
275            .server_config_from_pem(b"not a certificate", b"not a key")
276            .expect_err("garbage PEM must not build a config");
277        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
278    }
279
280    #[cfg(all(feature = "cert-gen", any(feature = "tor", feature = "i2p")))]
281    #[test]
282    fn server_config_from_pem_rejects_a_key_that_does_not_match_the_cert() {
283        let cert_a = crate::tls::generate_self_signed_cert(vec!["a.example".to_string()])
284            .expect("generate cert a");
285        let cert_b = crate::tls::generate_self_signed_cert(vec!["b.example".to_string()])
286            .expect("generate cert b");
287
288        let err = TlsPolicy::hardened()
289            .server_config_from_pem(cert_a.cert_pem.as_bytes(), cert_b.key_pem.as_bytes())
290            .expect_err("mismatched cert/key pair must not build a config");
291        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
292    }
293}