brainwires_network/identity/
credentials.rs1use anyhow::{Context, Result};
2use chacha20poly1305::aead::{Aead, KeyInit};
3use chacha20poly1305::{ChaCha20Poly1305, Nonce};
4use rand::RngExt;
5use sha2::{Digest, Sha256};
6use zeroize::Zeroizing;
7
8const KEY_DOMAIN: &str = "brainwires-network-identity-v1:";
10
11#[derive(Clone)]
16pub struct SigningKey {
17 cipher: ChaCha20Poly1305,
18}
19
20impl SigningKey {
21 pub fn from_secret(secret: &str) -> Self {
23 let key = derive_key(secret);
24 let cipher = ChaCha20Poly1305::new(key.as_ref().into());
25 Self { cipher }
26 }
27
28 pub fn sign(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
30 let mut nonce_bytes = [0u8; 12];
31 rand::rng().fill(&mut nonce_bytes);
32 let nonce = Nonce::from_slice(&nonce_bytes);
33
34 let ciphertext = self
35 .cipher
36 .encrypt(nonce, plaintext)
37 .map_err(|e| anyhow::anyhow!("signing failed: {e}"))?;
38
39 let mut output = Vec::with_capacity(12 + ciphertext.len());
41 output.extend_from_slice(&nonce_bytes);
42 output.extend_from_slice(&ciphertext);
43 Ok(output)
44 }
45}
46
47impl std::fmt::Debug for SigningKey {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("SigningKey")
50 .field("cipher", &"<redacted>")
51 .finish()
52 }
53}
54
55#[derive(Clone)]
59pub struct VerifyingKey {
60 cipher: ChaCha20Poly1305,
61}
62
63impl VerifyingKey {
64 pub fn from_secret(secret: &str) -> Self {
67 let key = derive_key(secret);
68 let cipher = ChaCha20Poly1305::new(key.as_ref().into());
69 Self { cipher }
70 }
71
72 pub fn verify(&self, signed: &[u8]) -> Result<Vec<u8>> {
74 if signed.len() < 12 {
75 anyhow::bail!("signed message too short");
76 }
77 let (nonce_bytes, ciphertext) = signed.split_at(12);
78 let nonce = Nonce::from_slice(nonce_bytes);
79
80 self.cipher
81 .decrypt(nonce, ciphertext)
82 .map_err(|e| anyhow::anyhow!("verification failed: {e}"))
83 .context("message authentication failed")
84 }
85}
86
87impl std::fmt::Debug for VerifyingKey {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("VerifyingKey")
90 .field("cipher", &"<redacted>")
91 .finish()
92 }
93}
94
95fn derive_key(secret: &str) -> Zeroizing<[u8; 32]> {
98 let mut hasher = Sha256::new();
99 hasher.update(KEY_DOMAIN.as_bytes());
100 hasher.update(secret.as_bytes());
101 Zeroizing::new(hasher.finalize().into())
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn sign_and_verify_roundtrip() {
110 let secret = "test-secret-key";
111 let signer = SigningKey::from_secret(secret);
112 let verifier = VerifyingKey::from_secret(secret);
113
114 let message = b"hello, agent network";
115 let signed = signer.sign(message).unwrap();
116 let recovered = verifier.verify(&signed).unwrap();
117
118 assert_eq!(recovered, message);
119 }
120
121 #[test]
122 fn wrong_secret_fails_verification() {
123 let signer = SigningKey::from_secret("secret-a");
124 let verifier = VerifyingKey::from_secret("secret-b");
125
126 let signed = signer.sign(b"test").unwrap();
127 assert!(verifier.verify(&signed).is_err());
128 }
129
130 #[test]
131 fn tampered_message_fails() {
132 let secret = "test-secret";
133 let signer = SigningKey::from_secret(secret);
134 let verifier = VerifyingKey::from_secret(secret);
135
136 let mut signed = signer.sign(b"test").unwrap();
137 if let Some(byte) = signed.last_mut() {
139 *byte ^= 0xFF;
140 }
141 assert!(verifier.verify(&signed).is_err());
142 }
143
144 #[test]
145 fn too_short_message_fails() {
146 let verifier = VerifyingKey::from_secret("test");
147 assert!(verifier.verify(&[0u8; 5]).is_err());
148 }
149
150 #[test]
151 fn debug_redacts_key_material() {
152 let signer = SigningKey::from_secret("secret");
153 let debug = format!("{signer:?}");
154 assert!(debug.contains("<redacted>"));
155 assert!(!debug.contains("secret"));
156 }
157}