OXYGENHUB

Coverage / crates/hub/src/keys.rs 96.9%

62 of 64 lines run, 9 of 12 functions · run #524 on main · run missed

1
//! The hub's own material under `<data>/keys/` (hub spec §15.5): its
2
//! delegate identity, published at `/.well-known/oxyhub-key`, and the
3
//! developer edition's self-signed TLS certificate.
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
/// The hub identity: public object and key id; the secret stays on disk
14
/// until something needs to sign (hub 0.6).
15
#[derive(Clone, Debug)]
16
pub struct HubKey {
17
    pub key: KeyId,
18
    pub identity: Identity,
19
    pub bytes: Vec<u8>,
20
}
21
22
/// Create the identity if absent, and return it.
23203×
pub fn ensure_identity(keys: &Path, site_name: &str, base_url: &str) -> anyhow::Result<HubKey> {
24203×
    fs::create_dir_all(keys)?;
25203×
    let id_path = keys.join(IDENTITY_FILE);
26203×
    if id_path.exists() {
271×
        return load_identity(keys);
28202×
    }
29202×
    let secret = SecretKey::generate();
30202×
    let created = crate::db::now_ms() as u64;
31202×
    let host = base_url.split("://").nth(1).unwrap_or(base_url).split('/').next().unwrap_or("hub");
32202×
    let identity = secret.identity(&format!("{site_name} (hub delegate)"), &format!("hub@{host}"), created);
33202×
    let key = identity.hash();
34202×
    write_private(&keys.join(SECRET_FILE), &secret.encode(&key))?;
35202×
    write_private(&id_path, &identity.encode())?;
36202×
    Ok(HubKey { key, bytes: identity.encode(), identity })
37203×
}
38
39
/// The hub's secret key, for signing changes and attestations it makes on
40
/// a user's behalf (hub spec §5.5). Read on demand, never kept in memory.
41531×
pub fn load_secret(keys: &Path) -> anyhow::Result<(KeyId, SecretKey)> {
42531×
    let bytes = fs::read(keys.join(SECRET_FILE))?;
43531×
    Ok(SecretKey::decode(&bytes)?)
44531×
}
45
461×
pub fn load_identity(keys: &Path) -> anyhow::Result<HubKey> {
471×
    let bytes = fs::read(keys.join(IDENTITY_FILE))?;
481×
    let identity = Identity::decode(&bytes)?;
491×
    Ok(HubKey { key: identity.hash(), identity, bytes })
501×
}
51
52
/// Generate a self-signed certificate for the base URL's host (and
53
/// localhost) if none exists. Returns whether one was written.
542×
pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result<bool> {
552×
    if cert.exists() && key.exists() {
561×
        return Ok(false);
571×
    }
581×
    let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string();
591×
    let mut names = vec![host.clone(), "localhost".to_string()];
601×
    if let Some(short) = host.split('.').next() {
611×
        if short != host {
621×
            names.push(short.to_string());
631×
        }
640×
    }
651×
    let ck = rcgen::generate_simple_self_signed(names)?;
661×
    if let Some(dir) = cert.parent() {
671×
        fs::create_dir_all(dir)?;
680×
    }
691×
    fs::write(cert, ck.cert.pem())?;
701×
    write_private(key, ck.signing_key.serialize_pem().as_bytes())?;
711×
    Ok(true)
722×
}
73
74405×
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
75405×
    fs::write(path, bytes)?;
76
    #[cfg(unix)]
77
    {
78
        use std::os::unix::fs::PermissionsExt;
79405×
        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
80
    }
81405×
    Ok(())
82405×
}
83
84
#[cfg(test)]
85
mod tests {
86
    use super::*;
87
88
    #[test]
891×
    fn identity_is_created_once_and_reloaded() {
901×
        let dir = tempfile::tempdir().unwrap();
911×
        let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap();
921×
        let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap();
931×
        assert_eq!(a.key, b.key);
941×
        assert_eq!(a.identity.email, "[email protected]:7343");
951×
        assert!(dir.path().join(SECRET_FILE).exists());
961×
    }
97
98
    #[test]
991×
    fn self_signed_certificate() {
1001×
        let dir = tempfile::tempdir().unwrap();
1011×
        let cert = dir.path().join("keys/tls.crt");
1021×
        let key = dir.path().join("keys/tls.key");
1031×
        assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
1041×
        assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
1051×
        assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE"));
1061×
    }
107
}