//! The hub's own material under `/keys/` (hub spec §15.5): its //! delegate identity, published at `/.well-known/oxyhub-key`, and the //! developer edition's self-signed TLS certificate. use oxy_core::hash::KeyId; use oxy_core::identity::{Identity, SecretKey}; use std::fs; use std::path::Path; const IDENTITY_FILE: &str = "hub.identity"; const SECRET_FILE: &str = "hub.secret"; /// The hub identity: public object and key id; the secret stays on disk /// until something needs to sign (hub 0.6). #[derive(Clone, Debug)] pub struct HubKey { pub key: KeyId, pub identity: Identity, pub bytes: Vec, } /// Create the identity if absent, and return it. pub fn ensure_identity(keys: &Path, site_name: &str, base_url: &str) -> anyhow::Result { fs::create_dir_all(keys)?; let id_path = keys.join(IDENTITY_FILE); if id_path.exists() { return load_identity(keys); } let secret = SecretKey::generate(); let created = crate::db::now_ms() as u64; let host = base_url.split("://").nth(1).unwrap_or(base_url).split('/').next().unwrap_or("hub"); let identity = secret.identity(&format!("{site_name} (hub delegate)"), &format!("hub@{host}"), created); let key = identity.hash(); write_private(&keys.join(SECRET_FILE), &secret.encode(&key))?; write_private(&id_path, &identity.encode())?; Ok(HubKey { key, bytes: identity.encode(), identity }) } /// The hub's secret key, for signing changes and attestations it makes on /// a user's behalf (hub spec §5.5). Read on demand, never kept in memory. pub fn load_secret(keys: &Path) -> anyhow::Result<(KeyId, SecretKey)> { let bytes = fs::read(keys.join(SECRET_FILE))?; Ok(SecretKey::decode(&bytes)?) } pub fn load_identity(keys: &Path) -> anyhow::Result { let bytes = fs::read(keys.join(IDENTITY_FILE))?; let identity = Identity::decode(&bytes)?; Ok(HubKey { key: identity.hash(), identity, bytes }) } /// Generate a self-signed certificate for the base URL's host (and /// localhost) if none exists. Returns whether one was written. pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result { if cert.exists() && key.exists() { return Ok(false); } let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string(); let mut names = vec![host.clone(), "localhost".to_string()]; if let Some(short) = host.split('.').next() { if short != host { names.push(short.to_string()); } } let ck = rcgen::generate_simple_self_signed(names)?; if let Some(dir) = cert.parent() { fs::create_dir_all(dir)?; } fs::write(cert, ck.cert.pem())?; write_private(key, ck.signing_key.serialize_pem().as_bytes())?; Ok(true) } fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> { fs::write(path, bytes)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn identity_is_created_once_and_reloaded() { let dir = tempfile::tempdir().unwrap(); let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap(); let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap(); assert_eq!(a.key, b.key); assert_eq!(a.identity.email, "hub@hub.example:7343"); assert!(dir.path().join(SECRET_FILE).exists()); } #[test] fn self_signed_certificate() { let dir = tempfile::tempdir().unwrap(); let cert = dir.path().join("keys/tls.crt"); let key = dir.path().join("keys/tls.key"); assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap()); assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE")); } }