1use crate::caveats::{Caveats, Scope};
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 rand::RngCore;
16use serde::{Deserialize, Serialize};
17use zeroize::Zeroize;
18
19const POP_DOMAIN: &[u8] = b"agent-mesh/possession-challenge/v1";
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct PossessionChallenge {
31 pub issuer_pubkey: [u8; 32],
33 pub subject_pubkey: [u8; 32],
35 pub nonce: [u8; 32],
37}
38
39impl PossessionChallenge {
40 #[must_use]
44 pub fn signing_bytes(&self) -> Vec<u8> {
45 let mut v = Vec::with_capacity(POP_DOMAIN.len() + 96);
46 v.extend_from_slice(POP_DOMAIN);
47 v.extend_from_slice(&self.issuer_pubkey);
48 v.extend_from_slice(&self.subject_pubkey);
49 v.extend_from_slice(&self.nonce);
50 v
51 }
52}
53
54pub struct AgentKey {
61 signing: SigningKey,
62 cert: CertChain,
63}
64
65impl AgentKey {
66 pub fn issue(user: &UserKey, metadata: AgentMetadata) -> Self {
73 Self::issue_with(user, SigningKey::generate(&mut OsRng), metadata)
74 }
75
76 pub fn issue_derived(user: &UserKey, label: &str, metadata: AgentMetadata) -> Self {
94 let mut seed = user.derived_agent_seed(label);
95 let signing = SigningKey::from_bytes(&seed);
96 seed.zeroize();
97 Self::issue_with(user, signing, metadata)
98 }
99
100 fn issue_with(user: &UserKey, signing: SigningKey, metadata: AgentMetadata) -> Self {
101 let agent_pubkey_bytes: [u8; 32] = *signing.verifying_key().as_bytes();
102
103 let to_sign = sign_payload(&agent_pubkey_bytes, &metadata);
104 let sig = user.sign(&to_sign);
105
106 let cert = CertChain {
107 agent_pubkey: agent_pubkey_bytes,
108 metadata,
109 issuer: Issuer::User(user.public()),
110 issuer_sig: SerdeSig(sig),
111 };
112 Self { signing, cert }
113 }
114
115 pub fn delegate(&self, metadata: AgentMetadata) -> Result<Self> {
125 let mut csprng = OsRng;
126 let signing = SigningKey::generate(&mut csprng);
127 let sub_pubkey: [u8; 32] = *signing.verifying_key().as_bytes();
128 let cert = self.certify_pubkey(sub_pubkey, metadata)?;
129 Ok(Self { signing, cert })
130 }
131
132 #[must_use]
138 pub fn possession_challenge(&self, subject_pubkey: [u8; 32]) -> PossessionChallenge {
139 let mut nonce = [0u8; 32];
140 OsRng.fill_bytes(&mut nonce);
141 PossessionChallenge {
142 issuer_pubkey: self.cert.agent_pubkey,
143 subject_pubkey,
144 nonce,
145 }
146 }
147
148 pub fn delegate_external(
174 &self,
175 challenge: &PossessionChallenge,
176 proof: &Signature,
177 metadata: AgentMetadata,
178 ) -> Result<CertChain> {
179 if challenge.issuer_pubkey != self.cert.agent_pubkey {
181 return Err(MeshError::InvalidCertChain(
182 "possession challenge was not issued by this agent".into(),
183 ));
184 }
185 verify_detached(&challenge.subject_pubkey, &challenge.signing_bytes(), proof)?;
189 self.certify_pubkey(challenge.subject_pubkey, metadata)
190 }
191
192 fn certify_pubkey(&self, pubkey: [u8; 32], metadata: AgentMetadata) -> Result<CertChain> {
197 if !metadata.caveats.leq(&self.cert.metadata.caveats) {
198 return Err(MeshError::CaveatAmplification);
199 }
200 let to_sign = sign_payload(&pubkey, &metadata);
201 let sig = self.signing.sign(&to_sign);
202 Ok(CertChain {
203 agent_pubkey: pubkey,
204 metadata,
205 issuer: Issuer::Agent {
206 pubkey: self.cert.agent_pubkey,
207 parent: Box::new(self.cert.clone()),
208 },
209 issuer_sig: SerdeSig(sig),
210 })
211 }
212
213 pub fn sign(&self, message: &[u8]) -> Signature {
215 self.signing.sign(message)
216 }
217
218 #[must_use]
220 pub fn fingerprint(&self) -> Fingerprint {
221 Fingerprint::of_bytes(&self.cert.agent_pubkey)
222 }
223
224 #[must_use]
226 pub fn cert(&self) -> &CertChain {
227 &self.cert
228 }
229
230 #[must_use]
232 pub fn public_bytes(&self) -> [u8; 32] {
233 self.cert.agent_pubkey
234 }
235
236 #[must_use]
245 pub fn signing_key_bytes(&self) -> [u8; 32] {
246 self.signing.to_bytes()
247 }
248
249 pub fn from_seed_and_cert(seed: &[u8; 32], cert: CertChain) -> Result<Self> {
260 let signing = ed25519_dalek::SigningKey::from_bytes(seed);
261 let derived_pub: [u8; 32] = *signing.verifying_key().as_bytes();
262 if derived_pub != cert.agent_pubkey {
263 return Err(MeshError::BadSignature);
264 }
265 Ok(Self { signing, cert })
266 }
267
268 #[must_use]
270 pub fn verifying_key(&self) -> VerifyingKey {
271 self.signing.verifying_key()
272 }
273}
274
275impl crate::signer::MeshSigner for AgentKey {
280 fn verifying_key(&self) -> VerifyingKey {
281 self.signing.verifying_key()
282 }
283
284 fn sign(&self, msg: &[u8]) -> Signature {
285 self.signing.sign(msg)
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293pub struct AgentMetadata {
294 pub role: String,
296 pub host: String,
298 pub capabilities: Vec<String>,
300 pub issued_at: String,
304 pub expires_at: Option<String>,
307 #[serde(default)]
318 pub caveats: Caveats,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
323pub enum Issuer {
324 User(UserPublic),
326 Agent {
330 pubkey: [u8; 32],
333 parent: Box<CertChain>,
335 },
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
341pub struct CertChain {
342 pub agent_pubkey: [u8; 32],
343 pub metadata: AgentMetadata,
344 pub issuer: Issuer,
346 pub issuer_sig: SerdeSig,
348}
349
350impl CertChain {
351 pub fn verify(&self) -> Result<()> {
363 self.verify_inner(None)
364 }
365
366 pub fn verify_at(&self, current_generation: u64) -> Result<()> {
373 self.verify_inner(Some(current_generation))
374 }
375
376 fn verify_inner(&self, current_generation: Option<u64>) -> Result<()> {
382 check_generation(
384 &self.metadata.caveats.valid_for_generation,
385 current_generation,
386 )?;
387 let to_verify = sign_payload(&self.agent_pubkey, &self.metadata);
388 match &self.issuer {
389 Issuer::User(user) => {
390 user.verify(&to_verify, &self.issuer_sig.0)
393 }
394 Issuer::Agent { pubkey, parent } => {
395 if *pubkey != parent.agent_pubkey {
396 return Err(MeshError::InvalidCertChain(
397 "delegated cert issuer pubkey does not match its parent".into(),
398 ));
399 }
400 parent.verify_inner(current_generation)?;
401 verify_detached(pubkey, &to_verify, &self.issuer_sig.0)?;
402 if !self.metadata.caveats.leq(&parent.metadata.caveats) {
403 return Err(MeshError::CaveatAmplification);
404 }
405 Ok(())
406 }
407 }
408 }
409
410 #[must_use]
412 pub fn agent_fingerprint(&self) -> Fingerprint {
413 Fingerprint::of_bytes(&self.agent_pubkey)
414 }
415
416 #[must_use]
419 pub fn user_fingerprint(&self) -> Fingerprint {
420 match &self.issuer {
421 Issuer::User(user) => user.fingerprint(),
422 Issuer::Agent { parent, .. } => parent.user_fingerprint(),
423 }
424 }
425
426 #[must_use]
428 pub fn root_user_pubkey(&self) -> UserPublic {
429 match &self.issuer {
430 Issuer::User(user) => user.clone(),
431 Issuer::Agent { parent, .. } => parent.root_user_pubkey(),
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
439pub struct SerdeSig(pub Signature);
440
441impl Serialize for SerdeSig {
442 fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
443 let bytes: [u8; 64] = self.0.to_bytes();
444 bytes.serialize(ser)
445 }
446}
447
448impl<'de> Deserialize<'de> for SerdeSig {
449 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
450 let bytes: Vec<u8> = Vec::deserialize(de)?;
451 if bytes.len() != 64 {
452 return Err(serde::de::Error::custom("expected 64-byte signature"));
453 }
454 let mut arr = [0u8; 64];
455 arr.copy_from_slice(&bytes);
456 Ok(Self(Signature::from_bytes(&arr)))
457 }
458}
459
460fn sign_payload(agent_pubkey: &[u8; 32], metadata: &AgentMetadata) -> Vec<u8> {
462 let meta_bytes =
463 serde_json::to_vec(metadata).expect("AgentMetadata serializes deterministically");
464 let mut out = Vec::with_capacity(32 + meta_bytes.len());
465 out.extend_from_slice(agent_pubkey);
466 out.extend_from_slice(&meta_bytes);
467 out
468}
469
470fn check_generation(scope: &Scope<u64>, current: Option<u64>) -> Result<()> {
475 match scope {
476 Scope::All => Ok(()),
477 Scope::Only(gens) => match current {
478 None => Err(MeshError::Generation(
479 "cert is generation-scoped; verify with a current generation via verify_at()"
480 .into(),
481 )),
482 Some(g) if gens.contains(&g) => Ok(()),
483 Some(g) => Err(MeshError::Generation(format!(
484 "cert is not valid for generation {g}"
485 ))),
486 },
487 }
488}
489
490fn verify_detached(pubkey: &[u8; 32], msg: &[u8], sig: &Signature) -> Result<()> {
493 let vk = VerifyingKey::from_bytes(pubkey).map_err(|_| MeshError::BadSignature)?;
494 vk.verify_strict(msg, sig)
495 .map_err(|_| MeshError::BadSignature)
496}
497
498impl MeshError {
499 #[cfg(test)]
501 pub(crate) fn is_bad_signature(&self) -> bool {
502 matches!(self, Self::BadSignature)
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn fixture_metadata(role: &str) -> AgentMetadata {
511 AgentMetadata {
512 role: role.to_string(),
513 host: "test-host".to_string(),
514 capabilities: vec!["test".to_string()],
515 issued_at: "2026-05-28T12:00:00Z".to_string(),
516 expires_at: None,
517 caveats: Caveats::top(),
518 }
519 }
520
521 #[test]
522 fn issue_agent_key_signed_by_user() {
523 let user = UserKey::generate();
524 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
525 assert_eq!(agent.cert().root_user_pubkey(), user.public());
526 assert_eq!(agent.cert().agent_pubkey, agent.public_bytes());
527 }
528
529 #[test]
533 fn issue_derived_is_stable_per_user_and_label() {
534 let user = UserKey::generate();
535 let a = AgentKey::issue_derived(&user, "dock/hub", fixture_metadata("hub"));
536 let again = AgentKey::issue_derived(&user, "dock/hub", fixture_metadata("other-role"));
537 assert_eq!(
538 a.public_bytes(),
539 again.public_bytes(),
540 "same label, same key"
541 );
542 assert_eq!(a.fingerprint(), again.fingerprint());
543
544 let other_label = AgentKey::issue_derived(&user, "dock/host", fixture_metadata("hub"));
545 assert_ne!(
546 a.public_bytes(),
547 other_label.public_bytes(),
548 "label separates keys"
549 );
550 let other_user =
551 AgentKey::issue_derived(&UserKey::generate(), "dock/hub", fixture_metadata("hub"));
552 assert_ne!(
553 a.public_bytes(),
554 other_user.public_bytes(),
555 "user separates keys"
556 );
557 assert_ne!(
558 a.public_bytes(),
559 user.public().as_bytes(),
560 "not the user key"
561 );
562
563 a.cert().verify().expect("derived cert verifies");
564 assert_eq!(a.cert().root_user_pubkey(), user.public());
565 assert_eq!(a.cert().agent_pubkey, a.public_bytes());
566 }
567
568 #[test]
569 fn verify_cert_chain_succeeds() {
570 let user = UserKey::generate();
571 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
572 agent.cert().verify().expect("fresh cert verifies");
573 }
574
575 #[test]
578 fn unbounded_generation_verifies_in_any_context() {
579 let user = UserKey::generate();
580 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
581 agent.cert().verify().expect("context-free ok");
582 agent.cert().verify_at(0).expect("gen 0 ok");
583 agent.cert().verify_at(9_999).expect("any gen ok");
584 }
585
586 #[test]
590 fn generation_scoped_cert_is_fail_closed_context_free() {
591 let mut meta = fixture_metadata("worker");
592 meta.caveats = Caveats {
593 valid_for_generation: crate::Scope::only([5u64]),
594 ..Caveats::top()
595 };
596 let user = UserKey::generate();
597 let agent = AgentKey::issue(&user, meta);
598
599 assert!(matches!(
601 agent.cert().verify().unwrap_err(),
602 MeshError::Generation(_)
603 ));
604 agent.cert().verify_at(5).expect("valid for generation 5");
606 assert!(matches!(
608 agent.cert().verify_at(6).unwrap_err(),
609 MeshError::Generation(_)
610 ));
611 }
612
613 #[test]
617 fn generation_gate_applies_across_the_chain() {
618 let scoped = |role: &str| AgentMetadata {
619 caveats: Caveats {
620 valid_for_generation: crate::Scope::only([5u64]),
621 ..Caveats::top()
622 },
623 ..fixture_metadata(role)
624 };
625 let user = UserKey::generate();
626 let parent = AgentKey::issue(&user, scoped("lead"));
627 let child = parent
628 .delegate(scoped("worker"))
629 .expect("attenuating delegate");
630
631 assert!(matches!(
632 child.cert().verify().unwrap_err(),
633 MeshError::Generation(_)
634 ));
635 child
636 .cert()
637 .verify_at(5)
638 .expect("chain valid for generation 5");
639 assert!(matches!(
640 child.cert().verify_at(6).unwrap_err(),
641 MeshError::Generation(_)
642 ));
643 }
644
645 #[test]
646 fn tampered_metadata_fails_verify() {
647 let user = UserKey::generate();
648 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
649 let mut cert = agent.cert().clone();
650 cert.metadata.role = "evil".to_string();
651 assert!(cert.verify().unwrap_err().is_bad_signature());
652 }
653
654 #[test]
656 fn fixture_caveats_default_to_top() {
657 assert_eq!(fixture_metadata("worker").caveats, Caveats::top());
658 }
659
660 #[test]
663 fn bounded_caveats_roundtrip_and_verify() {
664 let mut meta = fixture_metadata("worker");
665 meta.caveats = Caveats {
666 exec: crate::Scope::only(["git".to_string()]),
667 max_calls: crate::CountBound::AtMost(8),
668 ..Caveats::top()
669 };
670 let user = UserKey::generate();
671 let agent = AgentKey::issue(&user, meta.clone());
672 agent.cert().verify().expect("fresh cert verifies");
673
674 let json = serde_json::to_string(agent.cert()).unwrap();
675 let parsed: CertChain = serde_json::from_str(&json).unwrap();
676 assert_eq!(parsed.metadata.caveats, meta.caveats);
677 parsed.verify().expect("roundtripped cert verifies");
678 }
679
680 #[test]
683 fn tampered_caveats_fails_verify() {
684 let mut meta = fixture_metadata("worker");
685 meta.caveats = Caveats {
686 exec: crate::Scope::only(["git".to_string()]),
687 ..Caveats::top()
688 };
689 let user = UserKey::generate();
690 let agent = AgentKey::issue(&user, meta);
691 let mut cert = agent.cert().clone();
692 cert.metadata.caveats = Caveats::top(); assert!(cert.verify().unwrap_err().is_bad_signature());
694 }
695
696 #[test]
699 fn absent_caveats_default_to_top() {
700 let json = r#"{"role":"w","host":"h","capabilities":[],"issued_at":"2026-05-28T00:00:00Z","expires_at":null}"#;
701 let meta: AgentMetadata = serde_json::from_str(json).unwrap();
702 assert_eq!(meta.caveats, Caveats::top());
703 }
704
705 #[test]
706 fn tampered_agent_pubkey_fails_verify() {
707 let user = UserKey::generate();
708 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
709 let mut cert = agent.cert().clone();
710 cert.agent_pubkey[0] ^= 0xff;
711 assert!(cert.verify().unwrap_err().is_bad_signature());
712 }
713
714 #[test]
715 fn wrong_user_fails_verify() {
716 let user = UserKey::generate();
717 let other = UserKey::generate();
718 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
719 let mut cert = agent.cert().clone();
720 cert.issuer = Issuer::User(other.public());
721 assert!(cert.verify().unwrap_err().is_bad_signature());
722 }
723
724 #[test]
725 fn serde_roundtrip_cert_chain() {
726 let user = UserKey::generate();
727 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
728 let json = serde_json::to_string(agent.cert()).unwrap();
729 let parsed: CertChain = serde_json::from_str(&json).unwrap();
730 assert_eq!(&parsed, agent.cert());
731 parsed.verify().expect("roundtripped cert still verifies");
732 }
733
734 #[test]
735 fn fingerprints_match() {
736 let user = UserKey::generate();
737 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
738 let cert = agent.cert();
739 assert_eq!(agent.fingerprint(), cert.agent_fingerprint());
740 assert_eq!(cert.user_fingerprint(), user.fingerprint());
741 }
742
743 #[test]
744 fn agent_sign_distinct_from_user_sign() {
745 let user = UserKey::generate();
746 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
747 let user_sig = user.sign(b"x");
748 let agent_sig = agent.sign(b"x");
749 assert_ne!(user_sig.to_bytes(), agent_sig.to_bytes());
751 }
752
753 #[test]
754 fn signing_key_bytes_roundtrip_signs_identically() {
755 use ed25519_dalek::Signer;
756 let user = UserKey::generate();
757 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
758 let bytes = agent.signing_key_bytes();
759 assert_eq!(bytes.len(), 32);
760 let rebuilt = ed25519_dalek::SigningKey::from_bytes(&bytes);
763 let msg = b"transport-layer-handshake";
764 let from_agent = agent.sign(msg);
765 let from_rebuilt = rebuilt.sign(msg);
766 assert_eq!(from_agent.to_bytes(), from_rebuilt.to_bytes());
767 }
768
769 #[test]
770 fn from_seed_and_cert_roundtrips() {
771 let user = UserKey::generate();
772 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
773 let seed = agent.signing_key_bytes();
774 let cert = agent.cert().clone();
775 let rebuilt = AgentKey::from_seed_and_cert(&seed, cert).expect("seed+cert valid");
776 assert_eq!(rebuilt.fingerprint(), agent.fingerprint());
777 let msg = b"rebuild-test";
779 assert_eq!(rebuilt.sign(msg).to_bytes(), agent.sign(msg).to_bytes());
780 }
781
782 #[test]
783 fn from_seed_and_cert_rejects_mismatched_pairing() {
784 let user = UserKey::generate();
785 let agent_a = AgentKey::issue(&user, fixture_metadata("a"));
786 let agent_b = AgentKey::issue(&user, fixture_metadata("b"));
787 let res =
789 AgentKey::from_seed_and_cert(&agent_b.signing_key_bytes(), agent_a.cert().clone());
790 match res {
791 Ok(_) => panic!("mismatched pairing must reject"),
792 Err(e) => assert!(matches!(e, MeshError::BadSignature)),
793 }
794 }
795
796 #[test]
797 fn metadata_with_expiry_roundtrips() {
798 let mut meta = fixture_metadata("worker");
799 meta.expires_at = Some("2027-01-01T00:00:00Z".to_string());
800 let user = UserKey::generate();
801 let agent = AgentKey::issue(&user, meta.clone());
802 let cert = agent.cert();
803 assert_eq!(
804 cert.metadata.expires_at.as_deref(),
805 Some("2027-01-01T00:00:00Z")
806 );
807 let json = serde_json::to_string(cert).unwrap();
808 let parsed: CertChain = serde_json::from_str(&json).unwrap();
809 parsed.verify().unwrap();
810 }
811
812 fn meta_exec(role: &str, cmds: &[&str]) -> AgentMetadata {
816 AgentMetadata {
817 caveats: Caveats {
818 exec: crate::Scope::only(cmds.iter().map(|s| s.to_string())),
819 ..Caveats::top()
820 },
821 ..fixture_metadata(role)
822 }
823 }
824
825 #[test]
826 fn delegate_accepts_attenuation_and_roots_at_user() {
827 let user = UserKey::generate();
828 let parent = AgentKey::issue(&user, meta_exec("parent", &["git", "cargo"]));
829 let child = parent
831 .delegate(meta_exec("child", &["git"]))
832 .expect("attenuating delegation succeeds");
833 child.cert().verify().expect("delegated cert verifies");
834 assert_eq!(child.cert().user_fingerprint(), user.fingerprint());
835 assert_eq!(child.cert().root_user_pubkey(), user.public());
836 }
837
838 #[test]
839 fn delegate_rejects_amplification() {
840 let user = UserKey::generate();
841 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
842 let child = AgentMetadata {
844 caveats: Caveats::top(),
845 ..fixture_metadata("child")
846 };
847 assert!(matches!(
848 parent.delegate(child),
849 Err(MeshError::CaveatAmplification)
850 ));
851 }
852
853 #[test]
854 fn multi_level_delegation_attenuates_each_link() {
855 let user = UserKey::generate();
856 let a = AgentKey::issue(&user, meta_exec("a", &["git", "cargo"]));
857 let b = a.delegate(meta_exec("b", &["git"])).expect("B ⊑ A");
858 b.cert().verify().expect("B verifies through the chain");
859 assert_eq!(b.cert().user_fingerprint(), user.fingerprint());
860 assert!(matches!(
862 b.delegate(meta_exec("c", &["git", "rm"])),
863 Err(MeshError::CaveatAmplification)
864 ));
865 }
866
867 #[test]
868 fn delegated_cert_serde_roundtrips() {
869 let user = UserKey::generate();
870 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
871 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
872 let json = serde_json::to_string(child.cert()).unwrap();
873 let parsed: CertChain = serde_json::from_str(&json).unwrap();
874 assert_eq!(&parsed, child.cert());
875 parsed
876 .verify()
877 .expect("roundtripped delegated cert verifies");
878 }
879
880 #[test]
881 fn forged_amplifying_chain_fails_verify() {
882 let user = UserKey::generate();
886 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
887
888 let mut csprng = OsRng;
891 let child_signing = SigningKey::generate(&mut csprng);
892 let child_pubkey: [u8; 32] = *child_signing.verifying_key().as_bytes();
893 let child_meta = AgentMetadata {
894 caveats: Caveats::top(),
895 ..fixture_metadata("child")
896 };
897 let to_sign = sign_payload(&child_pubkey, &child_meta);
898 let sig = parent.sign(&to_sign); let forged = CertChain {
900 agent_pubkey: child_pubkey,
901 metadata: child_meta,
902 issuer: Issuer::Agent {
903 pubkey: parent.public_bytes(),
904 parent: Box::new(parent.cert().clone()),
905 },
906 issuer_sig: SerdeSig(sig),
907 };
908 assert!(matches!(
909 forged.verify(),
910 Err(MeshError::CaveatAmplification)
911 ));
912 }
913
914 #[test]
922 fn delegate_external_certifies_phone_and_roots_at_user() {
923 use crate::envelope::{Recipient, SignedEnvelope};
924
925 let user = UserKey::generate();
926 let worker = AgentKey::issue(&user, meta_exec("worker", &["git", "cargo"]));
927
928 let phone_seed = [42u8; 32];
930 let phone_signing = SigningKey::from_bytes(&phone_seed);
931 let phone_vk = phone_signing.verifying_key();
932
933 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
935 let proof = phone_signing.sign(&challenge.signing_bytes());
936 let phone_cert = worker
938 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
939 .expect("external delegation attenuates");
940
941 assert_eq!(phone_cert.agent_pubkey, *phone_vk.as_bytes());
943 phone_cert.verify().expect("external cert verifies");
944 assert_eq!(phone_cert.root_user_pubkey(), user.public());
945 assert_eq!(phone_cert.user_fingerprint(), user.fingerprint());
946
947 let phone =
949 AgentKey::from_seed_and_cert(&phone_seed, phone_cert).expect("phone seed matches cert");
950
951 let env = SignedEnvelope::new(
953 &phone,
954 Recipient::Topic {
955 name: "drake/work".into(),
956 },
957 1,
958 b"from-the-phone".to_vec(),
959 );
960 env.verify().expect("phone envelope verifies");
961 assert_eq!(env.sender_user_fp(), user.fingerprint());
962
963 assert!(phone
965 .cert()
966 .metadata
967 .caveats
968 .leq(&worker.cert().metadata.caveats));
969 }
970
971 #[test]
975 fn delegate_external_rejects_wrong_pubkey() {
976 let user = UserKey::generate();
977 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
978
979 let phone_seed = [42u8; 32];
980 let phone_signing = SigningKey::from_bytes(&phone_seed);
981 let phone_vk = phone_signing.verifying_key();
982 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
983 let proof = phone_signing.sign(&challenge.signing_bytes());
984 let cert = worker
985 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
986 .unwrap();
987
988 let other_seed = [99u8; 32];
990 assert!(matches!(
991 AgentKey::from_seed_and_cert(&other_seed, cert.clone()),
992 Err(MeshError::BadSignature)
993 ));
994
995 let mut tampered = cert;
997 tampered.agent_pubkey[0] ^= 0xff;
998 assert!(matches!(
999 tampered.verify(),
1000 Err(MeshError::BadSignature) | Err(MeshError::CaveatAmplification)
1001 ));
1002 }
1003
1004 #[test]
1007 fn delegate_external_rejects_amplification() {
1008 let user = UserKey::generate();
1009 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1010 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
1011 let phone_vk = phone_signing.verifying_key();
1012 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
1013 let proof = phone_signing.sign(&challenge.signing_bytes());
1014 let amplifying = AgentMetadata {
1017 caveats: Caveats::top(),
1018 ..fixture_metadata("phone")
1019 };
1020 assert!(matches!(
1021 worker.delegate_external(&challenge, &proof, amplifying),
1022 Err(MeshError::CaveatAmplification)
1023 ));
1024 }
1025
1026 #[test]
1030 fn delegate_external_requires_proof_of_possession() {
1031 let user = UserKey::generate();
1032 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1033 let victim_vk = SigningKey::from_bytes(&[7u8; 32]).verifying_key();
1034 let challenge = worker.possession_challenge(*victim_vk.as_bytes());
1035 let attacker = SigningKey::from_bytes(&[9u8; 32]);
1037 let forged = attacker.sign(&challenge.signing_bytes());
1038 assert!(matches!(
1039 worker.delegate_external(&challenge, &forged, meta_exec("v", &["git"])),
1040 Err(MeshError::BadSignature)
1041 ));
1042 }
1043
1044 #[test]
1047 fn delegate_external_rejects_a_foreign_challenge() {
1048 let user = UserKey::generate();
1049 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1050 let other = AgentKey::issue(&user, meta_exec("other", &["git"]));
1051 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
1052 let phone_vk = phone_signing.verifying_key();
1053 let foreign = other.possession_challenge(*phone_vk.as_bytes());
1055 let proof = phone_signing.sign(&foreign.signing_bytes());
1056 assert!(matches!(
1057 worker.delegate_external(&foreign, &proof, meta_exec("phone", &["git"])),
1058 Err(MeshError::InvalidCertChain(_))
1059 ));
1060 }
1061
1062 #[test]
1063 fn delegated_issuer_pubkey_must_match_parent() {
1064 let user = UserKey::generate();
1067 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
1068 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
1069 let mut cert = child.cert().clone();
1070 if let Issuer::Agent { pubkey, .. } = &mut cert.issuer {
1071 pubkey[0] ^= 0xff;
1072 }
1073 assert!(matches!(cert.verify(), Err(MeshError::InvalidCertChain(_))));
1074 }
1075}