Skip to content

Commit 17989e1

Browse files
committed
restrict server to TLS 1.3
also restricts, for the server, the file size of certs to max 1MiB
1 parent 8a26a74 commit 17989e1

File tree

3 files changed

+152
-12
lines changed

3 files changed

+152
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/standalone/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ axum-server = { version = "0.7.2", features = ["tls-rustls"] }
3737

3838
ring = "0.17" # explicitly add this for the CryptoProvider, even tho it's included transitively.
3939
rustls= { version="0.23", default-features = true, features=[ "ring" ] }
40+
rustls-pemfile = "2.2"
4041

4142
clap = { workspace = true, features = ["derive", "string"] }
4243
dirs.workspace = true

crates/standalone/src/subcommands/start.rs

Lines changed: 150 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
use std::sync::Arc;
2+
use std::path::Path;
3+
use std::path::PathBuf;
4+
use tokio::io::AsyncReadExt;
5+
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
6+
use rustls::pki_types::PrivatePkcs8KeyDer;
27

38
use crate::StandaloneEnv;
49
use anyhow::Context;
@@ -74,11 +79,127 @@ pub fn cli() -> clap::Command {
7479
)
7580
.arg(Arg::new("key").long("key").requires("ssl").value_name("FILE")
7681
.action(clap::ArgAction::Set)
77-
.value_parser(clap::value_parser!(std::path::PathBuf))
82+
.value_parser(clap::value_parser!(PathBuf))
7883
.help("--key server.key: The server keeps this private to decrypt and sign responses. ie. the server's private key"))
7984
// .after_help("Run `spacetime help start` for more detailed information.")
8085
}
8186

