Skip to main content

alopex_server/
tls.rs

1use std::fs::File;
2use std::io::BufReader;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use rustls::pki_types::{CertificateDer, PrivateKeyDer};
7use serde::Deserialize;
8
9use crate::error::{Result, ServerError};
10
11/// TLS configuration.
12#[derive(Clone, Debug, Deserialize)]
13pub struct TlsConfig {
14    pub cert_path: PathBuf,
15    pub key_path: PathBuf,
16    pub ca_path: Option<PathBuf>,
17    #[serde(default)]
18    pub min_version: TlsVersion,
19}
20
21#[derive(Clone, Copy, Debug, Default, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum TlsVersion {
24    #[default]
25    Tls12,
26    Tls13,
27}
28
29/// Build a rustls server config from TLS settings.
30///
31/// Uses the `ring` [`rustls::crypto::CryptoProvider`] explicitly (matching the
32/// provider already selected process-wide via `object_store`'s TLS stack), so
33/// this does not depend on `CryptoProvider::install_default()` having been
34/// called elsewhere.
35///
36/// Known constraint: `ring` (unlike `aws-lc-rs`) does not implement
37/// ECDSA P-521 (secp521r1) signature verification/signing, so certificates
38/// using that curve are not supported here. This is an accepted trade-off:
39/// alopex has no requirement to support P-521 certificates, and P-256/P-384
40/// ECDSA plus RSA remain fully supported. Switching to `aws-lc-rs` would lift
41/// this constraint but was rejected to avoid a mixed-provider setup and the
42/// extra `cmake`/`nasm` build-time requirements `aws-lc-rs` introduces (see
43/// the rustls 0.23 migration notes in the issue #41 rustls upgrade commit).
44pub fn build_rustls_config(config: &TlsConfig) -> Result<Arc<rustls::ServerConfig>> {
45    let certs = load_certs(&config.cert_path)?;
46    let key = load_key(&config.key_path)?;
47    let versions: Vec<&'static rustls::SupportedProtocolVersion> = match config.min_version {
48        TlsVersion::Tls12 => vec![&rustls::version::TLS13, &rustls::version::TLS12],
49        TlsVersion::Tls13 => vec![&rustls::version::TLS13],
50    };
51    let provider = Arc::new(rustls::crypto::ring::default_provider());
52    let builder = rustls::ServerConfig::builder_with_provider(provider.clone())
53        .with_protocol_versions(&versions)
54        .map_err(|err| ServerError::InvalidConfig(err.to_string()))?;
55    let mut server_config = if let Some(ca_path) = &config.ca_path {
56        let mut roots = rustls::RootCertStore::empty();
57        let ca_certs = load_certs(ca_path)?;
58        for cert in ca_certs {
59            roots
60                .add(cert)
61                .map_err(|_| ServerError::InvalidConfig("invalid CA certificate".into()))?;
62        }
63        let verifier =
64            rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
65                .build()
66                .map_err(|err| ServerError::InvalidConfig(err.to_string()))?;
67        builder
68            .with_client_cert_verifier(verifier)
69            .with_single_cert(certs, key)
70            .map_err(|err| ServerError::InvalidConfig(err.to_string()))?
71    } else {
72        builder
73            .with_no_client_auth()
74            .with_single_cert(certs, key)
75            .map_err(|err| ServerError::InvalidConfig(err.to_string()))?
76    };
77    server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
78    Ok(Arc::new(server_config))
79}
80
81fn load_certs(path: &PathBuf) -> Result<Vec<CertificateDer<'static>>> {
82    let file = File::open(path).map_err(ServerError::Io)?;
83    let mut reader = BufReader::new(file);
84    rustls_pemfile::certs(&mut reader)
85        .collect::<std::result::Result<Vec<_>, _>>()
86        .map_err(|_| ServerError::InvalidConfig("invalid certificate file".into()))
87}
88
89fn load_key(path: &PathBuf) -> Result<PrivateKeyDer<'static>> {
90    let file = File::open(path).map_err(ServerError::Io)?;
91    let mut reader = BufReader::new(file);
92    let keys: Vec<_> = rustls_pemfile::pkcs8_private_keys(&mut reader)
93        .collect::<std::result::Result<Vec<_>, _>>()
94        .map_err(|_| ServerError::InvalidConfig("invalid private key file".into()))?;
95    if let Some(key) = keys.into_iter().next() {
96        return Ok(PrivateKeyDer::Pkcs8(key));
97    }
98
99    let file = File::open(path).map_err(ServerError::Io)?;
100    let mut reader = BufReader::new(file);
101    let keys: Vec<_> = rustls_pemfile::rsa_private_keys(&mut reader)
102        .collect::<std::result::Result<Vec<_>, _>>()
103        .map_err(|_| ServerError::InvalidConfig("invalid private key file".into()))?;
104    keys.into_iter()
105        .next()
106        .map(PrivateKeyDer::Pkcs1)
107        .ok_or_else(|| ServerError::InvalidConfig("private key not found".into()))
108}