Skip to main content

borg_mesh/
identity.rs

1//! A node's identity in one collective: an ed25519 key in `<store>/mesh/node.key` (hex seed, 0600).
2//!
3//! Per collective, not per machine: membership of the work collective says nothing about a
4//! private one, and a key never has to be shared between two minds. The same seed is the node's
5//! iroh secret, so the id the transport authenticates is the key the membership lists.
6
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
11
12use crate::{from_hex, to_hex};
13
14pub struct Identity {
15    key: SigningKey,
16}
17
18pub fn dir(home: &Path) -> PathBuf {
19    home.join("mesh")
20}
21
22pub fn path(home: &Path) -> PathBuf {
23    dir(home).join("node.key")
24}
25
26/// `<store>/mesh/node.pub`: the public key, hex, for borg itself, which signs nothing: its messages
27/// name their node by it (borg msg.rs), and the mesh carries a message only under that key's origin
28/// (lines.rs `travels`). borg owns the path, since it reads the key.
29pub fn public_path(home: &Path) -> PathBuf {
30    borg::msg::node_pub_path(home)
31}
32
33impl Identity {
34    /// The node's key for the collective stored at `home`, created on first use.
35    /// Also writes `public_path` when it is missing.
36    pub fn load_or_create(home: &Path) -> Result<Identity, String> {
37        let p = path(home);
38        let me = match std::fs::read_to_string(&p) {
39            Ok(t) => Identity { key: SigningKey::from_bytes(&from_hex::<32>(&t).map_err(|e| format!("{}: {e}", p.display()))?) },
40            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
41                let seed = iroh::SecretKey::generate().to_bytes();
42                write_private(&p, &to_hex(&seed))?;
43                Identity { key: SigningKey::from_bytes(&seed) }
44            }
45            Err(e) => return Err(format!("{}: {e}", p.display())),
46        };
47        me.publish(home)?;
48        Ok(me)
49    }
50
51    /// Writes `public_path` unless it already names this key: whole, through a rename, so a reader
52    /// never sees half a key.
53    fn publish(&self, home: &Path) -> Result<(), String> {
54        let (p, key) = (public_path(home), self.public());
55        if std::fs::read_to_string(&p).is_ok_and(|t| t.trim() == key) {
56            return Ok(());
57        }
58        let tmp = p.with_extension(format!("pub.{}.tmp", std::process::id()));
59        std::fs::write(&tmp, format!("{key}\n")).map_err(|e| format!("{}: {e}", tmp.display()))?;
60        std::fs::rename(&tmp, &p).map_err(|e| format!("{}: {e}", p.display()))
61    }
62
63    /// The public key, hex: what `borg mesh id` prints and an admin invites.
64    pub fn public(&self) -> String {
65        to_hex(self.key.verifying_key().as_bytes())
66    }
67
68    pub fn sign(&self, msg: &[u8]) -> String {
69        to_hex(&self.key.sign(msg).to_bytes())
70    }
71
72    pub fn iroh_secret(&self) -> iroh::SecretKey {
73        iroh::SecretKey::from_bytes(&self.key.to_bytes())
74    }
75}
76
77/// Whether `key` (hex) is a valid ed25519 public key.
78pub fn check_key(key: &str) -> Result<VerifyingKey, String> {
79    VerifyingKey::from_bytes(&from_hex::<32>(key)?).map_err(|e| format!("bad key {key}: {e}"))
80}
81
82/// Whether `sig` (hex) is `key`'s (hex) signature of `msg`.
83pub fn verify(key: &str, msg: &[u8], sig: &str) -> Result<(), String> {
84    let k = check_key(key)?;
85    let s = Signature::from_bytes(&from_hex::<64>(sig)?);
86    k.verify_strict(msg, &s).map_err(|_| "bad signature".to_string())
87}
88
89/// Created 0600 inside a 0700 directory, never overwritten: a key that leaks is a member that leaks.
90fn write_private(p: &Path, text: &str) -> Result<(), String> {
91    use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
92    let d = p.parent().unwrap_or(Path::new("."));
93    std::fs::DirBuilder::new().recursive(true).mode(0o700).create(d).map_err(|e| format!("{}: {e}", d.display()))?;
94    let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(p).map_err(|e| format!("{}: {e}", p.display()))?;
95    f.write_all(text.as_bytes()).map_err(|e| format!("{}: {e}", p.display()))
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::os::unix::fs::PermissionsExt;
102
103    #[test]
104    fn created_once_private_and_stable() {
105        let home = std::env::temp_dir().join(format!("borg-mesh-id-{}", std::process::id()));
106        let _ = std::fs::remove_dir_all(&home);
107        let a = Identity::load_or_create(&home).unwrap();
108        let b = Identity::load_or_create(&home).unwrap();
109        assert_eq!(a.public(), b.public(), "the key is created once");
110        assert_eq!(std::fs::read_to_string(public_path(&home)).unwrap().trim(), a.public(), "published for borg");
111        assert_eq!(std::fs::metadata(path(&home)).unwrap().permissions().mode() & 0o777, 0o600);
112        assert_eq!(to_hex(a.iroh_secret().public().as_bytes()), a.public(), "iroh id == membership key");
113        let sig = a.sign(b"hello");
114        assert!(verify(&a.public(), b"hello", &sig).is_ok());
115        assert!(verify(&a.public(), b"hellO", &sig).is_err());
116        let _ = std::fs::remove_dir_all(&home);
117    }
118}