OXYGENHUB
# 8933faff2043 oxygenhub / crates / hub / src / keys.rs
4.0 KiB · Rust · last changed Web edits and browser conflict resolution; landing under a policy 1 month ago source blame history raw
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.
23
pub fn ensure_identity(keys: &Path, site_name: &str, base_url: &str) -> anyhow::Result<HubKey> {
24
    fs::create_dir_all(keys)?;
25
    let id_path = keys.join(IDENTITY_FILE);
26
    if id_path.exists() {
27
        return load_identity(keys);
28
    }
29
    let secret = SecretKey::generate();
30
    let created = crate::db::now_ms() as u64;
31
    let host = base_url.split("://").nth(1).unwrap_or(base_url).split('/').next().unwrap_or("hub");
32
    let identity = secret.identity(&format!("{site_name} (hub delegate)"), &format!("hub@{host}"), created);
33
    let key = identity.hash();
34
    write_private(&keys.join(SECRET_FILE), &secret.encode(&key))?;
35
    write_private(&id_path, &identity.encode())?;
36
    Ok(HubKey { key, bytes: identity.encode(), identity })
37
}
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.
41
pub fn load_secret(keys: &Path) -> anyhow::Result<(KeyId, SecretKey)> {
42
    let bytes = fs::read(keys.join(SECRET_FILE))?;
43
    Ok(SecretKey::decode(&bytes)?)
44
}
45
46
pub fn load_identity(keys: &Path) -> anyhow::Result<HubKey> {
47
    let bytes = fs::read(keys.join(IDENTITY_FILE))?;
48
    let identity = Identity::decode(&bytes)?;
49
    Ok(HubKey { key: identity.hash(), identity, bytes })
50
}
51
52
/// Generate a self-signed certificate for the base URL's host (and
53
/// localhost) if none exists. Returns whether one was written.
54
pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result<bool> {
55
    if cert.exists() && key.exists() {
56
        return Ok(false);
57
    }
58
    let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string();
59
    let mut names = vec![host.clone(), "localhost".to_string()];
60
    if let Some(short) = host.split('.').next() {
61
        if short != host {
62
            names.push(short.to_string());
63
        }
64
    }
65
    let ck = rcgen::generate_simple_self_signed(names)?;
66
    if let Some(dir) = cert.parent() {
67
        fs::create_dir_all(dir)?;
68
    }
69
    fs::write(cert, ck.cert.pem())?;
70
    write_private(key, ck.signing_key.serialize_pem().as_bytes())?;
71
    Ok(true)
72
}
73
74
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
75
    fs::write(path, bytes)?;
76
    #[cfg(unix)]
77
    {
78
        use std::os::unix::fs::PermissionsExt;
79
        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
80
    }
81
    Ok(())
82
}
83
84
#[cfg(test)]
85
mod tests {
86
    use super::*;
87
88
    #[test]
89
    fn identity_is_created_once_and_reloaded() {
90
        let dir = tempfile::tempdir().unwrap();
91
        let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap();
92
        let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap();
93
        assert_eq!(a.key, b.key);
94
        assert_eq!(a.identity.email, "[email protected]:7343");
95
        assert!(dir.path().join(SECRET_FILE).exists());
96
    }
97
98
    #[test]
99
    fn self_signed_certificate() {
100
        let dir = tempfile::tempdir().unwrap();
101
        let cert = dir.path().join("keys/tls.crt");
102
        let key = dir.path().join("keys/tls.key");
103
        assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
104
        assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
105
        assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE"));
106
    }
107
}