OXYGENHUB
# 19c71c828f86 oxygenhub / crates / hub / src / keys.rs
3.7 KiB · Rust · last changed Milestone 0.1: browse 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
pub fn load_identity(keys: &Path) -> anyhow::Result<HubKey> {
40
    let bytes = fs::read(keys.join(IDENTITY_FILE))?;
41
    let identity = Identity::decode(&bytes)?;
42
    Ok(HubKey { key: identity.hash(), identity, bytes })
43
}
44
45
/// Generate a self-signed certificate for the base URL's host (and
46
/// localhost) if none exists. Returns whether one was written.
47
pub fn ensure_self_signed(cert: &Path, key: &Path, base_url: &str) -> anyhow::Result<bool> {
48
    if cert.exists() && key.exists() {
49
        return Ok(false);
50
    }
51
    let host = base_url.split("://").nth(1).unwrap_or(base_url).split(['/', ':']).next().unwrap_or("localhost").to_string();
52
    let mut names = vec![host.clone(), "localhost".to_string()];
53
    if let Some(short) = host.split('.').next() {
54
        if short != host {
55
            names.push(short.to_string());
56
        }
57
    }
58
    let ck = rcgen::generate_simple_self_signed(names)?;
59
    if let Some(dir) = cert.parent() {
60
        fs::create_dir_all(dir)?;
61
    }
62
    fs::write(cert, ck.cert.pem())?;
63
    write_private(key, ck.signing_key.serialize_pem().as_bytes())?;
64
    Ok(true)
65
}
66
67
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
68
    fs::write(path, bytes)?;
69
    #[cfg(unix)]
70
    {
71
        use std::os::unix::fs::PermissionsExt;
72
        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
73
    }
74
    Ok(())
75
}
76
77
#[cfg(test)]
78
mod tests {
79
    use super::*;
80
81
    #[test]
82
    fn identity_is_created_once_and_reloaded() {
83
        let dir = tempfile::tempdir().unwrap();
84
        let a = ensure_identity(dir.path(), "Test", "https://hub.example:7343").unwrap();
85
        let b = ensure_identity(dir.path(), "Other", "https://elsewhere").unwrap();
86
        assert_eq!(a.key, b.key);
87
        assert_eq!(a.identity.email, "[email protected]:7343");
88
        assert!(dir.path().join(SECRET_FILE).exists());
89
    }
90
91
    #[test]
92
    fn self_signed_certificate() {
93
        let dir = tempfile::tempdir().unwrap();
94
        let cert = dir.path().join("keys/tls.crt");
95
        let key = dir.path().join("keys/tls.key");
96
        assert!(ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
97
        assert!(!ensure_self_signed(&cert, &key, "https://earth.andymccall.xyz:7343").unwrap());
98
        assert!(fs::read_to_string(&cert).unwrap().contains("BEGIN CERTIFICATE"));
99
    }
100
}