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 #[serde(with = "serde_bytes")]
33 pub issuer_pubkey: [u8; 32],
34 #[serde(with = "serde_bytes")]
36 pub subject_pubkey: [u8; 32],
37 #[serde(with = "serde_bytes")]
39 pub nonce: [u8; 32],
40}
41
42impl PossessionChallenge {
43 #[must_use]
47 pub fn signing_bytes(&self) -> Vec<u8> {
48 let mut v = Vec::with_capacity(POP_DOMAIN.len() + 96);
49 v.extend_from_slice(POP_DOMAIN);
50 v.extend_from_slice(&self.issuer_pubkey);
51 v.extend_from_slice(&self.subject_pubkey);
52 v.extend_from_slice(&self.nonce);
53 v
54 }
55}
56
57pub struct AgentKey {
64 signing: SigningKey,
65 cert: CertChain,
66}
67
68impl AgentKey {
69 pub fn issue(user: &UserKey, metadata: AgentMetadata) -> Self {
76 Self::issue_with(user, SigningKey::generate(&mut OsRng), metadata)
77 }
78
79 pub fn issue_derived(user: &UserKey, label: &str, metadata: AgentMetadata) -> Self {
97 let mut seed = user.derived_agent_seed(label);
98 let signing = SigningKey::from_bytes(&seed);
99 seed.zeroize();
100 Self::issue_with(user, signing, metadata)
101 }
102
103 fn issue_with(user: &UserKey, signing: SigningKey, metadata: AgentMetadata) -> Self {
104 let agent_pubkey_bytes: [u8; 32] = *signing.verifying_key().as_bytes();
105
106 let to_sign = sign_payload(&agent_pubkey_bytes, &metadata);
107 let sig = user.sign(&to_sign);
108
109 let cert = CertChain {
110 agent_pubkey: agent_pubkey_bytes,
111 metadata,
112 issuer: Issuer::User(user.public()),
113 issuer_sig: SerdeSig(sig),
114 };
115 Self { signing, cert }
116 }
117
118 pub fn delegate(&self, metadata: AgentMetadata) -> Result<Self> {
128 let mut csprng = OsRng;
129 let signing = SigningKey::generate(&mut csprng);
130 let sub_pubkey: [u8; 32] = *signing.verifying_key().as_bytes();
131 let cert = self.certify_pubkey(sub_pubkey, metadata)?;
132 Ok(Self { signing, cert })
133 }
134
135 #[must_use]
141 pub fn possession_challenge(&self, subject_pubkey: [u8; 32]) -> PossessionChallenge {
142 let mut nonce = [0u8; 32];
143 OsRng.fill_bytes(&mut nonce);
144 PossessionChallenge {
145 issuer_pubkey: self.cert.agent_pubkey,
146 subject_pubkey,
147 nonce,
148 }
149 }
150
151 pub fn delegate_external(
177 &self,
178 challenge: &PossessionChallenge,
179 proof: &Signature,
180 metadata: AgentMetadata,
181 ) -> Result<CertChain> {
182 if challenge.issuer_pubkey != self.cert.agent_pubkey {
184 return Err(MeshError::InvalidCertChain(
185 "possession challenge was not issued by this agent".into(),
186 ));
187 }
188 verify_detached(&challenge.subject_pubkey, &challenge.signing_bytes(), proof)?;
192 self.certify_pubkey(challenge.subject_pubkey, metadata)
193 }
194
195 fn certify_pubkey(&self, pubkey: [u8; 32], metadata: AgentMetadata) -> Result<CertChain> {
200 if !metadata.caveats.leq(&self.cert.metadata.caveats) {
201 return Err(MeshError::CaveatAmplification);
202 }
203 let to_sign = sign_payload(&pubkey, &metadata);
204 let sig = self.signing.sign(&to_sign);
205 Ok(CertChain {
206 agent_pubkey: pubkey,
207 metadata,
208 issuer: Issuer::Agent {
209 pubkey: self.cert.agent_pubkey,
210 parent: Box::new(self.cert.clone()),
211 },
212 issuer_sig: SerdeSig(sig),
213 })
214 }
215
216 pub fn sign(&self, message: &[u8]) -> Signature {
218 self.signing.sign(message)
219 }
220
221 #[must_use]
223 pub fn fingerprint(&self) -> Fingerprint {
224 Fingerprint::of_bytes(&self.cert.agent_pubkey)
225 }
226
227 #[must_use]
229 pub fn cert(&self) -> &CertChain {
230 &self.cert
231 }
232
233 #[must_use]
235 pub fn public_bytes(&self) -> [u8; 32] {
236 self.cert.agent_pubkey
237 }
238
239 #[must_use]
248 pub fn signing_key_bytes(&self) -> [u8; 32] {
249 self.signing.to_bytes()
250 }
251
252 pub fn from_seed_and_cert(seed: &[u8; 32], cert: CertChain) -> Result<Self> {
263 let signing = ed25519_dalek::SigningKey::from_bytes(seed);
264 let derived_pub: [u8; 32] = *signing.verifying_key().as_bytes();
265 if derived_pub != cert.agent_pubkey {
266 return Err(MeshError::BadSignature);
267 }
268 Ok(Self { signing, cert })
269 }
270
271 #[must_use]
273 pub fn verifying_key(&self) -> VerifyingKey {
274 self.signing.verifying_key()
275 }
276}
277
278impl crate::signer::MeshSigner for AgentKey {
283 fn verifying_key(&self) -> VerifyingKey {
284 self.signing.verifying_key()
285 }
286
287 fn sign(&self, msg: &[u8]) -> Signature {
288 self.signing.sign(msg)
289 }
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296pub struct AgentMetadata {
297 pub role: String,
299 pub host: String,
301 pub capabilities: Vec<String>,
303 pub issued_at: String,
307 pub expires_at: Option<String>,
310 #[serde(default)]
321 pub caveats: Caveats,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326pub enum Issuer {
327 User(UserPublic),
329 Agent {
333 #[serde(with = "serde_bytes")]
336 pubkey: [u8; 32],
337 parent: Box<CertChain>,
339 },
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
345pub struct CertChain {
346 #[serde(with = "serde_bytes")]
347 pub agent_pubkey: [u8; 32],
348 pub metadata: AgentMetadata,
349 pub issuer: Issuer,
351 pub issuer_sig: SerdeSig,
353}
354
355impl CertChain {
356 pub fn verify(&self) -> Result<()> {
368 self.verify_inner(None)
369 }
370
371 pub fn verify_at(&self, current_generation: u64) -> Result<()> {
378 self.verify_inner(Some(current_generation))
379 }
380
381 fn verify_inner(&self, current_generation: Option<u64>) -> Result<()> {
387 check_generation(
389 &self.metadata.caveats.valid_for_generation,
390 current_generation,
391 )?;
392 let to_verify = sign_payload(&self.agent_pubkey, &self.metadata);
393 match &self.issuer {
394 Issuer::User(user) => {
395 user.verify(&to_verify, &self.issuer_sig.0)
398 }
399 Issuer::Agent { pubkey, parent } => {
400 if *pubkey != parent.agent_pubkey {
401 return Err(MeshError::InvalidCertChain(
402 "delegated cert issuer pubkey does not match its parent".into(),
403 ));
404 }
405 parent.verify_inner(current_generation)?;
406 verify_detached(pubkey, &to_verify, &self.issuer_sig.0)?;
407 if !self.metadata.caveats.leq(&parent.metadata.caveats) {
408 return Err(MeshError::CaveatAmplification);
409 }
410 Ok(())
411 }
412 }
413 }
414
415 #[must_use]
417 pub fn agent_fingerprint(&self) -> Fingerprint {
418 Fingerprint::of_bytes(&self.agent_pubkey)
419 }
420
421 #[must_use]
424 pub fn user_fingerprint(&self) -> Fingerprint {
425 match &self.issuer {
426 Issuer::User(user) => user.fingerprint(),
427 Issuer::Agent { parent, .. } => parent.user_fingerprint(),
428 }
429 }
430
431 #[must_use]
433 pub fn root_user_pubkey(&self) -> UserPublic {
434 match &self.issuer {
435 Issuer::User(user) => user.clone(),
436 Issuer::Agent { parent, .. } => parent.root_user_pubkey(),
437 }
438 }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct SerdeSig(pub Signature);
445
446impl Serialize for SerdeSig {
447 fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
448 serde_bytes::Bytes::new(&self.0.to_bytes()).serialize(ser)
450 }
451}
452
453impl<'de> Deserialize<'de> for SerdeSig {
454 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
455 let bytes = serde_bytes::ByteBuf::deserialize(de)?;
456 if bytes.len() != 64 {
457 return Err(serde::de::Error::custom("expected 64-byte signature"));
458 }
459 let mut arr = [0u8; 64];
460 arr.copy_from_slice(&bytes);
461 Ok(Self(Signature::from_bytes(&arr)))
462 }
463}
464
465fn sign_payload(agent_pubkey: &[u8; 32], metadata: &AgentMetadata) -> Vec<u8> {
467 let meta_bytes =
468 serde_json::to_vec(metadata).expect("AgentMetadata serializes deterministically");
469 let mut out = Vec::with_capacity(32 + meta_bytes.len());
470 out.extend_from_slice(agent_pubkey);
471 out.extend_from_slice(&meta_bytes);
472 out
473}
474
475fn check_generation(scope: &Scope<u64>, current: Option<u64>) -> Result<()> {
480 match scope {
481 Scope::All => Ok(()),
482 Scope::Only(gens) => match current {
483 None => Err(MeshError::Generation(
484 "cert is generation-scoped; verify with a current generation via verify_at()"
485 .into(),
486 )),
487 Some(g) if gens.contains(&g) => Ok(()),
488 Some(g) => Err(MeshError::Generation(format!(
489 "cert is not valid for generation {g}"
490 ))),
491 },
492 }
493}
494
495fn verify_detached(pubkey: &[u8; 32], msg: &[u8], sig: &Signature) -> Result<()> {
498 let vk = VerifyingKey::from_bytes(pubkey).map_err(|_| MeshError::BadSignature)?;
499 vk.verify_strict(msg, sig)
500 .map_err(|_| MeshError::BadSignature)
501}
502
503impl MeshError {
504 #[cfg(test)]
506 pub(crate) fn is_bad_signature(&self) -> bool {
507 matches!(self, Self::BadSignature)
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn fixture_metadata(role: &str) -> AgentMetadata {
516 AgentMetadata {
517 role: role.to_string(),
518 host: "test-host".to_string(),
519 capabilities: vec!["test".to_string()],
520 issued_at: "2026-05-28T12:00:00Z".to_string(),
521 expires_at: None,
522 caveats: Caveats::top(),
523 }
524 }
525
526 #[test]
527 fn issue_agent_key_signed_by_user() {
528 let user = UserKey::generate();
529 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
530 assert_eq!(agent.cert().root_user_pubkey(), user.public());
531 assert_eq!(agent.cert().agent_pubkey, agent.public_bytes());
532 }
533
534 #[test]
538 fn issue_derived_is_stable_per_user_and_label() {
539 let user = UserKey::generate();
540 let a = AgentKey::issue_derived(&user, "dock/hub", fixture_metadata("hub"));
541 let again = AgentKey::issue_derived(&user, "dock/hub", fixture_metadata("other-role"));
542 assert_eq!(
543 a.public_bytes(),
544 again.public_bytes(),
545 "same label, same key"
546 );
547 assert_eq!(a.fingerprint(), again.fingerprint());
548
549 let other_label = AgentKey::issue_derived(&user, "dock/host", fixture_metadata("hub"));
550 assert_ne!(
551 a.public_bytes(),
552 other_label.public_bytes(),
553 "label separates keys"
554 );
555 let other_user =
556 AgentKey::issue_derived(&UserKey::generate(), "dock/hub", fixture_metadata("hub"));
557 assert_ne!(
558 a.public_bytes(),
559 other_user.public_bytes(),
560 "user separates keys"
561 );
562 assert_ne!(
563 a.public_bytes(),
564 user.public().as_bytes(),
565 "not the user key"
566 );
567
568 a.cert().verify().expect("derived cert verifies");
569 assert_eq!(a.cert().root_user_pubkey(), user.public());
570 assert_eq!(a.cert().agent_pubkey, a.public_bytes());
571 }
572
573 #[test]
574 fn verify_cert_chain_succeeds() {
575 let user = UserKey::generate();
576 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
577 agent.cert().verify().expect("fresh cert verifies");
578 }
579
580 #[test]
583 fn unbounded_generation_verifies_in_any_context() {
584 let user = UserKey::generate();
585 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
586 agent.cert().verify().expect("context-free ok");
587 agent.cert().verify_at(0).expect("gen 0 ok");
588 agent.cert().verify_at(9_999).expect("any gen ok");
589 }
590
591 #[test]
595 fn generation_scoped_cert_is_fail_closed_context_free() {
596 let mut meta = fixture_metadata("worker");
597 meta.caveats = Caveats {
598 valid_for_generation: crate::Scope::only([5u64]),
599 ..Caveats::top()
600 };
601 let user = UserKey::generate();
602 let agent = AgentKey::issue(&user, meta);
603
604 assert!(matches!(
606 agent.cert().verify().unwrap_err(),
607 MeshError::Generation(_)
608 ));
609 agent.cert().verify_at(5).expect("valid for generation 5");
611 assert!(matches!(
613 agent.cert().verify_at(6).unwrap_err(),
614 MeshError::Generation(_)
615 ));
616 }
617
618 #[test]
622 fn generation_gate_applies_across_the_chain() {
623 let scoped = |role: &str| AgentMetadata {
624 caveats: Caveats {
625 valid_for_generation: crate::Scope::only([5u64]),
626 ..Caveats::top()
627 },
628 ..fixture_metadata(role)
629 };
630 let user = UserKey::generate();
631 let parent = AgentKey::issue(&user, scoped("lead"));
632 let child = parent
633 .delegate(scoped("worker"))
634 .expect("attenuating delegate");
635
636 assert!(matches!(
637 child.cert().verify().unwrap_err(),
638 MeshError::Generation(_)
639 ));
640 child
641 .cert()
642 .verify_at(5)
643 .expect("chain valid for generation 5");
644 assert!(matches!(
645 child.cert().verify_at(6).unwrap_err(),
646 MeshError::Generation(_)
647 ));
648 }
649
650 #[test]
651 fn tampered_metadata_fails_verify() {
652 let user = UserKey::generate();
653 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
654 let mut cert = agent.cert().clone();
655 cert.metadata.role = "evil".to_string();
656 assert!(cert.verify().unwrap_err().is_bad_signature());
657 }
658
659 #[test]
661 fn fixture_caveats_default_to_top() {
662 assert_eq!(fixture_metadata("worker").caveats, Caveats::top());
663 }
664
665 #[test]
668 fn bounded_caveats_roundtrip_and_verify() {
669 let mut meta = fixture_metadata("worker");
670 meta.caveats = Caveats {
671 exec: crate::Scope::only(["git".to_string()]),
672 max_calls: crate::CountBound::AtMost(8),
673 ..Caveats::top()
674 };
675 let user = UserKey::generate();
676 let agent = AgentKey::issue(&user, meta.clone());
677 agent.cert().verify().expect("fresh cert verifies");
678
679 let json = serde_json::to_string(agent.cert()).unwrap();
680 let parsed: CertChain = serde_json::from_str(&json).unwrap();
681 assert_eq!(parsed.metadata.caveats, meta.caveats);
682 parsed.verify().expect("roundtripped cert verifies");
683 }
684
685 #[test]
688 fn tampered_caveats_fails_verify() {
689 let mut meta = fixture_metadata("worker");
690 meta.caveats = Caveats {
691 exec: crate::Scope::only(["git".to_string()]),
692 ..Caveats::top()
693 };
694 let user = UserKey::generate();
695 let agent = AgentKey::issue(&user, meta);
696 let mut cert = agent.cert().clone();
697 cert.metadata.caveats = Caveats::top(); assert!(cert.verify().unwrap_err().is_bad_signature());
699 }
700
701 #[test]
704 fn absent_caveats_default_to_top() {
705 let json = r#"{"role":"w","host":"h","capabilities":[],"issued_at":"2026-05-28T00:00:00Z","expires_at":null}"#;
706 let meta: AgentMetadata = serde_json::from_str(json).unwrap();
707 assert_eq!(meta.caveats, Caveats::top());
708 }
709
710 #[test]
711 fn tampered_agent_pubkey_fails_verify() {
712 let user = UserKey::generate();
713 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
714 let mut cert = agent.cert().clone();
715 cert.agent_pubkey[0] ^= 0xff;
716 assert!(cert.verify().unwrap_err().is_bad_signature());
717 }
718
719 #[test]
720 fn wrong_user_fails_verify() {
721 let user = UserKey::generate();
722 let other = UserKey::generate();
723 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
724 let mut cert = agent.cert().clone();
725 cert.issuer = Issuer::User(other.public());
726 assert!(cert.verify().unwrap_err().is_bad_signature());
727 }
728
729 #[test]
730 fn serde_roundtrip_cert_chain() {
731 let user = UserKey::generate();
732 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
733 let json = serde_json::to_string(agent.cert()).unwrap();
734 let parsed: CertChain = serde_json::from_str(&json).unwrap();
735 assert_eq!(&parsed, agent.cert());
736 parsed.verify().expect("roundtripped cert still verifies");
737 }
738
739 #[test]
740 fn fingerprints_match() {
741 let user = UserKey::generate();
742 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
743 let cert = agent.cert();
744 assert_eq!(agent.fingerprint(), cert.agent_fingerprint());
745 assert_eq!(cert.user_fingerprint(), user.fingerprint());
746 }
747
748 #[test]
749 fn agent_sign_distinct_from_user_sign() {
750 let user = UserKey::generate();
751 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
752 let user_sig = user.sign(b"x");
753 let agent_sig = agent.sign(b"x");
754 assert_ne!(user_sig.to_bytes(), agent_sig.to_bytes());
756 }
757
758 #[test]
759 fn signing_key_bytes_roundtrip_signs_identically() {
760 use ed25519_dalek::Signer;
761 let user = UserKey::generate();
762 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
763 let bytes = agent.signing_key_bytes();
764 assert_eq!(bytes.len(), 32);
765 let rebuilt = ed25519_dalek::SigningKey::from_bytes(&bytes);
768 let msg = b"transport-layer-handshake";
769 let from_agent = agent.sign(msg);
770 let from_rebuilt = rebuilt.sign(msg);
771 assert_eq!(from_agent.to_bytes(), from_rebuilt.to_bytes());
772 }
773
774 #[test]
775 fn from_seed_and_cert_roundtrips() {
776 let user = UserKey::generate();
777 let agent = AgentKey::issue(&user, fixture_metadata("worker"));
778 let seed = agent.signing_key_bytes();
779 let cert = agent.cert().clone();
780 let rebuilt = AgentKey::from_seed_and_cert(&seed, cert).expect("seed+cert valid");
781 assert_eq!(rebuilt.fingerprint(), agent.fingerprint());
782 let msg = b"rebuild-test";
784 assert_eq!(rebuilt.sign(msg).to_bytes(), agent.sign(msg).to_bytes());
785 }
786
787 #[test]
788 fn from_seed_and_cert_rejects_mismatched_pairing() {
789 let user = UserKey::generate();
790 let agent_a = AgentKey::issue(&user, fixture_metadata("a"));
791 let agent_b = AgentKey::issue(&user, fixture_metadata("b"));
792 let res =
794 AgentKey::from_seed_and_cert(&agent_b.signing_key_bytes(), agent_a.cert().clone());
795 match res {
796 Ok(_) => panic!("mismatched pairing must reject"),
797 Err(e) => assert!(matches!(e, MeshError::BadSignature)),
798 }
799 }
800
801 #[test]
802 fn metadata_with_expiry_roundtrips() {
803 let mut meta = fixture_metadata("worker");
804 meta.expires_at = Some("2027-01-01T00:00:00Z".to_string());
805 let user = UserKey::generate();
806 let agent = AgentKey::issue(&user, meta.clone());
807 let cert = agent.cert();
808 assert_eq!(
809 cert.metadata.expires_at.as_deref(),
810 Some("2027-01-01T00:00:00Z")
811 );
812 let json = serde_json::to_string(cert).unwrap();
813 let parsed: CertChain = serde_json::from_str(&json).unwrap();
814 parsed.verify().unwrap();
815 }
816
817 fn meta_exec(role: &str, cmds: &[&str]) -> AgentMetadata {
821 AgentMetadata {
822 caveats: Caveats {
823 exec: crate::Scope::only(cmds.iter().map(|s| s.to_string())),
824 ..Caveats::top()
825 },
826 ..fixture_metadata(role)
827 }
828 }
829
830 #[test]
831 fn delegate_accepts_attenuation_and_roots_at_user() {
832 let user = UserKey::generate();
833 let parent = AgentKey::issue(&user, meta_exec("parent", &["git", "cargo"]));
834 let child = parent
836 .delegate(meta_exec("child", &["git"]))
837 .expect("attenuating delegation succeeds");
838 child.cert().verify().expect("delegated cert verifies");
839 assert_eq!(child.cert().user_fingerprint(), user.fingerprint());
840 assert_eq!(child.cert().root_user_pubkey(), user.public());
841 }
842
843 #[test]
844 fn delegate_rejects_amplification() {
845 let user = UserKey::generate();
846 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
847 let child = AgentMetadata {
849 caveats: Caveats::top(),
850 ..fixture_metadata("child")
851 };
852 assert!(matches!(
853 parent.delegate(child),
854 Err(MeshError::CaveatAmplification)
855 ));
856 }
857
858 #[test]
859 fn multi_level_delegation_attenuates_each_link() {
860 let user = UserKey::generate();
861 let a = AgentKey::issue(&user, meta_exec("a", &["git", "cargo"]));
862 let b = a.delegate(meta_exec("b", &["git"])).expect("B ⊑ A");
863 b.cert().verify().expect("B verifies through the chain");
864 assert_eq!(b.cert().user_fingerprint(), user.fingerprint());
865 assert!(matches!(
867 b.delegate(meta_exec("c", &["git", "rm"])),
868 Err(MeshError::CaveatAmplification)
869 ));
870 }
871
872 #[test]
873 fn delegated_cert_serde_roundtrips() {
874 let user = UserKey::generate();
875 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
876 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
877 let json = serde_json::to_string(child.cert()).unwrap();
878 let parsed: CertChain = serde_json::from_str(&json).unwrap();
879 assert_eq!(&parsed, child.cert());
880 parsed
881 .verify()
882 .expect("roundtripped delegated cert verifies");
883 }
884
885 #[test]
886 fn forged_amplifying_chain_fails_verify() {
887 let user = UserKey::generate();
891 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
892
893 let mut csprng = OsRng;
896 let child_signing = SigningKey::generate(&mut csprng);
897 let child_pubkey: [u8; 32] = *child_signing.verifying_key().as_bytes();
898 let child_meta = AgentMetadata {
899 caveats: Caveats::top(),
900 ..fixture_metadata("child")
901 };
902 let to_sign = sign_payload(&child_pubkey, &child_meta);
903 let sig = parent.sign(&to_sign); let forged = CertChain {
905 agent_pubkey: child_pubkey,
906 metadata: child_meta,
907 issuer: Issuer::Agent {
908 pubkey: parent.public_bytes(),
909 parent: Box::new(parent.cert().clone()),
910 },
911 issuer_sig: SerdeSig(sig),
912 };
913 assert!(matches!(
914 forged.verify(),
915 Err(MeshError::CaveatAmplification)
916 ));
917 }
918
919 #[test]
927 fn delegate_external_certifies_phone_and_roots_at_user() {
928 use crate::envelope::{Recipient, SignedEnvelope};
929
930 let user = UserKey::generate();
931 let worker = AgentKey::issue(&user, meta_exec("worker", &["git", "cargo"]));
932
933 let phone_seed = [42u8; 32];
935 let phone_signing = SigningKey::from_bytes(&phone_seed);
936 let phone_vk = phone_signing.verifying_key();
937
938 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
940 let proof = phone_signing.sign(&challenge.signing_bytes());
941 let phone_cert = worker
943 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
944 .expect("external delegation attenuates");
945
946 assert_eq!(phone_cert.agent_pubkey, *phone_vk.as_bytes());
948 phone_cert.verify().expect("external cert verifies");
949 assert_eq!(phone_cert.root_user_pubkey(), user.public());
950 assert_eq!(phone_cert.user_fingerprint(), user.fingerprint());
951
952 let phone =
954 AgentKey::from_seed_and_cert(&phone_seed, phone_cert).expect("phone seed matches cert");
955
956 let env = SignedEnvelope::new(
958 &phone,
959 Recipient::Topic {
960 name: "drake/work".into(),
961 },
962 1,
963 b"from-the-phone".to_vec(),
964 );
965 env.verify().expect("phone envelope verifies");
966 assert_eq!(env.sender_user_fp(), user.fingerprint());
967
968 assert!(phone
970 .cert()
971 .metadata
972 .caveats
973 .leq(&worker.cert().metadata.caveats));
974 }
975
976 #[test]
980 fn delegate_external_rejects_wrong_pubkey() {
981 let user = UserKey::generate();
982 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
983
984 let phone_seed = [42u8; 32];
985 let phone_signing = SigningKey::from_bytes(&phone_seed);
986 let phone_vk = phone_signing.verifying_key();
987 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
988 let proof = phone_signing.sign(&challenge.signing_bytes());
989 let cert = worker
990 .delegate_external(&challenge, &proof, meta_exec("phone", &["git"]))
991 .unwrap();
992
993 let other_seed = [99u8; 32];
995 assert!(matches!(
996 AgentKey::from_seed_and_cert(&other_seed, cert.clone()),
997 Err(MeshError::BadSignature)
998 ));
999
1000 let mut tampered = cert;
1002 tampered.agent_pubkey[0] ^= 0xff;
1003 assert!(matches!(
1004 tampered.verify(),
1005 Err(MeshError::BadSignature) | Err(MeshError::CaveatAmplification)
1006 ));
1007 }
1008
1009 #[test]
1012 fn delegate_external_rejects_amplification() {
1013 let user = UserKey::generate();
1014 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1015 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
1016 let phone_vk = phone_signing.verifying_key();
1017 let challenge = worker.possession_challenge(*phone_vk.as_bytes());
1018 let proof = phone_signing.sign(&challenge.signing_bytes());
1019 let amplifying = AgentMetadata {
1022 caveats: Caveats::top(),
1023 ..fixture_metadata("phone")
1024 };
1025 assert!(matches!(
1026 worker.delegate_external(&challenge, &proof, amplifying),
1027 Err(MeshError::CaveatAmplification)
1028 ));
1029 }
1030
1031 #[test]
1035 fn delegate_external_requires_proof_of_possession() {
1036 let user = UserKey::generate();
1037 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1038 let victim_vk = SigningKey::from_bytes(&[7u8; 32]).verifying_key();
1039 let challenge = worker.possession_challenge(*victim_vk.as_bytes());
1040 let attacker = SigningKey::from_bytes(&[9u8; 32]);
1042 let forged = attacker.sign(&challenge.signing_bytes());
1043 assert!(matches!(
1044 worker.delegate_external(&challenge, &forged, meta_exec("v", &["git"])),
1045 Err(MeshError::BadSignature)
1046 ));
1047 }
1048
1049 #[test]
1052 fn delegate_external_rejects_a_foreign_challenge() {
1053 let user = UserKey::generate();
1054 let worker = AgentKey::issue(&user, meta_exec("worker", &["git"]));
1055 let other = AgentKey::issue(&user, meta_exec("other", &["git"]));
1056 let phone_signing = SigningKey::from_bytes(&[42u8; 32]);
1057 let phone_vk = phone_signing.verifying_key();
1058 let foreign = other.possession_challenge(*phone_vk.as_bytes());
1060 let proof = phone_signing.sign(&foreign.signing_bytes());
1061 assert!(matches!(
1062 worker.delegate_external(&foreign, &proof, meta_exec("phone", &["git"])),
1063 Err(MeshError::InvalidCertChain(_))
1064 ));
1065 }
1066
1067 #[test]
1068 fn delegated_issuer_pubkey_must_match_parent() {
1069 let user = UserKey::generate();
1072 let parent = AgentKey::issue(&user, meta_exec("parent", &["git"]));
1073 let child = parent.delegate(meta_exec("child", &["git"])).unwrap();
1074 let mut cert = child.cert().clone();
1075 if let Issuer::Agent { pubkey, .. } = &mut cert.issuer {
1076 pubkey[0] ^= 0xff;
1077 }
1078 assert!(matches!(cert.verify(), Err(MeshError::InvalidCertChain(_))));
1079 }
1080}