Skip to main content

agent_mesh_protocol/
envelope.rs

1//! Signed wire envelope. Every message between mesh peers is wrapped
2//! in one of these — the cert chain proves the sender belongs to a
3//! user, the agent signature proves the message wasn't tampered
4//! with, and the payload CID lets receivers reject mismatched bodies
5//! before paying for downstream parsing.
6
7use 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
15/// Domain-separation tag for envelope signatures.
16const ENVELOPE_TAG: &[u8] = b"agent-mesh-envelope-v1";
17
18/// Recipient of an envelope — direct peer, named topic, or anycast.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(tag = "kind", rename_all = "snake_case")]
21pub enum Recipient {
22    /// Direct peer, addressed by agent pubkey fingerprint.
23    Direct { agent_fp: Fingerprint },
24    /// Pub/sub topic — a string name, scoped to the sender's user
25    /// namespace.
26    Topic { name: String },
27    /// Anycast: any agent claiming the named capability.
28    Anycast { capability: String },
29}
30
31/// A wire envelope, signed by the sender's agent key.
32///
33/// Fields, in the order they're produced by [`Self::new`]:
34///
35/// * `cert_chain` — proves the sender's agent identity.
36/// * `recipient` — addressing tag.
37/// * `nonce` — 24 random bytes; replay-protection scope.
38/// * `sequence` — monotonic per-session counter.
39/// * `payload_cid` — BLAKE3 of `payload`.
40/// * `payload` — opaque bytes (the actual message).
41/// * `agent_sig` — signature over
42///   `ENVELOPE_TAG || recipient_bytes || nonce || seq || payload_cid`.
43#[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    /// Build and sign a new envelope.
56    ///
57    /// The 24-byte `nonce` is drawn from `rand::thread_rng`; callers
58    /// don't manage it. The `sequence` is supplied by the caller —
59    /// it's session-scoped state, not crate state.
60    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    /// Verify the envelope end-to-end:
80    ///
81    /// 1. Cert chain is valid (user sig over agent metadata).
82    /// 2. `payload_cid` matches the actual `payload` BLAKE3.
83    /// 3. Agent signature is valid over
84    ///    `(recipient, nonce, sequence, payload_cid)`.
85    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    /// Fingerprint of the sending agent.
108    #[must_use]
109    pub fn sender_agent_fp(&self) -> Fingerprint {
110        self.cert_chain.agent_fingerprint()
111    }
112
113    /// Fingerprint of the user the sender belongs to.
114    #[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}