diff options
Diffstat (limited to 'src/server/setup.rs')
| -rw-r--r-- | src/server/setup.rs | 140 |
1 files changed, 140 insertions, 0 deletions
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 @@ | |||
| 1 | //! First-time TLS identity generation for the server. | ||
| 2 | |||
| 3 | use std::path::{Path, PathBuf}; | ||
| 4 | use std::process::Command; | ||
| 5 | |||
| 6 | #[derive(Debug)] | ||
| 7 | pub struct SetupReport { | ||
| 8 | pub identity: PathBuf, | ||
| 9 | pub key: PathBuf, | ||
| 10 | pub cert: PathBuf, | ||
| 11 | } | ||
| 12 | |||
| 13 | /// Generate a self-signed RSA identity in `dir`. | ||
| 14 | /// | ||
| 15 | /// Produces `identity.p12` (empty password, matching `native_tls`), `key.pem`, | ||
| 16 | /// and `cert.pem`. Refuses to overwrite an existing identity. | ||
| 17 | pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, String> { | ||
| 18 | let common_name = if common_name.trim().is_empty() { | ||
| 19 | "localhost" | ||
| 20 | } else { | ||
| 21 | common_name.trim() | ||
| 22 | }; | ||
| 23 | |||
| 24 | let identity = dir.join("identity.p12"); | ||
| 25 | let key = dir.join("key.pem"); | ||
| 26 | let cert = dir.join("cert.pem"); | ||
| 27 | |||
| 28 | let existing: Vec<String> = [&identity, &key, &cert] | ||
| 29 | .into_iter() | ||
| 30 | .filter(|path| path.exists()) | ||
| 31 | .map(|path| path.display().to_string()) | ||
| 32 | .collect(); | ||
| 33 | if !existing.is_empty() { | ||
| 34 | return Err(format!( | ||
| 35 | "Refusing to overwrite existing TLS files: {}\n\ | ||
| 36 | Remove them first if you really want a new identity.", | ||
| 37 | existing.join(", ") | ||
| 38 | )); | ||
| 39 | } | ||
| 40 | |||
| 41 | std::fs::create_dir_all(dir) | ||
| 42 | .map_err(|error| format!("Could not create {}: {error}", dir.display()))?; | ||
| 43 | |||
| 44 | let manual = manual_instructions(common_name); | ||
| 45 | let subject = format!("/CN={common_name}"); | ||
| 46 | |||
| 47 | run_openssl( | ||
| 48 | dir, | ||
| 49 | &[ | ||
| 50 | "req", | ||
| 51 | "-x509", | ||
| 52 | "-newkey", | ||
| 53 | "rsa:2048", | ||
| 54 | "-keyout", | ||
| 55 | "key.pem", | ||
| 56 | "-out", | ||
| 57 | "cert.pem", | ||
| 58 | "-days", | ||
| 59 | "3650", | ||
| 60 | "-nodes", | ||
| 61 | "-subj", | ||
| 62 | &subject, | ||
| 63 | ], | ||
| 64 | "generate the certificate", | ||
| 65 | &manual, | ||
| 66 | )?; | ||
| 67 | |||
| 68 | run_openssl( | ||
| 69 | dir, | ||
| 70 | &[ | ||
| 71 | "pkcs12", | ||
| 72 | "-export", | ||
| 73 | "-out", | ||
| 74 | "identity.p12", | ||
| 75 | "-inkey", | ||
| 76 | "key.pem", | ||
| 77 | "-in", | ||
| 78 | "cert.pem", | ||
| 79 | "-passout", | ||
| 80 | "pass:", | ||
| 81 | "-keypbe", | ||
| 82 | "PBE-SHA1-3DES", | ||
| 83 | "-certpbe", | ||
| 84 | "PBE-SHA1-3DES", | ||
| 85 | "-macalg", | ||
| 86 | "sha1", | ||
| 87 | ], | ||
| 88 | "build identity.p12", | ||
| 89 | &manual, | ||
| 90 | )?; | ||
| 91 | |||
| 92 | #[cfg(unix)] | ||
| 93 | { | ||
| 94 | use std::os::unix::fs::PermissionsExt; | ||
| 95 | |||
| 96 | for path in [&key, &identity] { | ||
| 97 | let metadata = std::fs::metadata(path) | ||
| 98 | .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; | ||
| 99 | let mut permissions = metadata.permissions(); | ||
| 100 | permissions.set_mode(0o600); | ||
| 101 | std::fs::set_permissions(path, permissions).map_err(|error| { | ||
| 102 | format!("Could not protect {}: {error}", path.display()) | ||
| 103 | })?; | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | Ok(SetupReport { | ||
| 108 | identity, | ||
| 109 | key, | ||
| 110 | cert, | ||
| 111 | }) | ||
| 112 | } | ||
| 113 | |||
| 114 | fn run_openssl(dir: &Path, args: &[&str], action: &str, manual: &str) -> Result<(), String> { | ||
| 115 | let output = Command::new("openssl") | ||
| 116 | .current_dir(dir) | ||
| 117 | .args(args) | ||
| 118 | .output() | ||
| 119 | .map_err(|error| format!("Could not run openssl to {action}: {error}\n\n{manual}"))?; | ||
| 120 | |||
| 121 | if output.status.success() { | ||
| 122 | return Ok(()); | ||
| 123 | } | ||
| 124 | |||
| 125 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 126 | Err(format!( | ||
| 127 | "openssl failed to {action}:\n{}\n\n{manual}", | ||
| 128 | stderr.trim() | ||
| 129 | )) | ||
| 130 | } | ||
| 131 | |||
| 132 | fn manual_instructions(common_name: &str) -> String { | ||
| 133 | format!( | ||
| 134 | "Install openssl, or run these commands manually:\n\ | ||
| 135 | \x20 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \\\n\ | ||
| 136 | \x20 -days 3650 -nodes -subj \"/CN={common_name}\"\n\ | ||
| 137 | \x20 openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem \\\n\ | ||
| 138 | \x20 -passout pass:" | ||
| 139 | ) | ||
| 140 | } | ||
