| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | | use oxy_core::hash::KeyId; |
| 6 | | use oxy_core::identity::{Identity, SecretKey}; |
| 7 | | use std::fs; |
| 8 | | use std::path::Path; |
| 9 | |
|
| 10 | | const IDENTITY_FILE: &str = "hub.identity"; |
| 11 | | const SECRET_FILE: &str = "hub.secret"; |
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | | #[derive(Clone, Debug)] |
| 16 | | pub struct HubKey { |
| 17 | | pub key: KeyId, |
| 18 | | pub identity: Identity, |
| 19 | | pub bytes: Vec<u8>, |
| 20 | | } |
| 21 | |
|
| 22 | |
|
| 23 | 195× | pub fn ensure_identity(keys: &Path, site_name: &str, base_url: &str) -> anyhow::Result<HubKey> { |
| 24 | 195× | fs::create_dir_all(keys)?; |
| 25 | 195× | let id_path = keys.join(IDENTITY_FILE); |
| 26 | 195× | if id_path.exists() { |
| 27 | 1× | return load_identity(keys); |
| 28 | 194× | } |
| 29 | 194× | let secret = SecretKey::generate(); |
| 30 | 194× | let created = crate::db::now_ms() as u64; |
| 31 | 194× | let host = base_url.split("://").nth(1).unwrap_or(base_url).split('/').next().unwrap_or("hub"); |
| 32 | 194× | let identity = secret.identity(&format!("{site_name} (hub delegate)"), &format!("hub@{host}"), created); |
| 33 | 194× | let key = identity.hash(); |
| 34 | 194× | write_private(&keys.join(SECRET_FILE), &secret.encode(&key))?; |
| 35 | 194× | write_private(&id_path, &identity.encode())?; |
| 36 | 194× | Ok(HubKey { key, bytes: identity.encode(), identity }) |
| 37 | 195× | } |
| 38 | |
|
| 39 | |
|
| 40 | |
|
| 41 | 508× | pub fn load_secret(keys: &Path) -> anyhow::Result<(KeyId, SecretKey)> { |
| 42 | 508× | let bytes = fs::read(keys.join(SECRET_FILE))?; |
| 43 | 508× | Ok(SecretKey::decode(&bytes)?) |
| 44 | 508× | } |
| 45 | |
|
| 46 | 1× | pub fn load_identity(keys: &Path) -> anyhow::Result<HubKey> { |
| 47 | 1× | let bytes = fs::read(keys.join(IDENTITY_FILE))?; |
| 48 | 1× | let identity = Identity::decode(&bytes)?; |
| 49 | 1× | Ok(HubKey { key: identity.hash(), identity, bytes }) |
| 50 | 1× | } |
| 51 | |
|
| 52 | |
|
| 53 | |
|
| 54 | 2× | pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result<bool> { |
| 55 | 2× | if cert.exists() && key.exists() { |
| 56 | 1× | return Ok(false); |
| 57 | 1× | } |
| 58 | 1× | let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string(); |
| 59 | 1× | let mut names = vec![host.clone(), "localhost".to_string()]; |
| 60 | 1× | if let Some(short) = host.split('.').next() { |
| 61 | 1× | if short != host { |
| 62 | 1× | names.push(short.to_string()); |
| 63 | 1× | } |
| 64 | 0× | } |
| 65 | 1× | let ck = rcgen::generate_simple_self_signed(names)?; |
| 66 | 1× | if let Some(dir) = cert.parent() { |
| 67 | 1× | fs::create_dir_all(dir)?; |
| 68 | 0× | } |
| 69 | 1× | fs::write(cert, ck.cert.pem())?; |
| 70 | 1× | write_private(key, ck.signing_key.serialize_pem().as_bytes())?; |
| 71 | 1× | Ok(true) |
| 72 | 2× | } |
| 73 | |
|
| 74 | 389× | fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> { |
| 75 | 389× | fs::write(path, bytes)?; |
| 76 | | #[cfg(unix)] |
| 77 | | { |
| 78 | | use std::os::unix::fs::PermissionsExt; |
| 79 | 389× | fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; |
| 80 | | } |
| 81 | 389× | Ok(()) |
| 82 | 389× | } |
| 83 | |
|
| 84 | | #[cfg(test)] |
| 85 | | mod tests { |
| 86 | | use super::*; |
| 87 | |
|
| 88 | | #[test] |
| 89 | 1× | fn identity_is_created_once_and_reloaded() { |
| 90 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 91 | 1× | let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap(); |
| 92 | 1× | let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap(); |
| 93 | 1× | assert_eq!(a.key, b.key); |
| 94 | 1× | assert_eq!(a.identity.email, "[email protected]:7343"); |
| 95 | 1× | assert!(dir.path().join(SECRET_FILE).exists()); |
| 96 | 1× | } |
| 97 | |
|
| 98 | | #[test] |
| 99 | 1× | fn self_signed_certificate() { |
| 100 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 101 | 1× | let cert = dir.path().join("keys/tls.crt"); |
| 102 | 1× | let key = dir.path().join("keys/tls.key"); |
| 103 | 1× | assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); |
| 104 | 1× | assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); |
| 105 | 1× | assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE")); |
| 106 | 1× | } |
| 107 | | } |