huddle_protocol/
identity.rs1use ed25519_dalek::{Signer, SigningKey};
2use sha2::{Digest, Sha256};
3use zeroize::Zeroizing;
4
5use crate::crypto::pqc::{self, PqKeypair};
6use crate::error::Result;
7
8pub struct IdentityKeys {
17 signing_key: SigningKey,
18 fingerprint: String,
19}
20
21impl IdentityKeys {
22 pub fn generate() -> Result<Self> {
23 let mut rng = rand::thread_rng();
24 Ok(Self::from_signing_key(SigningKey::generate(&mut rng)))
25 }
26
27 pub fn from_secret_bytes(bytes: [u8; 32]) -> Result<Self> {
28 Ok(Self::from_signing_key(SigningKey::from_bytes(&bytes)))
29 }
30
31 fn from_signing_key(signing_key: SigningKey) -> Self {
32 let public = signing_key.verifying_key().to_bytes();
33 let fingerprint = compute_fingerprint(&public);
34 Self {
35 signing_key,
36 fingerprint,
37 }
38 }
39
40 pub fn fingerprint(&self) -> &str {
41 &self.fingerprint
42 }
43
44 pub fn secret_bytes(&self) -> [u8; 32] {
45 self.signing_key.to_bytes()
46 }
47
48 pub fn public_bytes(&self) -> [u8; 32] {
49 self.signing_key.verifying_key().to_bytes()
50 }
51
52 pub fn sign(&self, msg: &[u8]) -> [u8; 64] {
56 self.signing_key.sign(msg).to_bytes()
57 }
58
59 pub fn pq_keypair(&self) -> PqKeypair {
65 let seed = Zeroizing::new(self.signing_key.to_bytes());
66 PqKeypair::from_identity_seed(&seed)
67 }
68
69 pub fn mlkem_public_bytes(&self) -> [u8; pqc::MLKEM_EK_LEN] {
73 self.pq_keypair().encapsulation_key_bytes()
74 }
75
76 pub fn mldsa_public_bytes(&self) -> [u8; crate::crypto::mldsa::MLDSA_PK_LEN] {
81 let seed = Zeroizing::new(self.signing_key.to_bytes());
82 crate::crypto::mldsa::MlDsaKeypair::from_identity_seed(&seed).public_bytes()
83 }
84
85 pub fn mldsa_sign(&self, msg: &[u8]) -> [u8; crate::crypto::mldsa::MLDSA_SIG_LEN] {
89 let seed = Zeroizing::new(self.signing_key.to_bytes());
90 crate::crypto::mldsa::MlDsaKeypair::from_identity_seed(&seed).sign(msg)
91 }
92
93 pub fn seed(&self) -> Zeroizing<[u8; 32]> {
100 Zeroizing::new(self.signing_key.to_bytes())
101 }
102
103 pub fn from_seed(seed: Zeroizing<[u8; 32]>) -> Result<Self> {
107 Ok(Self::from_signing_key(SigningKey::from_bytes(&seed)))
108 }
109}
110
111pub fn compute_fingerprint(public_key: &[u8; 32]) -> String {
117 let hash = Sha256::digest(public_key);
118 let hex_str = hex::encode(&hash[..12]);
119 hex_str
120 .as_bytes()
121 .chunks(4)
122 .map(|chunk| std::str::from_utf8(chunk).unwrap())
123 .collect::<Vec<&str>>()
124 .join("-")
125}
126
127pub const RELAY_AUTH_DOMAIN: &[u8] = b"huddle-relay-auth-v1";
134
135pub fn relay_auth_msg(nonce: &[u8]) -> Vec<u8> {
140 let mut m = Vec::with_capacity(RELAY_AUTH_DOMAIN.len() + nonce.len());
141 m.extend_from_slice(RELAY_AUTH_DOMAIN);
142 m.extend_from_slice(nonce);
143 m
144}
145
146pub fn safety_code(public_key: &[u8; 32]) -> String {
151 let hash = Sha256::digest(public_key);
152 let hex_str = hex::encode(&hash[..6]).to_ascii_uppercase();
153 let groups: Vec<&str> = hex_str
154 .as_bytes()
155 .chunks(4)
156 .map(|chunk| std::str::from_utf8(chunk).unwrap())
157 .collect();
158 format!("SAFE-{}", groups.join("-"))
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn fingerprint_is_deterministic_and_well_formed() {
167 let id = IdentityKeys::from_secret_bytes([42u8; 32]).unwrap();
168 let id2 = IdentityKeys::from_secret_bytes([42u8; 32]).unwrap();
169 assert_eq!(id.fingerprint(), id2.fingerprint());
170 let parts: Vec<&str> = id.fingerprint().split('-').collect();
171 assert_eq!(parts.len(), 6);
172 for p in &parts {
173 assert_eq!(p.len(), 4);
174 assert!(p.chars().all(|c| c.is_ascii_hexdigit()));
175 }
176 }
177
178 #[test]
179 fn mlkem_pubkey_is_stable_and_per_identity() {
180 let bytes = IdentityKeys::generate().unwrap().secret_bytes();
181 let a = IdentityKeys::from_secret_bytes(bytes).unwrap();
182 let b = IdentityKeys::from_secret_bytes(bytes).unwrap();
183 assert_eq!(a.mlkem_public_bytes(), b.mlkem_public_bytes());
184 assert_eq!(a.mlkem_public_bytes().len(), pqc::MLKEM_EK_LEN);
185 let other = IdentityKeys::generate().unwrap();
186 assert_ne!(a.mlkem_public_bytes(), other.mlkem_public_bytes());
187 }
188
189 #[test]
190 fn seed_round_trips_keys() {
191 let id = IdentityKeys::generate().unwrap();
192 assert_eq!(*id.seed(), id.secret_bytes());
193 let restored = IdentityKeys::from_seed(id.seed()).unwrap();
194 assert_eq!(id.fingerprint(), restored.fingerprint());
195 assert_eq!(id.mlkem_public_bytes(), restored.mlkem_public_bytes());
196 }
197
198 #[test]
199 fn safety_code_is_stable_and_well_formed() {
200 let a = safety_code(&[7u8; 32]);
201 assert_eq!(a, safety_code(&[7u8; 32]));
202 assert!(a.starts_with("SAFE-"));
203 let groups: Vec<&str> = a.trim_start_matches("SAFE-").split('-').collect();
204 assert_eq!(groups.len(), 3);
205 for g in &groups {
206 assert_eq!(g.len(), 4);
207 }
208 }
209
210 #[test]
211 fn relay_auth_msg_is_domain_prefixed() {
212 let m = relay_auth_msg(&[9u8; 32]);
213 assert!(m.starts_with(RELAY_AUTH_DOMAIN));
214 assert_eq!(m.len(), RELAY_AUTH_DOMAIN.len() + 32);
215 }
216}