1use crate::caveats::Caveats;
10use crate::fingerprint::Fingerprint;
11use crate::user_key::{UserKey, UserPublic};
12use crate::{MeshError, Result};
13use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
14use rand::rngs::OsRng;
15use serde::{Deserialize, Serialize};
16
17pub struct AgentKey {
24 signing: SigningKey,
25 cert: CertChain,
26}
27
28impl AgentKey {
29 pub fn issue(user: &UserKey, metadata: AgentMetadata) -> Self {
36 let mut csprng = OsRng;
37 let signing = SigningKey::generate(&mut csprng);
38 let agent_pubkey_bytes: [u8; 32] = *signing.verifying_key().as_bytes();
39
40 let to_sign = sign_payload(&agent_pubkey_bytes, &metadata);
41 let sig = user.sign(&to_sign);
42
43 let cert = CertChain {
44 agent_pubkey: agent_pubkey_bytes,
45 metadata,
46 issuer: Issuer::User(user.public()),
47 issuer_sig: SerdeSig(sig),
48 };
49 Self { signing, cert }
50 }
51
52 pub fn delegate(&self, metadata: AgentMetadata) -> Result<Self> {
62 if !metadata.caveats.leq(&self.cert.metadata.caveats) {
63 return Err(MeshError::CaveatAmplification);
64 }
65 let mut csprng = OsRng;
66 let signing = SigningKey::generate(&mut csprng);
67 let sub_pubkey: [u8; 32] = *signing.verifying_key().as_bytes();
68
69 let to_sign = sign_payload(&sub_pubkey, &metadata);
70 let sig = self.signing.sign(&to_sign);
71
72 let cert = CertChain {
73 agent_pubkey: sub_pubkey,
74 metadata,
75 issuer: Issuer::Agent {
76 pubkey: self.cert.agent_pubkey,
77 parent: Box::new(self.cert.clone()),
78 },
79 issuer_sig: SerdeSig(sig),
80 };
81 Ok(Self { signing, cert })
82 }
83
84 pub fn sign(&self, message: &[u8]) -> Signature {
86 self.signing.sign(message)
87 }
88
89 #[must_use]
91 pub fn fingerprint(&self) -> Fingerprint {
92 Fingerprint::of_bytes(&self.cert.agent_pubkey)
93 }
94
95 #[must_use]
97 pub fn cert(&self) -> &CertChain {
98 &self.cert
99 }
100
101 #[must_use]
103 pub fn public_bytes(&self) -> [u8; 32] {
104 self.cert.agent_pubkey
105 }
106
107 #[must_use]
116 pub fn signing_key_bytes(&self) -> [u8; 32] {
117 self.signing.to_bytes()
118 }
119
120 pub fn from_seed_and_cert(seed: &[u8; 32], cert: CertChain) -> Result<Self> {
131 let signing = ed25519_dalek::SigningKey::from_bytes(seed);
132 let derived_pub: [u8; 32] = *signing.verifying_key().as_bytes();
133 if derived_pub != cert.agent_pubkey {
134 return Err(MeshError::BadSignature);
135 }
136 Ok(Self { signing, cert })
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct AgentMetadata {
145 pub role: String,
147 pub host: String,
149 pub capabilities: Vec<String>,
151 pub issued_at: String,
155 pub expires_at: Option<String>,
158 #[serde(default)]
169 pub caveats: Caveats,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub enum Issuer {
175 User(UserPublic),
177 Agent {
181 pubkey: [u8; 32],
184 parent: Box<CertChain>,
186 },
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct CertChain {
193 pub agent_pubkey: [u8; 32],
194 pub metadata: AgentMetadata,
195 pub issuer: Issuer,
197 pub issuer_sig: SerdeSig,
199}
200
201impl CertChain {
202 pub fn verify(&self) -> Result<()> {
214 let to_verify = sign_payload(&self.agent_pubkey, &self.metadata);
215 match &self.issuer {
216 Issuer::User(user) => {
217 user.verify(&to_verify, &self.issuer_sig.0)
220 }
221 Issuer::Agent { pubkey, parent } => {
222 if *pubkey != parent.agent_pubkey {
223 return Err(MeshError::InvalidCertChain(
224 "delegated cert issuer pubkey does not match its parent".into(),
225 ));
226 }
227 parent.verify()?;
228 verify_detached(pubkey, &to_verify, &self.issuer_sig.0)?;
229 if !self.metadata.caveats.leq(&parent.metadata.caveats) {
230 return Err(MeshError::CaveatAmplification);
231 }
232 Ok(())
233 }
234 }
235 }
236
237 #[must_use]
239 pub fn agent_fingerprint(&self) -> Fingerprint {
240 Fingerprint::of_bytes(&self.agent_pubkey)
241 }
242
243 #[must_use]
246 pub fn user_fingerprint(&self) -> Fingerprint {
247 match &self.issuer {
248 Issuer::User(user) => user.fingerprint(),
249 Issuer::Agent { parent, .. } => parent.user_fingerprint(),
250 }
251 }
252
253 #[must_use]
255 pub fn root_user_pubkey(&self) -> UserPublic {
256 match &self.issuer {
257 Issuer::User(user) => user.clone(),
258 Issuer::Agent { parent, .. } => parent.root_user_pubkey(),
259 }
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct SerdeSig(pub Signature);
267
268impl Serialize for SerdeSig {
269 fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
270 let bytes: [u8; 64] = self.0.to_bytes();
271 bytes.serialize(ser)
272 }
273}
274
275impl<'de> Deserialize<'de> for SerdeSig {
276 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
277 let bytes: Vec<u8> = Vec::deserialize(de)?;
278 if bytes.len() != 64 {
279 return Err(serde::de::Error::custom("expected 64-byte signature"));
280 }
281 let mut arr = [0u8; 64];
282 arr.copy_from_slice(&bytes);
283 Ok(Self(Signature::from_bytes(&arr)))
284 }
285}
286
287fn sign_payload(agent_pubkey: &[u8; 32], metadata: &AgentMetadata) -> Vec<u8> {
289 let meta_bytes =
290 serde_json::to_vec(metadata).expect("AgentMetadata serializes deterministically");
291 let mut out = Vec::with_capacity(32 + meta_bytes.len());
292 out.extend_from_slice(agent_pubkey);
293 out.extend_from_slice(&meta_bytes);
294 out
295}
296
297fn verify_detached(pubkey: &[u8; 32], msg: &[u8], sig: &Signature) -> Result<()> {
300 let vk = VerifyingKey::from_bytes(pubkey).map_err(|_| MeshError::BadSignature)?;
301 vk.verify_strict(msg, sig)
302 .map_err(|_| MeshError::BadSignature)
303}
304
305impl MeshError {
306 #[cfg(test)]
308 pub(crate) fn is_bad_signature(&self) -> bool {
309 matches!(self, Self::BadSignature)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn fixture_metadata(role: &str) -> AgentMetadata {
318 AgentMetadata {
319 role: role.to_string(),
320 host: "test-host".to_string(),
321 capabilities: vec!["test".to_string()],
322 issued_at: "2026-05-28T12:00:00Z".to_string(),
323 expires_at: None,
324 caveats: Caveats::top(),
325 }
326 }
327
328 #[test]
329 fn issue_agent_key_signed_by_user() {
330 let user = UserKey::generate();
331 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
332 assert_eq!(agent.cert().root_user_pubkey(), user.public());
333 assert_eq!(agent.cert().agent_pubkey, agent.public_bytes());
334 }
335
336 #[test]
337 fn verify_cert_chain_succeeds() {
338 let user = UserKey::generate();
339 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
340 agent.cert().verify().expect("fresh cert verifies");
341 }
342
343 #[test]
344 fn tampered_metadata_fails_verify() {
345 let user = UserKey::generate();
346 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
347 let mut cert = agent.cert().clone();
348 cert.metadata.role = "evil".to_string();
349 assert!(cert.verify().unwrap_err().is_bad_signature());
350 }
351
352 #[test]
354 fn fixture_caveats_default_to_top() {
355 assert_eq!(fixture_metadata("worker").caveats, Caveats::top());
356 }
357
358 #[test]
361 fn bounded_caveats_roundtrip_and_verify() {
362 let mut meta = fixture_metadata("worker");
363 meta.caveats = Caveats {
364 exec: crate::Scope::only(["git".to_string()]),
365 max_calls: crate::CountBound::AtMost(8),
366 ..Caveats::top()
367 };
368 let user = UserKey::generate();
369 let agent = AgentKey::issue(&user, meta.clone());
370 agent.cert().verify().expect("fresh cert verifies");
371
372 let json = serde_json::to_string(agent.cert()).unwrap();
373 let parsed: CertChain = serde_json::from_str(&json).unwrap();
374 assert_eq!(parsed.metadata.caveats, meta.caveats);
375 parsed.verify().expect("roundtripped cert verifies");
376 }
377
378 #[test]
381 fn tampered_caveats_fails_verify() {
382 let mut meta = fixture_metadata("worker");
383 meta.caveats = Caveats {
384 exec: crate::Scope::only(["git".to_string()]),
385 ..Caveats::top()
386 };
387 let user = UserKey::generate();
388 let agent = AgentKey::issue(&user, meta);
389 let mut cert = agent.cert().clone();
390 cert.metadata.caveats = Caveats::top(); assert!(cert.verify().unwrap_err().is_bad_signature());
392 }
393
394 #[test]
397 fn absent_caveats_default_to_top() {
398 let json = r#"{"role":"w","host":"h","capabilities":[],"issued_at":"2026-05-28T00:00:00Z","expires_at":null}"#;
399 let meta: AgentMetadata = serde_json::from_str(json).unwrap();
400 assert_eq!(meta.caveats, Caveats::top());
401 }
402
403 #[test]
404 fn tampered_agent_pubkey_fails_verify() {
405 let user = UserKey::generate();
406 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
407 let mut cert = agent.cert().clone();
408 cert.agent_pubkey[0] ^= 0xff;
409 assert!(cert.verify().unwrap_err().is_bad_signature());
410 }
411
412 #[test]
413 fn wrong_user_fails_verify() {
414 let user = UserKey::generate();
415 let other = UserKey::generate();
416 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
417 let mut cert = agent.cert().clone();
418 cert.issuer = Issuer::User(other.public());
419 assert!(cert.verify().unwrap_err().is_bad_signature());
420 }
421
422 #[test]
423 fn serde_roundtrip_cert_chain() {
424 let user = UserKey::generate();
425 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
426 let json = serde_json::to_string(agent.cert()).unwrap();
427 let parsed: CertChain = serde_json::from_str(&json).unwrap();
428 assert_eq!(&parsed, agent.cert());
429 parsed.verify().expect("roundtripped cert still verifies");
430 }
431
432 #[test]
433 fn fingerprints_match() {
434 let user = UserKey::generate();
435 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
436 let cert = agent.cert();
437 assert_eq!(agent.fingerprint(), cert.agent_fingerprint());
438 assert_eq!(cert.user_fingerprint(), user.fingerprint());
439 }
440
441 #[test]
442 fn agent_sign_distinct_from_user_sign() {
443 let user = UserKey::generate();
444 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
445 let user_sig = user.sign(b"x");
446 let agent_sig = agent.sign(b"x");
447 assert_ne!(user_sig.to_bytes(), agent_sig.to_bytes());
449 }
450
451 #[test]
452 fn signing_key_bytes_roundtrip_signs_identically() {
453 use ed25519_dalek::Signer;
454 let user = UserKey::generate();
455 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
456 let bytes = agent.signing_key_bytes();
457 assert_eq!(bytes.len(), 32);
458 let rebuilt = ed25519_dalek::SigningKey::from_bytes(&bytes);
461 let msg = b"transport-layer-handshake";
462 let from_agent = agent.sign(msg);
463 let from_rebuilt = rebuilt.sign(msg);
464 assert_eq!(from_agent.to_bytes(), from_rebuilt.to_bytes());
465 }
466
467 #[test]
468 fn from_seed_and_cert_roundtrips() {
469 let user = UserKey::generate();
470 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
471 let seed = agent.signing_key_bytes();
472 let cert = agent.cert().clone();
473 let rebuilt = AgentKey::from_seed_and_cert(&seed, cert).expect("seed+cert valid");
474 assert_eq!(rebuilt.fingerprint(), agent.fingerprint());
475 let msg = b"rebuild-test";
477 assert_eq!(rebuilt.sign(msg).to_bytes(), agent.sign(msg).to_bytes());
478 }
479
480 #[test]
481 fn from_seed_and_cert_rejects_mismatched_pairing() {
482 let user = UserKey::generate();
483 let agent_a = AgentKey::issue(&user, fixture_metadata("a"));
484 let agent_b = AgentKey::issue(&user, fixture_metadata("b"));
485 let res =
487 AgentKey::from_seed_and_cert(&agent_b.signing_key_bytes(), agent_a.cert().clone());
488 match res {
489 Ok(_) => panic!("mismatched pairing must reject"),
490 Err(e) => assert!(matches!(e, MeshError::BadSignature)),
491 }
492 }
493
494 #[test]
495 fn metadata_with_expiry_roundtrips() {
496 let mut meta = fixture_metadata("worker");
497 meta.expires_at = Some("2027-01-01T00:00:00Z".to_string());
498 let user = UserKey::generate();
499 let agent = AgentKey::issue(&user, meta.clone());
500 let cert = agent.cert();
501 assert_eq!(
502 cert.metadata.expires_at.as_deref(),
503 Some("2027-01-01T00:00:00Z")
504 );
505 let json = serde_json::to_string(cert).unwrap();
506 let parsed: CertChain = serde_json::from_str(&json).unwrap();
507 parsed.verify().unwrap();
508 }
509
510 fn meta_exec(role: &str, cmds: &[&str]) -> AgentMetadata {
514 AgentMetadata {
515 caveats: Caveats {
516 exec: crate::Scope::only(cmds.iter().map(|s| s.to_string())),
517 ..Caveats::top()
518 },
519 ..fixture_metadata(role)
520 }
521 }
522
523 #[test]
524 fn delegate_accepts_attenuation_and_roots_at_user() {
525 let user = UserKey::generate();
526 let parent = AgentKey::issue(&user, meta_exec("parent", &["git", "cargo"]));
527 let child = parent
529 .delegate(meta_exec("child", &["git"]))
530 .expect("attenuating delegation succeeds");
531 child.cert().verify().expect("delegated cert verifies");
532 assert_eq!(child.cert().user_fingerprint(), user.fingerprint());
533 assert_eq!(child.cert().root_user_pubkey(), user.public());
534 }
535
536 #[test]
537 fn delegate_rejects_amplification() {
538 let user = UserKey::generate();
539 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
540 let child = AgentMetadata {
542 caveats: Caveats::top(),
543 ..fixture_metadata("child")
544 };
545 assert!(matches!(
546 parent.delegate(child),
547 Err(MeshError::CaveatAmplification)
548 ));
549 }
550
551 #[test]
552 fn multi_level_delegation_attenuates_each_link() {
553 let user = UserKey::generate();
554 let a = AgentKey::issue(&user, meta_exec("a", &["git", "cargo"]));
555 let b = a.delegate(meta_exec("b", &["git"])).expect("B ⊑ A");
556 b.cert().verify().expect("B verifies through the chain");
557 assert_eq!(b.cert().user_fingerprint(), user.fingerprint());
558 assert!(matches!(
560 b.delegate(meta_exec("c", &["git", "rm"])),
561 Err(MeshError::CaveatAmplification)
562 ));
563 }
564
565 #[test]
566 fn delegated_cert_serde_roundtrips() {
567 let user = UserKey::generate();
568 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
569 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
570 let json = serde_json::to_string(child.cert()).unwrap();
571 let parsed: CertChain = serde_json::from_str(&json).unwrap();
572 assert_eq!(&parsed, child.cert());
573 parsed
574 .verify()
575 .expect("roundtripped delegated cert verifies");
576 }
577
578 #[test]
579 fn forged_amplifying_chain_fails_verify() {
580 let user = UserKey::generate();
584 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
585
586 let mut csprng = OsRng;
589 let child_signing = SigningKey::generate(&mut csprng);
590 let child_pubkey: [u8; 32] = *child_signing.verifying_key().as_bytes();
591 let child_meta = AgentMetadata {
592 caveats: Caveats::top(),
593 ..fixture_metadata("child")
594 };
595 let to_sign = sign_payload(&child_pubkey, &child_meta);
596 let sig = parent.sign(&to_sign); let forged = CertChain {
598 agent_pubkey: child_pubkey,
599 metadata: child_meta,
600 issuer: Issuer::Agent {
601 pubkey: parent.public_bytes(),
602 parent: Box::new(parent.cert().clone()),
603 },
604 issuer_sig: SerdeSig(sig),
605 };
606 assert!(matches!(
607 forged.verify(),
608 Err(MeshError::CaveatAmplification)
609 ));
610 }
611
612 #[test]
613 fn delegated_issuer_pubkey_must_match_parent() {
614 let user = UserKey::generate();
617 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
618 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
619 let mut cert = child.cert().clone();
620 if let Issuer::Agent { pubkey, .. } = &mut cert.issuer {
621 pubkey[0] ^= 0xff;
622 }
623 assert!(matches!(cert.verify(), Err(MeshError::InvalidCertChain(_))));
624 }
625}