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};
17
18const POP_DOMAIN: &[u8] = b"agent-mesh/possession-challenge/v1";
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct PossessionChallenge {
30 pub issuer_pubkey: [u8; 32],
32 pub subject_pubkey: [u8; 32],
34 pub nonce: [u8; 32],
36}
37
38impl PossessionChallenge {
39 #[must_use]
43 pub fn signing_bytes(&self) -> Vec<u8> {
44 let mut v = Vec::with_capacity(POP_DOMAIN.len() + 96);
45 v.extend_from_slice(POP_DOMAIN);
46 v.extend_from_slice(&self.issuer_pubkey);
47 v.extend_from_slice(&self.subject_pubkey);
48 v.extend_from_slice(&self.nonce);
49 v
50 }
51}
52
53pub struct AgentKey {
60 signing: SigningKey,
61 cert: CertChain,
62}
63
64impl AgentKey {
65 pub fn issue(user: &UserKey, metadata: AgentMetadata) -> Self {
72 let mut csprng = OsRng;
73 let signing = SigningKey::generate(&mut csprng);
74 let agent_pubkey_bytes: [u8; 32] = *signing.verifying_key().as_bytes();
75
76 let to_sign = sign_payload(&agent_pubkey_bytes, &metadata);
77 let sig = user.sign(&to_sign);
78
79 let cert = CertChain {
80 agent_pubkey: agent_pubkey_bytes,
81 metadata,
82 issuer: Issuer::User(user.public()),
83 issuer_sig: SerdeSig(sig),
84 };
85 Self { signing, cert }
86 }
87
88 pub fn delegate(&self, metadata: AgentMetadata) -> Result<Self> {
98 let mut csprng = OsRng;
99 let signing = SigningKey::generate(&mut csprng);
100 let sub_pubkey: [u8; 32] = *signing.verifying_key().as_bytes();
101 let cert = self.certify_pubkey(sub_pubkey, metadata)?;
102 Ok(Self { signing, cert })
103 }
104
105 #[must_use]
111 pub fn possession_challenge(&self, subject_pubkey: [u8; 32]) -> PossessionChallenge {
112 let mut nonce = [0u8; 32];
113 OsRng.fill_bytes(&mut nonce);
114 PossessionChallenge {
115 issuer_pubkey: self.cert.agent_pubkey,
116 subject_pubkey,
117 nonce,
118 }
119 }
120
121 pub fn delegate_external(
147 &self,
148 challenge: &PossessionChallenge,
149 proof: &Signature,
150 metadata: AgentMetadata,
151 ) -> Result<CertChain> {
152 if challenge.issuer_pubkey != self.cert.agent_pubkey {
154 return Err(MeshError::InvalidCertChain(
155 "possession challenge was not issued by this agent".into(),
156 ));
157 }
158 verify_detached(&challenge.subject_pubkey, &challenge.signing_bytes(), proof)?;
162 self.certify_pubkey(challenge.subject_pubkey, metadata)
163 }
164
165 fn certify_pubkey(&self, pubkey: [u8; 32], metadata: AgentMetadata) -> Result<CertChain> {
170 if !metadata.caveats.leq(&self.cert.metadata.caveats) {
171 return Err(MeshError::CaveatAmplification);
172 }
173 let to_sign = sign_payload(&pubkey, &metadata);
174 let sig = self.signing.sign(&to_sign);
175 Ok(CertChain {
176 agent_pubkey: pubkey,
177 metadata,
178 issuer: Issuer::Agent {
179 pubkey: self.cert.agent_pubkey,
180 parent: Box::new(self.cert.clone()),
181 },
182 issuer_sig: SerdeSig(sig),
183 })
184 }
185
186 pub fn sign(&self, message: &[u8]) -> Signature {
188 self.signing.sign(message)
189 }
190
191 #[must_use]
193 pub fn fingerprint(&self) -> Fingerprint {
194 Fingerprint::of_bytes(&self.cert.agent_pubkey)
195 }
196
197 #[must_use]
199 pub fn cert(&self) -> &CertChain {
200 &self.cert
201 }
202
203 #[must_use]
205 pub fn public_bytes(&self) -> [u8; 32] {
206 self.cert.agent_pubkey
207 }
208
209 #[must_use]
218 pub fn signing_key_bytes(&self) -> [u8; 32] {
219 self.signing.to_bytes()
220 }
221
222 pub fn from_seed_and_cert(seed: &[u8; 32], cert: CertChain) -> Result<Self> {
233 let signing = ed25519_dalek::SigningKey::from_bytes(seed);
234 let derived_pub: [u8; 32] = *signing.verifying_key().as_bytes();
235 if derived_pub != cert.agent_pubkey {
236 return Err(MeshError::BadSignature);
237 }
238 Ok(Self { signing, cert })
239 }
240
241 #[must_use]
243 pub fn verifying_key(&self) -> VerifyingKey {
244 self.signing.verifying_key()
245 }
246}
247
248impl crate::signer::MeshSigner for AgentKey {
253 fn verifying_key(&self) -> VerifyingKey {
254 self.signing.verifying_key()
255 }
256
257 fn sign(&self, msg: &[u8]) -> Signature {
258 self.signing.sign(msg)
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266pub struct AgentMetadata {
267 pub role: String,
269 pub host: String,
271 pub capabilities: Vec<String>,
273 pub issued_at: String,
277 pub expires_at: Option<String>,
280 #[serde(default)]
291 pub caveats: Caveats,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296pub enum Issuer {
297 User(UserPublic),
299 Agent {
303 pubkey: [u8; 32],
306 parent: Box<CertChain>,
308 },
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314pub struct CertChain {
315 pub agent_pubkey: [u8; 32],
316 pub metadata: AgentMetadata,
317 pub issuer: Issuer,
319 pub issuer_sig: SerdeSig,
321}
322
323impl CertChain {
324 pub fn verify(&self) -> Result<()> {
336 self.verify_inner(None)
337 }
338
339 pub fn verify_at(&self, current_generation: u64) -> Result<()> {
346 self.verify_inner(Some(current_generation))
347 }
348
349 fn verify_inner(&self, current_generation: Option<u64>) -> Result<()> {
355 check_generation(
357 &self.metadata.caveats.valid_for_generation,
358 current_generation,
359 )?;
360 let to_verify = sign_payload(&self.agent_pubkey, &self.metadata);
361 match &self.issuer {
362 Issuer::User(user) => {
363 user.verify(&to_verify, &self.issuer_sig.0)
366 }
367 Issuer::Agent { pubkey, parent } => {
368 if *pubkey != parent.agent_pubkey {
369 return Err(MeshError::InvalidCertChain(
370 "delegated cert issuer pubkey does not match its parent".into(),
371 ));
372 }
373 parent.verify_inner(current_generation)?;
374 verify_detached(pubkey, &to_verify, &self.issuer_sig.0)?;
375 if !self.metadata.caveats.leq(&parent.metadata.caveats) {
376 return Err(MeshError::CaveatAmplification);
377 }
378 Ok(())
379 }
380 }
381 }
382
383 #[must_use]
385 pub fn agent_fingerprint(&self) -> Fingerprint {
386 Fingerprint::of_bytes(&self.agent_pubkey)
387 }
388
389 #[must_use]
392 pub fn user_fingerprint(&self) -> Fingerprint {
393 match &self.issuer {
394 Issuer::User(user) => user.fingerprint(),
395 Issuer::Agent { parent, .. } => parent.user_fingerprint(),
396 }
397 }
398
399 #[must_use]
401 pub fn root_user_pubkey(&self) -> UserPublic {
402 match &self.issuer {
403 Issuer::User(user) => user.clone(),
404 Issuer::Agent { parent, .. } => parent.root_user_pubkey(),
405 }
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
412pub struct SerdeSig(pub Signature);
413
414impl Serialize for SerdeSig {
415 fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
416 let bytes: [u8; 64] = self.0.to_bytes();
417 bytes.serialize(ser)
418 }
419}
420
421impl<'de> Deserialize<'de> for SerdeSig {
422 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
423 let bytes: Vec<u8> = Vec::deserialize(de)?;
424 if bytes.len() != 64 {
425 return Err(serde::de::Error::custom("expected 64-byte signature"));
426 }
427 let mut arr = [0u8; 64];
428 arr.copy_from_slice(&bytes);
429 Ok(Self(Signature::from_bytes(&arr)))
430 }
431}
432
433fn sign_payload(agent_pubkey: &[u8; 32], metadata: &AgentMetadata) -> Vec<u8> {
435 let meta_bytes =
436 serde_json::to_vec(metadata).expect("AgentMetadata serializes deterministically");
437 let mut out = Vec::with_capacity(32 + meta_bytes.len());
438 out.extend_from_slice(agent_pubkey);
439 out.extend_from_slice(&meta_bytes);
440 out
441}
442
443fn check_generation(scope: &Scope<u64>, current: Option<u64>) -> Result<()> {
448 match scope {
449 Scope::All => Ok(()),
450 Scope::Only(gens) => match current {
451 None => Err(MeshError::Generation(
452 "cert is generation-scoped; verify with a current generation via verify_at()"
453 .into(),
454 )),
455 Some(g) if gens.contains(&g) => Ok(()),
456 Some(g) => Err(MeshError::Generation(format!(
457 "cert is not valid for generation {g}"
458 ))),
459 },
460 }
461}
462
463fn verify_detached(pubkey: &[u8; 32], msg: &[u8], sig: &Signature) -> Result<()> {
466 let vk = VerifyingKey::from_bytes(pubkey).map_err(|_| MeshError::BadSignature)?;
467 vk.verify_strict(msg, sig)
468 .map_err(|_| MeshError::BadSignature)
469}
470
471impl MeshError {
472 #[cfg(test)]
474 pub(crate) fn is_bad_signature(&self) -> bool {
475 matches!(self, Self::BadSignature)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn fixture_metadata(role: &str) -> AgentMetadata {
484 AgentMetadata {
485 role: role.to_string(),
486 host: "test-host".to_string(),
487 capabilities: vec!["test".to_string()],
488 issued_at: "2026-05-28T12:00:00Z".to_string(),
489 expires_at: None,
490 caveats: Caveats::top(),
491 }
492 }
493
494 #[test]
495 fn issue_agent_key_signed_by_user() {
496 let user = UserKey::generate();
497 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
498 assert_eq!(agent.cert().root_user_pubkey(), user.public());
499 assert_eq!(agent.cert().agent_pubkey, agent.public_bytes());
500 }
501
502 #[test]
503 fn verify_cert_chain_succeeds() {
504 let user = UserKey::generate();
505 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
506 agent.cert().verify().expect("fresh cert verifies");
507 }
508
509 #[test]
512 fn unbounded_generation_verifies_in_any_context() {
513 let user = UserKey::generate();
514 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
515 agent.cert().verify().expect("context-free ok");
516 agent.cert().verify_at(0).expect("gen 0 ok");
517 agent.cert().verify_at(9_999).expect("any gen ok");
518 }
519
520 #[test]
524 fn generation_scoped_cert_is_fail_closed_context_free() {
525 let mut meta = fixture_metadata("worker");
526 meta.caveats = Caveats {
527 valid_for_generation: crate::Scope::only([5u64]),
528 ..Caveats::top()
529 };
530 let user = UserKey::generate();
531 let agent = AgentKey::issue(&user, meta);
532
533 assert!(matches!(
535 agent.cert().verify().unwrap_err(),
536 MeshError::Generation(_)
537 ));
538 agent.cert().verify_at(5).expect("valid for generation 5");
540 assert!(matches!(
542 agent.cert().verify_at(6).unwrap_err(),
543 MeshError::Generation(_)
544 ));
545 }
546
547 #[test]
551 fn generation_gate_applies_across_the_chain() {
552 let scoped = |role: &str| AgentMetadata {
553 caveats: Caveats {
554 valid_for_generation: crate::Scope::only([5u64]),
555 ..Caveats::top()
556 },
557 ..fixture_metadata(role)
558 };
559 let user = UserKey::generate();
560 let parent = AgentKey::issue(&user, scoped("lead"));
561 let child = parent
562 .delegate(scoped("worker"))
563 .expect("attenuating delegate");
564
565 assert!(matches!(
566 child.cert().verify().unwrap_err(),
567 MeshError::Generation(_)
568 ));
569 child
570 .cert()
571 .verify_at(5)
572 .expect("chain valid for generation 5");
573 assert!(matches!(
574 child.cert().verify_at(6).unwrap_err(),
575 MeshError::Generation(_)
576 ));
577 }
578
579 #[test]
580 fn tampered_metadata_fails_verify() {
581 let user = UserKey::generate();
582 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
583 let mut cert = agent.cert().clone();
584 cert.metadata.role = "evil".to_string();
585 assert!(cert.verify().unwrap_err().is_bad_signature());
586 }
587
588 #[test]
590 fn fixture_caveats_default_to_top() {
591 assert_eq!(fixture_metadata("worker").caveats, Caveats::top());
592 }
593
594 #[test]
597 fn bounded_caveats_roundtrip_and_verify() {
598 let mut meta = fixture_metadata("worker");
599 meta.caveats = Caveats {
600 exec: crate::Scope::only(["git".to_string()]),
601 max_calls: crate::CountBound::AtMost(8),
602 ..Caveats::top()
603 };
604 let user = UserKey::generate();
605 let agent = AgentKey::issue(&user, meta.clone());
606 agent.cert().verify().expect("fresh cert verifies");
607
608 let json = serde_json::to_string(agent.cert()).unwrap();
609 let parsed: CertChain = serde_json::from_str(&json).unwrap();
610 assert_eq!(parsed.metadata.caveats, meta.caveats);
611 parsed.verify().expect("roundtripped cert verifies");
612 }
613
614 #[test]
617 fn tampered_caveats_fails_verify() {
618 let mut meta = fixture_metadata("worker");
619 meta.caveats = Caveats {
620 exec: crate::Scope::only(["git".to_string()]),
621 ..Caveats::top()
622 };
623 let user = UserKey::generate();
624 let agent = AgentKey::issue(&user, meta);
625 let mut cert = agent.cert().clone();
626 cert.metadata.caveats = Caveats::top(); assert!(cert.verify().unwrap_err().is_bad_signature());
628 }
629
630 #[test]
633 fn absent_caveats_default_to_top() {
634 let json = r#"{"role":"w","host":"h","capabilities":[],"issued_at":"2026-05-28T00:00:00Z","expires_at":null}"#;
635 let meta: AgentMetadata = serde_json::from_str(json).unwrap();
636 assert_eq!(meta.caveats, Caveats::top());
637 }
638
639 #[test]
640 fn tampered_agent_pubkey_fails_verify() {
641 let user = UserKey::generate();
642 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
643 let mut cert = agent.cert().clone();
644 cert.agent_pubkey[0] ^= 0xff;
645 assert!(cert.verify().unwrap_err().is_bad_signature());
646 }
647
648 #[test]
649 fn wrong_user_fails_verify() {
650 let user = UserKey::generate();
651 let other = UserKey::generate();
652 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
653 let mut cert = agent.cert().clone();
654 cert.issuer = Issuer::User(other.public());
655 assert!(cert.verify().unwrap_err().is_bad_signature());
656 }
657
658 #[test]
659 fn serde_roundtrip_cert_chain() {
660 let user = UserKey::generate();
661 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
662 let json = serde_json::to_string(agent.cert()).unwrap();
663 let parsed: CertChain = serde_json::from_str(&json).unwrap();
664 assert_eq!(&parsed, agent.cert());
665 parsed.verify().expect("roundtripped cert still verifies");
666 }
667
668 #[test]
669 fn fingerprints_match() {
670 let user = UserKey::generate();
671 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
672 let cert = agent.cert();
673 assert_eq!(agent.fingerprint(), cert.agent_fingerprint());
674 assert_eq!(cert.user_fingerprint(), user.fingerprint());
675 }
676
677 #[test]
678 fn agent_sign_distinct_from_user_sign() {
679 let user = UserKey::generate();
680 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
681 let user_sig = user.sign(b"x");
682 let agent_sig = agent.sign(b"x");
683 assert_ne!(user_sig.to_bytes(), agent_sig.to_bytes());
685 }
686
687 #[test]
688 fn signing_key_bytes_roundtrip_signs_identically() {
689 use ed25519_dalek::Signer;
690 let user = UserKey::generate();
691 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
692 let bytes = agent.signing_key_bytes();
693 assert_eq!(bytes.len(), 32);
694 let rebuilt = ed25519_dalek::SigningKey::from_bytes(&bytes);
697 let msg = b"transport-layer-handshake";
698 let from_agent = agent.sign(msg);
699 let from_rebuilt = rebuilt.sign(msg);
700 assert_eq!(from_agent.to_bytes(), from_rebuilt.to_bytes());
701 }
702
703 #[test]
704 fn from_seed_and_cert_roundtrips() {
705 let user = UserKey::generate();
706 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
707 let seed = agent.signing_key_bytes();
708 let cert = agent.cert().clone();
709 let rebuilt = AgentKey::from_seed_and_cert(&seed, cert).expect("seed+cert valid");
710 assert_eq!(rebuilt.fingerprint(), agent.fingerprint());
711 let msg = b"rebuild-test";
713 assert_eq!(rebuilt.sign(msg).to_bytes(), agent.sign(msg).to_bytes());
714 }
715
716 #[test]
717 fn from_seed_and_cert_rejects_mismatched_pairing() {
718 let user = UserKey::generate();
719 let agent_a = AgentKey::issue(&user, fixture_metadata("a"));
720 let agent_b = AgentKey::issue(&user, fixture_metadata("b"));
721 let res =
723 AgentKey::from_seed_and_cert(&agent_b.signing_key_bytes(), agent_a.cert().clone());
724 match res {
725 Ok(_) => panic!("mismatched pairing must reject"),
726 Err(e) => assert!(matches!(e, MeshError::BadSignature)),
727 }
728 }
729
730 #[test]
731 fn metadata_with_expiry_roundtrips() {
732 let mut meta = fixture_metadata("worker");
733 meta.expires_at = Some("2027-01-01T00:00:00Z".to_string());
734 let user = UserKey::generate();
735 let agent = AgentKey::issue(&user, meta.clone());
736 let cert = agent.cert();
737 assert_eq!(
738 cert.metadata.expires_at.as_deref(),
739 Some("2027-01-01T00:00:00Z")
740 );
741 let json = serde_json::to_string(cert).unwrap();
742 let parsed: CertChain = serde_json::from_str(&json).unwrap();
743 parsed.verify().unwrap();
744 }
745
746 fn meta_exec(role: &str, cmds: &[&str]) -> AgentMetadata {
750 AgentMetadata {
751 caveats: Caveats {
752 exec: crate::Scope::only(cmds.iter().map(|s| s.to_string())),
753 ..Caveats::top()
754 },
755 ..fixture_metadata(role)
756 }
757 }
758
759 #[test]
760 fn delegate_accepts_attenuation_and_roots_at_user() {
761 let user = UserKey::generate();
762 let parent = AgentKey::issue(&user, meta_exec("parent", &["git", "cargo"]));
763 let child = parent
765 .delegate(meta_exec("child", &["git"]))
766 .expect("attenuating delegation succeeds");
767 child.cert().verify().expect("delegated cert verifies");
768 assert_eq!(child.cert().user_fingerprint(), user.fingerprint());
769 assert_eq!(child.cert().root_user_pubkey(), user.public());
770 }
771
772 #[test]
773 fn delegate_rejects_amplification() {
774 let user = UserKey::generate();
775 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
776 let child = AgentMetadata {
778 caveats: Caveats::top(),
779 ..fixture_metadata("child")
780 };
781 assert!(matches!(
782 parent.delegate(child),
783 Err(MeshError::CaveatAmplification)
784 ));
785 }
786
787 #[test]
788 fn multi_level_delegation_attenuates_each_link() {
789 let user = UserKey::generate();
790 let a = AgentKey::issue(&user, meta_exec("a", &["git", "cargo"]));
791 let b = a.delegate(meta_exec("b", &["git"])).expect("B ⊑ A");
792 b.cert().verify().expect("B verifies through the chain");
793 assert_eq!(b.cert().user_fingerprint(), user.fingerprint());
794 assert!(matches!(
796 b.delegate(meta_exec("c", &["git", "rm"])),
797 Err(MeshError::CaveatAmplification)
798 ));
799 }
800
801 #[test]
802 fn delegated_cert_serde_roundtrips() {
803 let user = UserKey::generate();
804 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
805 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
806 let json = serde_json::to_string(child.cert()).unwrap();
807 let parsed: CertChain = serde_json::from_str(&json).unwrap();
808 assert_eq!(&parsed, child.cert());
809 parsed
810 .verify()
811 .expect("roundtripped delegated cert verifies");
812 }
813
814 #[test]
815 fn forged_amplifying_chain_fails_verify() {
816 let user = UserKey::generate();
820 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
821
822 let mut csprng = OsRng;
825 let child_signing = SigningKey::generate(&mut csprng);
826 let child_pubkey: [u8; 32] = *child_signing.verifying_key().as_bytes();
827 let child_meta = AgentMetadata {
828 caveats: Caveats::top(),
829 ..fixture_metadata("child")
830 };
831 let to_sign = sign_payload(&child_pubkey, &child_meta);
832 let sig = parent.sign(&to_sign); let forged = CertChain {
834 agent_pubkey: child_pubkey,
835 metadata: child_meta,
836 issuer: Issuer::Agent {
837 pubkey: parent.public_bytes(),
838 parent: Box::new(parent.cert().clone()),
839 },
840 issuer_sig: SerdeSig(sig),
841 };
842 assert!(matches!(
843 forged.verify(),
844 Err(MeshError::CaveatAmplification)
845 ));
846 }
847
848 #[test]
856 fn delegate_external_certifies_phone_and_roots_at_user() {
857 use crate::envelope::{Recipient, SignedEnvelope};
858
859 let user = UserKey::generate();
860 let worker = AgentKey::issue(&user, meta_exec("worker", &["git", "cargo"]));
861
862 let phone_seed = [42u8; 32];
864 let phone_signing = SigningKey::from_bytes(&phone_seed);
865 let phone_vk = phone_signing.verifying_key();
866
867 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
869 let proof = phone_signing.sign(&challenge.signing_bytes());
870 let phone_cert = worker
872 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
873 .expect("external delegation attenuates");
874
875 assert_eq!(phone_cert.agent_pubkey, *phone_vk.as_bytes());
877 phone_cert.verify().expect("external cert verifies");
878 assert_eq!(phone_cert.root_user_pubkey(), user.public());
879 assert_eq!(phone_cert.user_fingerprint(), user.fingerprint());
880
881 let phone =
883 AgentKey::from_seed_and_cert(&phone_seed, phone_cert).expect("phone seed matches cert");
884
885 let env = SignedEnvelope::new(
887 &phone,
888 Recipient::Topic {
889 name: "drake/work".into(),
890 },
891 1,
892 b"from-the-phone".to_vec(),
893 );
894 env.verify().expect("phone envelope verifies");
895 assert_eq!(env.sender_user_fp(), user.fingerprint());
896
897 assert!(phone
899 .cert()
900 .metadata
901 .caveats
902 .leq(&worker.cert().metadata.caveats));
903 }
904
905 #[test]
909 fn delegate_external_rejects_wrong_pubkey() {
910 let user = UserKey::generate();
911 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
912
913 let phone_seed = [42u8; 32];
914 let phone_signing = SigningKey::from_bytes(&phone_seed);
915 let phone_vk = phone_signing.verifying_key();
916 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
917 let proof = phone_signing.sign(&challenge.signing_bytes());
918 let cert = worker
919 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
920 .unwrap();
921
922 let other_seed = [99u8; 32];
924 assert!(matches!(
925 AgentKey::from_seed_and_cert(&other_seed, cert.clone()),
926 Err(MeshError::BadSignature)
927 ));
928
929 let mut tampered = cert;
931 tampered.agent_pubkey[0] ^= 0xff;
932 assert!(matches!(
933 tampered.verify(),
934 Err(MeshError::BadSignature) | Err(MeshError::CaveatAmplification)
935 ));
936 }
937
938 #[test]
941 fn delegate_external_rejects_amplification() {
942 let user = UserKey::generate();
943 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
944 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
945 let phone_vk = phone_signing.verifying_key();
946 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
947 let proof = phone_signing.sign(&challenge.signing_bytes());
948 let amplifying = AgentMetadata {
951 caveats: Caveats::top(),
952 ..fixture_metadata("phone")
953 };
954 assert!(matches!(
955 worker.delegate_external(&challenge, &proof, amplifying),
956 Err(MeshError::CaveatAmplification)
957 ));
958 }
959
960 #[test]
964 fn delegate_external_requires_proof_of_possession() {
965 let user = UserKey::generate();
966 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
967 let victim_vk = SigningKey::from_bytes(&[7u8; 32]).verifying_key();
968 let challenge = worker.possession_challenge(*victim_vk.as_bytes());
969 let attacker = SigningKey::from_bytes(&[9u8; 32]);
971 let forged = attacker.sign(&challenge.signing_bytes());
972 assert!(matches!(
973 worker.delegate_external(&challenge, &forged, meta_exec("v", &["git"])),
974 Err(MeshError::BadSignature)
975 ));
976 }
977
978 #[test]
981 fn delegate_external_rejects_a_foreign_challenge() {
982 let user = UserKey::generate();
983 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
984 let other = AgentKey::issue(&user, meta_exec("other", &["git"]));
985 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
986 let phone_vk = phone_signing.verifying_key();
987 let foreign = other.possession_challenge(*phone_vk.as_bytes());
989 let proof = phone_signing.sign(&foreign.signing_bytes());
990 assert!(matches!(
991 worker.delegate_external(&foreign, &proof, meta_exec("phone", &["git"])),
992 Err(MeshError::InvalidCertChain(_))
993 ));
994 }
995
996 #[test]
997 fn delegated_issuer_pubkey_must_match_parent() {
998 let user = UserKey::generate();
1001 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
1002 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
1003 let mut cert = child.cert().clone();
1004 if let Issuer::Agent { pubkey, .. } = &mut cert.issuer {
1005 pubkey[0] ^= 0xff;
1006 }
1007 assert!(matches!(cert.verify(), Err(MeshError::InvalidCertChain(_))));
1008 }
1009}