|
1 | 1 | 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; |
2 | 7 |
|
3 | 8 | use crate::StandaloneEnv;
|
4 | 9 | use anyhow::Context;
|
@@ -74,11 +79,127 @@ pub fn cli() -> clap::Command {
|
74 | 79 | )
|
75 | 80 | .arg(Arg::new("key").long("key").requires("ssl").value_name("FILE")
|
76 | 81 | .action(clap::ArgAction::Set)
|
77 |
| - .value_parser(clap::value_parser!(std::path::PathBuf)) |
| 82 | + .value_parser(clap::value_parser!(PathBuf)) |
78 | 83 | .help("--key server.key: The server keeps this private to decrypt and sign responses. ie. the server's private key"))
|
79 | 84 | // .after_help("Run `spacetime help start` for more detailed information.")
|
80 | 85 | }
|
81 | 86 |
|
| 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 | + |
82 | 203 | pub async fn exec(args: &ArgMatches) -> anyhow::Result<()> {
|
83 | 204 | let listen_addr = args.get_one::<String>("listen_addr").unwrap();
|
84 | 205 | 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<()> {
|
154 | 275 |
|
155 | 276 | use std::net::SocketAddr;
|
156 | 277 | let addr: SocketAddr = listen_addr.parse()?;
|
| 278 | + |
157 | 279 | 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 | + |
159 | 284 | let cert_path: &Path = args.get_one::<PathBuf>("cert").context("Missing --cert for SSL")?.as_path();
|
160 | 285 | 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 | + ); |
171 | 309 | axum_server::bind_rustls(addr, tls_config)
|
172 | 310 | .serve(service.into_make_service())
|
173 | 311 | .await?;
|
|
0 commit comments