87+
/// Asynchronously reads a file with a maximum size limit of 1 MiB.
88+
async fn read_file_limited(path: &Path) -> anyhow::Result<Vec<u8>> {
89+
const MAX_SIZE: usize = 1_048_576; // 1 MiB
90+
91+
let file = tokio::fs::File::open(path)
92+
.await
93+
.map_err(|e| anyhow::anyhow!("Failed to open file {}: {}", path.display(), e))?;
94+
let metadata = file
95+
.metadata()
96+
.await
97+
.map_err(|e| anyhow::anyhow!("Failed to read metadata for {}: {}", path.display(), e))?;
98+
99+
if metadata.len() > MAX_SIZE as u64 {
100+
return Err(anyhow::anyhow!(
101+
"File {} exceeds maximum size of {} bytes",
102+
path.display(),
103+
MAX_SIZE
104+
));
105+
}
106+
107+
let mut reader = tokio::io::BufReader::new(file);
108+
let mut data = Vec::with_capacity(metadata.len() as usize);
109+
reader
110+
.read_to_end(&mut data)
111+
.await
112+
.map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", path.display(), e))?;
113+
114+
Ok(data)
115+
}
116+
117+
/// Loads certificates from a PEM file.
118+
async fn load_certs(file_path: &Path) -> anyhow::Result<Vec<CertificateDer<'static>>> {
119+
let data = read_file_limited(file_path).await?;
120+
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut std::io::Cursor::new(data))
121+
.collect::<Result<Vec<_>, _>>()
122+
.map_err(|e| anyhow::anyhow!("Failed to parse certificates from {}: {:?}", file_path.display(), e))?;
123+
if certs.is_empty() {
124+
return Err(anyhow::anyhow!("No certificates found in file {}", file_path.display()));
125+
}
126+
Ok(certs)
127+
}
128+
129+
/// Loads a private key from a PEM file.
130+
async fn load_private_key(file_path: &Path) -> anyhow::Result<PrivateKeyDer<'static>> {
131+
let data = read_file_limited(file_path).await?;
132+
let keys: Vec<PrivatePkcs8KeyDer<'static>> = rustls_pemfile::pkcs8_private_keys(&mut std::io::Cursor::new(data))
133+
.collect::<Result<Vec<_>, _>>()
134+
.map_err(|e| anyhow::anyhow!("Failed to parse private keys from {}: {:?}", file_path.display(), e))?;
135+
let key = keys
136+
.into_iter()
137+
.next()
138+
.ok_or_else(|| anyhow::anyhow!("No private key found in file {}", file_path.display()))?;
139+
Ok(PrivateKeyDer::Pkcs8(key))
140+
}
141+
142+
/// Creates a custom CryptoProvider with specific cipher suites.
143+
fn custom_crypto_provider() -> rustls::crypto::CryptoProvider {
144+
use rustls::crypto::ring::default_provider;
145+
use rustls::crypto::ring::cipher_suite;
146+
use rustls::crypto::ring::kx_group;
147+
148+
let cipher_suites = vec![
149+
// TLS 1.3
150+
// test with: $ openssl s_client -connect 127.0.0.1:3000 -tls1_3
151+
cipher_suite::TLS13_AES_256_GCM_SHA384,
152+
cipher_suite::TLS13_AES_128_GCM_SHA256,
153+
cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
154+
// TLS 1.2
155+
// these are ignored if builder_with_protocol_versions() below doesn't contain TLS 1.2
156+
// test with: $ openssl s_client -connect 127.0.0.1:3000 -tls1_2
157+
cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
158+
cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
159+
cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
160+
cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
161+
cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
162+
cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
163+
];
164+
165+
// (KX) groups used in TLS handshakes to negotiate the shared secret between client and server.
166+
let kx_groups = vec![
167+
kx_group::X25519,
168+
/*
169+
X25519:
170+
171+
An elliptic curve Diffie-Hellman (ECDH) key exchange algorithm based on Curve25519.
172+
Known for high security, speed, and resistance to side-channel attacks.
173+
Commonly used in modern TLS (1.2 and 1.3) due to its efficiency and forward secrecy.
174+
Preferred by many clients (e.g., browsers) for TLS 1.3 handshakes.
175+
*/
176+
kx_group::SECP256R1,
177+
/*
178+
SECP256R1 (aka NIST P-256):
179+
180+
An elliptic curve standardized by NIST, using a 256-bit prime field.
181+
Widely supported across TLS 1.2 and 1.3, especially in enterprise environments.
182+
Slightly less performant than X25519 but trusted due to long-standing use.
183+
Common in certificates signed by older CAs or legacy systems.
184+
*/
185+
kx_group::SECP384R1,
186+
/*
187+
SECP384R1 (aka NIST P-384):
188+
189+
Another NIST elliptic curve, using a 384-bit prime field for higher security.
190+
Offers stronger cryptographic strength than SECP256R1, at the cost of slower performance.
191+
Used in TLS 1.2 and 1.3 when higher assurance is needed (e.g., government systems).
192+
Less common than X25519 or SECP256R1 due to computational overhead.
193+
*/
194+
];
195+
196+
rustls::crypto::CryptoProvider {
197+
cipher_suites,
198+
kx_groups,
199+
..default_provider()
200+
}
201+
}
202+
82203
pub async fn exec(args: &ArgMatches) -> anyhow::Result<()> {
83204
let listen_addr = args.get_one::<String>("listen_addr").unwrap();
84205
let cert_dir = args.get_one::<spacetimedb_paths::cli::ConfigDir>("jwt_key_dir");
@@ -154,20 +275,37 @@ pub async fn exec(args: &ArgMatches) -> anyhow::Result<()> {
154275

155276
use std::net::SocketAddr;
156277
let addr: SocketAddr = listen_addr.parse()?;
278+
157279
if args.get_flag("ssl") {
158-
use std::path::{Path, PathBuf};
280+
// Install custom CryptoProvider at the start
281+
rustls::crypto::CryptoProvider::install_default(custom_crypto_provider())
282+
.map_err(|e| anyhow::anyhow!("Failed to install custom CryptoProvider: {:?}", e))?;
283+
159284
let cert_path: &Path = args.get_one::<PathBuf>("cert").context("Missing --cert for SSL")?.as_path();
160285
let key_path: &Path = args.get_one::<PathBuf>("key").context("Missing --key for SSL")?.as_path();
161-
// Install the default CryptoProvider at the start of the function
162-
// This only needs to happen once per process, so it's safe to call here
163-
use rustls::crypto::ring::default_provider;
164-
use rustls::crypto::CryptoProvider;
165-
CryptoProvider::install_default(default_provider())
166-
.expect("Failed to install default CryptoProvider");
167-
168-
use axum_server::tls_rustls::RustlsConfig;
169-
let tls_config = RustlsConfig::from_pem_file(cert_path, key_path).await?;
170-
log::debug!("Starting SpacetimeDB with SSL listening on {}", addr);
286+
287+
// Load certificate and private key with file size limit
288+
let cert_chain = load_certs(cert_path).await?;
289+
let private_key = load_private_key(key_path).await?;
290+
291+
// Create ServerConfig with secure settings
292+
let config=
293+
rustls::ServerConfig::builder_with_protocol_versions(&[
294+
&rustls::version::TLS13,
295+
// &rustls::version::TLS12,
296+
])
297+
// rustls::ServerConfig::builder() // using this instead, wouldn't restrict proto versions.
298+
.with_no_client_auth()
299+
.with_single_cert(cert_chain, private_key)
300+
.map_err(|e| anyhow::anyhow!("Failed to set certificates from files pub:'{}', priv:'{}', err: {}", cert_path.display(), key_path.display(), e))?;
301+
302+
// Use axum_server with custom config
303+
let tls_config = axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(config));
304+
305+
log::info!(
306+
"Starting SpacetimeDB with SSL on {}.",
307+
addr,
308+
);
171309
axum_server::bind_rustls(addr, tls_config)
172310
.serve(service.into_make_service())
173311
.await?;

0 commit comments

Comments
 (0)