seyd_signal_client/
identity.rs1use base64::Engine;
9use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
10use std::path::Path;
11
12#[derive(Clone)]
13pub struct Identity {
14 signing: SigningKey,
15}
16
17impl Identity {
18 pub fn generate() -> Self {
19 Self {
20 signing: SigningKey::generate(&mut rand::rngs::OsRng),
21 }
22 }
23
24 pub fn from_seed(seed: [u8; 32]) -> Self {
25 Self {
26 signing: SigningKey::from_bytes(&seed),
27 }
28 }
29
30 pub fn load_or_create(path: &Path) -> std::io::Result<Self> {
32 match std::fs::read(path) {
33 Ok(bytes) if bytes.len() == 32 => {
34 let mut seed = [0u8; 32];
35 seed.copy_from_slice(&bytes);
36 Ok(Self::from_seed(seed))
37 }
38 Ok(_) => Err(std::io::Error::new(
39 std::io::ErrorKind::InvalidData,
40 format!("{}: expected a 32-byte Ed25519 seed", path.display()),
41 )),
42 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
43 let id = Self::generate();
44 if let Some(dir) = path.parent() {
45 std::fs::create_dir_all(dir)?;
46 }
47 std::fs::write(path, id.signing.to_bytes())?;
48 #[cfg(unix)]
49 {
50 use std::os::unix::fs::PermissionsExt;
51 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
52 }
53 tracing::info!(path = %path.display(), "created new robot identity");
54 Ok(id)
55 }
56 Err(e) => Err(e),
57 }
58 }
59
60 pub fn verifying_key(&self) -> VerifyingKey {
61 self.signing.verifying_key()
62 }
63
64 pub fn public_key_b64(&self) -> String {
65 base64::engine::general_purpose::STANDARD.encode(self.verifying_key().to_bytes())
66 }
67
68 pub fn sign_b64(&self, msg: &[u8]) -> String {
69 base64::engine::general_purpose::STANDARD.encode(self.signing.sign(msg).to_bytes())
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use ed25519_dalek::Verifier;
77
78 #[test]
79 fn signature_verifies_with_public_key() {
80 let id = Identity::generate();
81 let sig = id.sign_b64(b"nonce");
82 let sig_bytes = base64::engine::general_purpose::STANDARD
83 .decode(sig)
84 .unwrap();
85 let sig = ed25519_dalek::Signature::from_slice(&sig_bytes).unwrap();
86 assert!(id.verifying_key().verify(b"nonce", &sig).is_ok());
87 }
88
89 #[test]
90 fn load_or_create_roundtrip() {
91 let dir = std::env::temp_dir().join(format!("seyd-id-{}", std::process::id()));
92 let path = dir.join("robot.key");
93 let a = Identity::load_or_create(&path).unwrap();
94 let b = Identity::load_or_create(&path).unwrap();
95 assert_eq!(a.public_key_b64(), b.public_key_b64());
96 std::fs::remove_dir_all(dir).unwrap();
97 }
98}