1use crate::agent_key::{AgentKey, CertChain, SerdeSig};
8use crate::fingerprint::Fingerprint;
9use crate::signer::MeshSigner;
10use crate::{MeshError, Result};
11use ed25519_dalek::{Verifier, VerifyingKey};
12use rand::RngCore;
13use serde::{Deserialize, Serialize};
14use serde_bytes::ByteBuf;
15
16const ENVELOPE_TAG: &[u8] = b"agent-mesh-envelope-v1";
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22pub enum Recipient {
23 Direct { agent_fp: Fingerprint },
25 Topic { name: String },
28 Anycast { capability: String },
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct SignedEnvelope {
46 pub cert_chain: CertChain,
47 pub recipient: Recipient,
48 pub nonce: [u8; 24],
49 pub sequence: u64,
50 pub payload_cid: [u8; 32],
51 pub payload: ByteBuf,
52 pub agent_sig: SerdeSig,
53}
54
55impl SignedEnvelope {
56 pub fn new(sender: &AgentKey, recipient: Recipient, sequence: u64, payload: Vec<u8>) -> Self {
62 Self::new_with_signer(sender, sender.cert().clone(), recipient, sequence, payload)
66 }
67
68 pub fn new_with_signer<S: MeshSigner + ?Sized>(
84 signer: &S,
85 cert_chain: CertChain,
86 recipient: Recipient,
87 sequence: u64,
88 payload: Vec<u8>,
89 ) -> Self {
90 let mut nonce = [0u8; 24];
91 rand::thread_rng().fill_bytes(&mut nonce);
92 let payload_cid: [u8; 32] = *blake3::hash(&payload).as_bytes();
93
94 let to_sign = signing_message(&recipient, &nonce, sequence, &payload_cid);
95 let sig = signer.sign(&to_sign);
96
97 Self {
98 cert_chain,
99 recipient,
100 nonce,
101 sequence,
102 payload_cid,
103 payload: ByteBuf::from(payload),
104 agent_sig: SerdeSig(sig),
105 }
106 }
107
108 pub fn verify(&self) -> Result<()> {
115 self.cert_chain.verify()?;
116
117 let actual_cid: [u8; 32] = *blake3::hash(&self.payload).as_bytes();
118 if actual_cid != self.payload_cid {
119 return Err(MeshError::MalformedEnvelope("payload_cid mismatch".into()));
120 }
121
122 let agent_vk = VerifyingKey::from_bytes(&self.cert_chain.agent_pubkey)
123 .map_err(|e| MeshError::InvalidKey(e.to_string()))?;
124 let to_verify = signing_message(
125 &self.recipient,
126 &self.nonce,
127 self.sequence,
128 &self.payload_cid,
129 );
130 agent_vk
131 .verify(&to_verify, &self.agent_sig.0)
132 .map_err(|_| MeshError::BadSignature)?;
133 Ok(())
134 }
135
136 #[must_use]
138 pub fn sender_agent_fp(&self) -> Fingerprint {
139 self.cert_chain.agent_fingerprint()
140 }
141
142 #[must_use]
144 pub fn sender_user_fp(&self) -> Fingerprint {
145 self.cert_chain.user_fingerprint()
146 }
147}
148
149fn signing_message(
150 recipient: &Recipient,
151 nonce: &[u8; 24],
152 sequence: u64,
153 payload_cid: &[u8; 32],
154) -> Vec<u8> {
155 let recipient_bytes =
156 serde_json::to_vec(recipient).expect("Recipient serializes deterministically");
157 let mut msg = Vec::with_capacity(ENVELOPE_TAG.len() + recipient_bytes.len() + 24 + 8 + 32);
158 msg.extend_from_slice(ENVELOPE_TAG);
159 msg.extend_from_slice(&recipient_bytes);
160 msg.extend_from_slice(nonce);
161 msg.extend_from_slice(&sequence.to_be_bytes());
162 msg.extend_from_slice(payload_cid);
163 msg
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::agent_key::AgentMetadata;
170 use crate::UserKey;
171 use std::collections::HashSet;
172
173 fn fixture_user_and_agent() -> (UserKey, AgentKey) {
174 let user = UserKey::generate();
175 let agent = AgentKey::issue(
176 &user,
177 AgentMetadata {
178 role: "worker".to_string(),
179 host: "test-host".to_string(),
180 capabilities: vec!["test".to_string()],
181 issued_at: "2026-05-28T12:00:00Z".to_string(),
182 expires_at: None,
183 caveats: crate::Caveats::top(),
184 },
185 );
186 (user, agent)
187 }
188
189 fn direct_recipient() -> Recipient {
190 Recipient::Direct {
191 agent_fp: Fingerprint::of_bytes(b"some peer"),
192 }
193 }
194
195 #[test]
196 fn roundtrip_envelope_verifies() {
197 let (_user, agent) = fixture_user_and_agent();
198 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"hello".to_vec());
199 env.verify().expect("fresh envelope verifies");
200 }
201
202 #[test]
203 fn tampered_payload_fails_verify() {
204 let (_user, agent) = fixture_user_and_agent();
205 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"original".to_vec());
206 env.payload = ByteBuf::from(b"tampered".to_vec());
207 let err = env.verify().unwrap_err();
208 match err {
209 MeshError::MalformedEnvelope(_) => {}
210 other => panic!("expected MalformedEnvelope, got {other:?}"),
211 }
212 }
213
214 #[test]
215 fn tampered_recipient_fails_verify() {
216 let (_user, agent) = fixture_user_and_agent();
217 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
218 env.recipient = Recipient::Topic {
219 name: "other".to_string(),
220 };
221 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
222 }
223
224 #[test]
225 fn tampered_sequence_fails_verify() {
226 let (_user, agent) = fixture_user_and_agent();
227 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
228 env.sequence = 999;
229 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
230 }
231
232 #[test]
233 fn tampered_nonce_fails_verify() {
234 let (_user, agent) = fixture_user_and_agent();
235 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
236 env.nonce[0] ^= 0xff;
237 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
238 }
239
240 #[test]
241 fn mismatched_payload_cid_fails() {
242 let (_user, agent) = fixture_user_and_agent();
243 let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
244 env.payload_cid[0] ^= 0xff;
245 let err = env.verify().unwrap_err();
246 match err {
247 MeshError::MalformedEnvelope(_) => {}
248 other => panic!("expected MalformedEnvelope, got {other:?}"),
249 }
250 }
251
252 #[test]
253 fn serde_roundtrip_envelope() {
254 let (_user, agent) = fixture_user_and_agent();
255 let env = SignedEnvelope::new(&agent, direct_recipient(), 7, b"payload".to_vec());
256 let json = serde_json::to_string(&env).unwrap();
257 let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
258 assert_eq!(parsed, env);
259 parsed.verify().expect("roundtripped envelope verifies");
260 }
261
262 #[test]
263 fn unique_nonces_across_envelopes() {
264 let (_user, agent) = fixture_user_and_agent();
265 let mut seen = HashSet::new();
266 for i in 0..100 {
267 let env = SignedEnvelope::new(&agent, direct_recipient(), i, b"x".to_vec());
268 assert!(seen.insert(env.nonce), "duplicate nonce after {i} draws");
269 }
270 }
271
272 #[test]
273 fn sender_fingerprints_match_cert_chain() {
274 let (user, agent) = fixture_user_and_agent();
275 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
276 assert_eq!(env.sender_agent_fp(), agent.fingerprint());
277 assert_eq!(env.sender_user_fp(), user.fingerprint());
278 }
279
280 #[test]
281 fn topic_and_anycast_recipients_roundtrip() {
282 let (_user, agent) = fixture_user_and_agent();
283 for r in [
284 Recipient::Topic {
285 name: "drake/work".to_string(),
286 },
287 Recipient::Anycast {
288 capability: "ollama".to_string(),
289 },
290 ] {
291 let env = SignedEnvelope::new(&agent, r.clone(), 1, b"x".to_vec());
292 env.verify().expect("verify");
293 let json = serde_json::to_string(&env).unwrap();
294 let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
295 assert_eq!(parsed.recipient, r);
296 }
297 }
298
299 #[test]
300 fn empty_payload_is_legal() {
301 let (_user, agent) = fixture_user_and_agent();
302 let env = SignedEnvelope::new(&agent, direct_recipient(), 1, vec![]);
303 env.verify().expect("empty payload is fine");
304 }
305
306 use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
309
310 struct ExternalSigner {
313 signing: SigningKey,
314 }
315
316 impl crate::signer::MeshSigner for ExternalSigner {
317 fn verifying_key(&self) -> VerifyingKey {
318 self.signing.verifying_key()
319 }
320 fn sign(&self, msg: &[u8]) -> Signature {
321 self.signing.sign(msg)
322 }
323 }
324
325 #[test]
329 fn external_signer_envelope_verifies_via_seam() {
330 let user = crate::UserKey::generate();
331 let worker = AgentKey::issue(
332 &user,
333 AgentMetadata {
334 role: "worker".into(),
335 host: "test-host".into(),
336 capabilities: vec!["test".into()],
337 issued_at: "2026-05-28T12:00:00Z".into(),
338 expires_at: None,
339 caveats: crate::Caveats::top(),
340 },
341 );
342
343 let phone_seed = [42u8; 32];
344 let phone = ExternalSigner {
345 signing: SigningKey::from_bytes(&phone_seed),
346 };
347 let challenge = worker.possession_challenge(*phone.verifying_key().as_bytes());
348 let proof = phone.signing.sign(&challenge.signing_bytes());
349 let phone_cert = worker
350 .delegate_external(
351 &challenge,
352 &proof,
353 AgentMetadata {
354 role: "phone".into(),
355 host: "phone-host".into(),
356 capabilities: vec!["chat".into()],
357 issued_at: "2026-05-28T12:00:00Z".into(),
358 expires_at: None,
359 caveats: crate::Caveats::top(),
360 },
361 )
362 .expect("external delegation");
363
364 let env = SignedEnvelope::new_with_signer(
366 &phone,
367 phone_cert,
368 direct_recipient(),
369 1,
370 b"seam".to_vec(),
371 );
372 env.verify().expect("seam-signed envelope verifies");
373 assert_eq!(env.sender_user_fp(), user.fingerprint());
374 }
375
376 #[test]
379 fn mismatched_signer_and_cert_fails_verify() {
380 let (_user, agent) = fixture_user_and_agent();
381 let stranger = ExternalSigner {
383 signing: SigningKey::from_bytes(&[7u8; 32]),
384 };
385 let env = SignedEnvelope::new_with_signer(
386 &stranger,
387 agent.cert().clone(),
388 direct_recipient(),
389 1,
390 b"forge".to_vec(),
391 );
392 assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
393 }
394
395 #[test]
398 fn new_is_thin_wrapper_over_new_with_signer() {
399 let (_user, agent) = fixture_user_and_agent();
400 let via_signer = SignedEnvelope::new_with_signer(
401 &agent,
402 agent.cert().clone(),
403 direct_recipient(),
404 5,
405 b"x".to_vec(),
406 );
407 via_signer.verify().expect("signer path verifies");
408 assert_eq!(via_signer.cert_chain, *agent.cert());
409 }
410}