From 87440d5a23f0fd894d949295a7c1ff85043550fc Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:55:51 +0100 Subject: v0.4.0: add first-time TLS setup and server help --- src/server/setup.rs | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/server/setup.rs (limited to 'src/server/setup.rs') diff --git a/src/server/setup.rs b/src/server/setup.rs new file mode 100644 index 0000000..96982c5 --- /dev/null +++ b/src/server/setup.rs @@ -0,0 +1,140 @@ +//! First-time TLS identity generation for the server. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug)] +pub struct SetupReport { + pub identity: PathBuf, + pub key: PathBuf, + pub cert: PathBuf, +} + +/// Generate a self-signed RSA identity in `dir`. +/// +/// Produces `identity.p12` (empty password, matching `native_tls`), `key.pem`, +/// and `cert.pem`. Refuses to overwrite an existing identity. +pub fn init_identity(dir: &Path, common_name: &str) -> Result { + let common_name = if common_name.trim().is_empty() { + "localhost" + } else { + common_name.trim() + }; + + let identity = dir.join("identity.p12"); + let key = dir.join("key.pem"); + let cert = dir.join("cert.pem"); + + let existing: Vec = [&identity, &key, &cert] + .into_iter() + .filter(|path| path.exists()) + .map(|path| path.display().to_string()) + .collect(); + if !existing.is_empty() { + return Err(format!( + "Refusing to overwrite existing TLS files: {}\n\ + Remove them first if you really want a new identity.", + existing.join(", ") + )); + } + + std::fs::create_dir_all(dir) + .map_err(|error| format!("Could not create {}: {error}", dir.display()))?; + + let manual = manual_instructions(common_name); + let subject = format!("/CN={common_name}"); + + run_openssl( + dir, + &[ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + "key.pem", + "-out", + "cert.pem", + "-days", + "3650", + "-nodes", + "-subj", + &subject, + ], + "generate the certificate", + &manual, + )?; + + run_openssl( + dir, + &[ + "pkcs12", + "-export", + "-out", + "identity.p12", + "-inkey", + "key.pem", + "-in", + "cert.pem", + "-passout", + "pass:", + "-keypbe", + "PBE-SHA1-3DES", + "-certpbe", + "PBE-SHA1-3DES", + "-macalg", + "sha1", + ], + "build identity.p12", + &manual, + )?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + for path in [&key, &identity] { + let metadata = std::fs::metadata(path) + .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; + let mut permissions = metadata.permissions(); + permissions.set_mode(0o600); + std::fs::set_permissions(path, permissions).map_err(|error| { + format!("Could not protect {}: {error}", path.display()) + })?; + } + } + + Ok(SetupReport { + identity, + key, + cert, + }) +} + +fn run_openssl(dir: &Path, args: &[&str], action: &str, manual: &str) -> Result<(), String> { + let output = Command::new("openssl") + .current_dir(dir) + .args(args) + .output() + .map_err(|error| format!("Could not run openssl to {action}: {error}\n\n{manual}"))?; + + if output.status.success() { + return Ok(()); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!( + "openssl failed to {action}:\n{}\n\n{manual}", + stderr.trim() + )) +} + +fn manual_instructions(common_name: &str) -> String { + format!( + "Install openssl, or run these commands manually:\n\ + \x20 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \\\n\ + \x20 -days 3650 -nodes -subj \"/CN={common_name}\"\n\ + \x20 openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem \\\n\ + \x20 -passout pass:" + ) +} -- cgit v1.2.3