1use crate::agent_key::{AgentKey, CertChain, SerdeSig};
8use crate::fingerprint::Fingerprint;
9use crate::{MeshError, Result};
10use ed25519_dalek::{Verifier, VerifyingKey};
11use rand::RngCore;
12use serde::{Deserialize, Serialize};
13use serde_bytes::ByteBuf;
14
15const ENVELOPE_TAG: &[u8] = b"agent-mesh-envelope-v1";
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(tag = "kind", rename_all = "snake_case")]
21pub enum Recipient {
22 Direct { agent_fp: Fingerprint },
24 Topic { name: String },
27 Anycast { capability: String },
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct SignedEnvelope {
45 pub cert_chain: CertChain,
46 pub recipient: Recipient,
47 pub nonce: [u8; 24],
48 pub sequence: u64,
49 pub payload_cid: [u8; 32],
50 pub payload: ByteBuf,
51 pub agent_sig: SerdeSig,
52}
53
54impl SignedEnvelope {
55 pub fn new(sender: &AgentKey, recipient: Recipient, sequence: u64, payload: Vec<u8>) -> Self {
61 let mut nonce = [0u8; 24];
62 rand::thread_rng().fill_bytes(&mut nonce);
63 let payload_cid: [u8; 32] = *blake3::hash(&payload).as_bytes();
64
65 let to_sign = signing_message(&recipient, &nonce, sequence, &payload_cid);
66 let sig = sender.sign(&to_sign);
67
68 Self {
69 cert_chain: sender.cert().clone(),
70 recipient,
71 nonce,
72 sequence,
73 payload_cid,
74 payload: ByteBuf::from(payload),
75 agent_sig: SerdeSig(sig),
76 }
77 }
78
79 pub fn verify(&self) -> Result<()> {
86 self.cert_chain.verify()?;
87
88 let actual_cid: [u8; 32] = *blake3::hash(&self.payload).as_bytes();
89 if actual_cid != self.payload_cid {
90 return Err(MeshError::MalformedEnvelope("payload_cid mismatch".into()));
91 }
92
93 let agent_vk = VerifyingKey::from_bytes(&self.cert_chain.agent_pubkey)
94 .map_err(|e| MeshError::InvalidKey(e.to_string()))?;
95 let to_verify = signing_message(
96 &self.recipient,
97 &self.nonce,
98 self.sequence,
99 &self.payload_cid,
100 );
101 agent_vk
102 .verify(&to_verify, &self.agent_sig.0)
103 .map_err(|_| MeshError::BadSignature)?;
104 Ok(())
105 }
106
107 #[must_use]
109 pub fn sender_agent_fp(&self) -> Fingerprint {
110 self.cert_chain.agent_fingerprint()
111 }
112
113 #[must_use]
115 pub fn sender_user_fp(&self) -> Fingerprint {
116 self.cert_chain.user_fingerprint()
117 }
118}
119
120fn signing_message(
121 recipient: &Recipient,
122 nonce: &[u8; 24],
123 sequence: u64,
124 payload_cid: &[u8; 32],
125) -> Vec<u8> {
126 let recipient_bytes =
127 serde_json::to_vec(recipient).expect("Recipient serializes deterministically");
128 let mut msg = Vec::with_capacity(ENVELOPE_TAG.len() + recipient_bytes.len() + 24 + 8 + 32);
129 msg.extend_from_slice(ENVELOPE_TAG);
130 msg.extend_from_slice(&recipient_bytes);
131 msg.extend_from_slice(nonce);
132 msg.extend_from_slice(&sequence.to_be_bytes());
133 msg.extend_from_slice(payload_cid);
134 msg
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::agent_key::AgentMetadata;
141 use crate::UserKey;
142 use std::collections::HashSet;
143
144 fn fixture_user_and_agent() -> (UserKey, AgentKey) {
145 let user = UserKey::generate();
146 let agent = AgentKey::issue(
147 &user,
148 AgentMetadata {
149 role: "worker".to_string(),
150 host: "test-host".to_string(),
151 capabilities: vec!["test".to_string()],
152 issued_at: "2026-05-28T12:00:00Z".to_string(),
153 expires_at: None,
154 caveats: crate::Caveats::top(),
155 },
156 );
157 (user, agent)
158 }
159
160 fn direct_recipient() -> Recipient {
161 Recipient::Direct {
162 agent_fp: Fingerprint::of_bytes(b"some peer"),
163 }
164 }
165
166 #[test]
167 fn roundtrip_envelope_verifies() {
168 let (_user, agent) = fixture_user_and_agent();
169 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"hello".to_vec());
170 env.verify().expect("fresh envelope verifies");
171 }
172
173 #[test]
174 fn tampered_payload_fails_verify() {
175 let (_user, agent) = fixture_user_and_agent();
176 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"original".to_vec());
177 env.payload = ByteBuf::from(b"tampered".to_vec());
178 let err = env.verify().unwrap_err();
179 match err {
180 MeshError::MalformedEnvelope(_) => {}
181 other => panic!("expected MalformedEnvelope, got {other:?}"),
182 }
183 }
184
185 #[test]
186 fn tampered_recipient_fails_verify() {
187 let (_user, agent) = fixture_user_and_agent();
188 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
189 env.recipient = Recipient::Topic {
190 name: "other".to_string(),
191 };
192 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
193 }
194
195 #[test]
196 fn tampered_sequence_fails_verify() {
197 let (_user, agent) = fixture_user_and_agent();
198 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
199 env.sequence = 999;
200 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
201 }
202
203 #[test]
204 fn tampered_nonce_fails_verify() {
205 let (_user, agent) = fixture_user_and_agent();
206 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
207 env.nonce[0] ^= 0xff;
208 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
209 }
210
211 #[test]
212 fn mismatched_payload_cid_fails() {
213 let (_user, agent) = fixture_user_and_agent();
214 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
215 env.payload_cid[0] ^= 0xff;
216 let err = env.verify().unwrap_err();
217 match err {
218 MeshError::MalformedEnvelope(_) => {}
219 other => panic!("expected MalformedEnvelope, got {other:?}"),
220 }
221 }
222
223 #[test]
224 fn serde_roundtrip_envelope() {
225 let (_user, agent) = fixture_user_and_agent();
226 let env = SignedEnvelope::new(&agent, direct_recipient(), 7, b"payload".to_vec());
227 let json = serde_json::to_string(&env).unwrap();
228 let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
229 assert_eq!(parsed, env);
230 parsed.verify().expect("roundtripped envelope verifies");
231 }
232
233 #[test]
234 fn unique_nonces_across_envelopes() {
235 let (_user, agent) = fixture_user_and_agent();
236 let mut seen = HashSet::new();
237 for i in 0..100 {
238 let env = SignedEnvelope::new(&agent, direct_recipient(), i, b"x".to_vec());
239 assert!(seen.insert(env.nonce), "duplicate nonce after {i} draws");
240 }
241 }
242
243 #[test]
244 fn sender_fingerprints_match_cert_chain() {
245 let (user, agent) = fixture_user_and_agent();
246 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
247 assert_eq!(env.sender_agent_fp(), agent.fingerprint());
248 assert_eq!(env.sender_user_fp(), user.fingerprint());
249 }
250
251 #[test]
252 fn topic_and_anycast_recipients_roundtrip() {
253 let (_user, agent) = fixture_user_and_agent();
254 for r in [
255 Recipient::Topic {
256 name: "drake/work".to_string(),
257 },
258 Recipient::Anycast {
259 capability: "ollama".to_string(),
260 },
261 ] {
262 let env = SignedEnvelope::new(&agent, r.clone(), 1, b"x".to_vec());
263 env.verify().expect("verify");
264 let json = serde_json::to_string(&env).unwrap();
265 let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
266 assert_eq!(parsed.recipient, r);
267 }
268 }
269
270 #[test]
271 fn empty_payload_is_legal() {
272 let (_user, agent) = fixture_user_and_agent();
273 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, vec![]);
274 env.verify().expect("empty payload is fine");
275 }
276}