1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3
4use async_trait::async_trait;
5use ed25519_dalek::{
6 Signature as DalekSignature, Signer, SigningKey, Verifier, VerifyingKey, PUBLIC_KEY_LENGTH,
7 SECRET_KEY_LENGTH, SIGNATURE_LENGTH,
8};
9use rand::rngs::OsRng;
10use sha2::{Digest, Sha256};
11
12use aex_core::{AgentId, Error, IdentityProvider, Result, Signature, SignatureAlgorithm};
13
14#[derive(Default)]
23pub struct PeerRegistry {
24 peers: RwLock<HashMap<AgentId, VerifyingKey>>,
25}
26
27impl PeerRegistry {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn register(&self, agent_id: AgentId, public_key: VerifyingKey) {
33 self.peers.write().unwrap().insert(agent_id, public_key);
34 }
35
36 pub fn lookup(&self, agent_id: &AgentId) -> Option<VerifyingKey> {
37 self.peers.read().unwrap().get(agent_id).copied()
38 }
39
40 pub fn len(&self) -> usize {
41 self.peers.read().unwrap().len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.peers.read().unwrap().is_empty()
46 }
47}
48
49pub struct SpizeNativeProvider {
57 agent_id: AgentId,
58 signing_key: SigningKey,
59 peer_registry: Arc<PeerRegistry>,
60}
61
62impl SpizeNativeProvider {
63 pub fn generate(org: &str, name: &str, peer_registry: Arc<PeerRegistry>) -> Result<Self> {
65 let signing_key = SigningKey::generate(&mut OsRng);
66 Self::from_signing_key(org, name, signing_key, peer_registry)
67 }
68
69 pub fn from_secret_bytes(
71 org: &str,
72 name: &str,
73 secret: [u8; SECRET_KEY_LENGTH],
74 peer_registry: Arc<PeerRegistry>,
75 ) -> Result<Self> {
76 let signing_key = SigningKey::from_bytes(&secret);
77 Self::from_signing_key(org, name, signing_key, peer_registry)
78 }
79
80 fn from_signing_key(
81 org: &str,
82 name: &str,
83 signing_key: SigningKey,
84 peer_registry: Arc<PeerRegistry>,
85 ) -> Result<Self> {
86 validate_label(org, "org")?;
87 validate_label(name, "name")?;
88 let verifying_key = signing_key.verifying_key();
89 let fingerprint = compute_fingerprint(&verifying_key);
90 let id_str = format!("spize:{}/{}:{}", org, name, fingerprint);
91 let agent_id = AgentId::new(id_str)?;
92 Ok(Self {
93 agent_id,
94 signing_key,
95 peer_registry,
96 })
97 }
98
99 pub fn public_key_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] {
103 self.signing_key.verifying_key().to_bytes()
104 }
105
106 pub fn verifying_key(&self) -> VerifyingKey {
108 self.signing_key.verifying_key()
109 }
110
111 pub fn secret_key_bytes(&self) -> [u8; SECRET_KEY_LENGTH] {
115 self.signing_key.to_bytes()
116 }
117}
118
119fn validate_label(s: &str, field: &str) -> Result<()> {
123 if s.is_empty() {
124 return Err(Error::InvalidAgentId(format!("{} is empty", field)));
125 }
126 if s.len() > 64 {
127 return Err(Error::InvalidAgentId(format!(
128 "{} too long: {}",
129 field,
130 s.len()
131 )));
132 }
133 for (i, c) in s.chars().enumerate() {
134 let ok = c.is_ascii_alphanumeric() || c == '-' || c == '_';
135 if !ok {
136 return Err(Error::InvalidAgentId(format!(
137 "{} char at {}: {:?} (allowed: a-z 0-9 - _)",
138 field, i, c
139 )));
140 }
141 }
142 Ok(())
143}
144
145fn compute_fingerprint(key: &VerifyingKey) -> String {
150 let hash = Sha256::digest(key.as_bytes());
151 hex::encode(&hash[..3])
152}
153
154#[async_trait]
155impl IdentityProvider for SpizeNativeProvider {
156 fn agent_id(&self) -> &AgentId {
157 &self.agent_id
158 }
159
160 async fn sign(&self, message: &[u8]) -> Result<Signature> {
161 let sig = self.signing_key.sign(message);
162 Ok(Signature {
163 algorithm: SignatureAlgorithm::Ed25519,
164 bytes: sig.to_bytes().to_vec(),
165 })
166 }
167
168 async fn verify_peer(
169 &self,
170 peer_id: &AgentId,
171 message: &[u8],
172 signature: &Signature,
173 ) -> Result<()> {
174 if signature.algorithm != SignatureAlgorithm::Ed25519 {
175 return Err(Error::SignatureFormat(format!(
176 "SpizeNative only accepts Ed25519, got {:?}",
177 signature.algorithm
178 )));
179 }
180 if signature.bytes.len() != SIGNATURE_LENGTH {
181 return Err(Error::SignatureFormat(format!(
182 "expected {} bytes, got {}",
183 SIGNATURE_LENGTH,
184 signature.bytes.len()
185 )));
186 }
187
188 let verifying_key = self
189 .peer_registry
190 .lookup(peer_id)
191 .ok_or_else(|| Error::NotFound(format!("peer {} not in registry", peer_id)))?;
192
193 let sig_bytes: [u8; SIGNATURE_LENGTH] = signature
194 .bytes
195 .as_slice()
196 .try_into()
197 .map_err(|_| Error::SignatureFormat("length mismatch".into()))?;
198 let dalek_sig = DalekSignature::from_bytes(&sig_bytes);
199
200 verifying_key
201 .verify(message, &dalek_sig)
202 .map_err(|_| Error::SignatureInvalid)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn setup_pair() -> (Arc<PeerRegistry>, SpizeNativeProvider, SpizeNativeProvider) {
211 let reg = Arc::new(PeerRegistry::new());
212 let alice = SpizeNativeProvider::generate("acme", "alice", reg.clone()).unwrap();
213 let bob = SpizeNativeProvider::generate("acme", "bob", reg.clone()).unwrap();
214 reg.register(alice.agent_id().clone(), alice.verifying_key());
215 reg.register(bob.agent_id().clone(), bob.verifying_key());
216 (reg, alice, bob)
217 }
218
219 #[tokio::test]
220 async fn sign_and_verify_roundtrip() {
221 let (_reg, alice, bob) = setup_pair();
222 let msg = b"hello bob, from alice";
223 let sig = alice.sign(msg).await.unwrap();
224 bob.verify_peer(alice.agent_id(), msg, &sig).await.unwrap();
225 }
226
227 #[tokio::test]
228 async fn tampered_message_rejected() {
229 let (_reg, alice, bob) = setup_pair();
230 let msg = b"hello";
231 let sig = alice.sign(msg).await.unwrap();
232 let err = bob
233 .verify_peer(alice.agent_id(), b"hxllo", &sig)
234 .await
235 .unwrap_err();
236 assert!(matches!(err, Error::SignatureInvalid));
237 }
238
239 #[tokio::test]
240 async fn tampered_signature_rejected() {
241 let (_reg, alice, bob) = setup_pair();
242 let msg = b"hello";
243 let mut sig = alice.sign(msg).await.unwrap();
244 sig.bytes[0] ^= 0xff;
245 let err = bob
246 .verify_peer(alice.agent_id(), msg, &sig)
247 .await
248 .unwrap_err();
249 assert!(matches!(err, Error::SignatureInvalid));
250 }
251
252 #[tokio::test]
253 async fn unknown_peer_rejected() {
254 let reg = Arc::new(PeerRegistry::new());
255 let alice = SpizeNativeProvider::generate("acme", "alice", reg.clone()).unwrap();
256 let bob = SpizeNativeProvider::generate("acme", "bob", reg.clone()).unwrap();
257 let sig = alice.sign(b"hi").await.unwrap();
259 let err = bob
260 .verify_peer(alice.agent_id(), b"hi", &sig)
261 .await
262 .unwrap_err();
263 assert!(matches!(err, Error::NotFound(_)));
264 }
265
266 #[tokio::test]
267 async fn wrong_algorithm_rejected() {
268 let (_reg, alice, bob) = setup_pair();
269 let wrong = Signature {
270 algorithm: SignatureAlgorithm::EcdsaSecp256k1,
271 bytes: vec![0u8; SIGNATURE_LENGTH],
272 };
273 let err = bob
274 .verify_peer(alice.agent_id(), b"hi", &wrong)
275 .await
276 .unwrap_err();
277 assert!(matches!(err, Error::SignatureFormat(_)));
278 }
279
280 #[tokio::test]
281 async fn wrong_signature_length_rejected() {
282 let (_reg, alice, bob) = setup_pair();
283 let wrong = Signature {
284 algorithm: SignatureAlgorithm::Ed25519,
285 bytes: vec![0u8; 32], };
287 let err = bob
288 .verify_peer(alice.agent_id(), b"hi", &wrong)
289 .await
290 .unwrap_err();
291 assert!(matches!(err, Error::SignatureFormat(_)));
292 }
293
294 #[test]
295 fn generate_produces_expected_agent_id_format() {
296 let reg = Arc::new(PeerRegistry::new());
297 let p = SpizeNativeProvider::generate("acme", "alice", reg).unwrap();
298 let id = p.agent_id().as_str();
299 assert!(id.starts_with("spize:acme/alice:"));
300 let fingerprint = id.rsplit(':').next().unwrap();
301 assert_eq!(fingerprint.len(), 6);
302 assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
303 }
304
305 #[test]
306 fn deterministic_id_from_same_secret() {
307 let reg = Arc::new(PeerRegistry::new());
308 let secret = [7u8; SECRET_KEY_LENGTH];
309 let p1 =
310 SpizeNativeProvider::from_secret_bytes("acme", "alice", secret, reg.clone()).unwrap();
311 let p2 = SpizeNativeProvider::from_secret_bytes("acme", "alice", secret, reg).unwrap();
312 assert_eq!(p1.agent_id(), p2.agent_id());
313 assert_eq!(p1.public_key_bytes(), p2.public_key_bytes());
314 }
315
316 #[test]
317 fn different_secrets_yield_different_ids() {
318 let reg = Arc::new(PeerRegistry::new());
319 let a = SpizeNativeProvider::from_secret_bytes(
320 "acme",
321 "alice",
322 [1u8; SECRET_KEY_LENGTH],
323 reg.clone(),
324 )
325 .unwrap();
326 let b =
327 SpizeNativeProvider::from_secret_bytes("acme", "alice", [2u8; SECRET_KEY_LENGTH], reg)
328 .unwrap();
329 assert_ne!(a.agent_id(), b.agent_id());
330 }
331
332 #[test]
333 fn empty_org_rejected() {
334 let reg = Arc::new(PeerRegistry::new());
335 assert!(matches!(
336 SpizeNativeProvider::generate("", "alice", reg),
337 Err(Error::InvalidAgentId(_))
338 ));
339 }
340
341 #[test]
342 fn empty_name_rejected() {
343 let reg = Arc::new(PeerRegistry::new());
344 assert!(matches!(
345 SpizeNativeProvider::generate("acme", "", reg),
346 Err(Error::InvalidAgentId(_))
347 ));
348 }
349
350 #[test]
351 fn bad_label_chars_rejected() {
352 let reg = Arc::new(PeerRegistry::new());
353 assert!(matches!(
354 SpizeNativeProvider::generate("acme corp", "alice", reg),
355 Err(Error::InvalidAgentId(_))
356 ));
357 }
358
359 #[tokio::test]
360 async fn cross_verification_between_many_peers() {
361 let reg = Arc::new(PeerRegistry::new());
362 let agents: Vec<SpizeNativeProvider> = (0..10)
363 .map(|i| {
364 let p = SpizeNativeProvider::generate("acme", &format!("agent-{}", i), reg.clone())
365 .unwrap();
366 reg.register(p.agent_id().clone(), p.verifying_key());
367 p
368 })
369 .collect();
370
371 let msg = b"broadcast";
373 for signer in &agents {
374 let sig = signer.sign(msg).await.unwrap();
375 for verifier in &agents {
376 verifier
377 .verify_peer(signer.agent_id(), msg, &sig)
378 .await
379 .expect("cross-verification failed");
380 }
381 }
382 }
383}