use crate::agent_key::{AgentKey, CertChain, SerdeSig};
use crate::fingerprint::Fingerprint;
use crate::signer::MeshSigner;
use crate::{MeshError, Result};
use ed25519_dalek::{Verifier, VerifyingKey};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
const ENVELOPE_TAG: &[u8] = b"agent-mesh-envelope-v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Recipient {
Direct { agent_fp: Fingerprint },
Topic { name: String },
Anycast { capability: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignedEnvelope {
pub cert_chain: CertChain,
pub recipient: Recipient,
pub nonce: [u8; 24],
pub sequence: u64,
pub payload_cid: [u8; 32],
pub payload: ByteBuf,
pub agent_sig: SerdeSig,
}
impl SignedEnvelope {
pub fn new(sender: &AgentKey, recipient: Recipient, sequence: u64, payload: Vec<u8>) -> Self {
Self::new_with_signer(sender, sender.cert().clone(), recipient, sequence, payload)
}
pub fn new_with_signer<S: MeshSigner + ?Sized>(
signer: &S,
cert_chain: CertChain,
recipient: Recipient,
sequence: u64,
payload: Vec<u8>,
) -> Self {
let mut nonce = [0u8; 24];
rand::thread_rng().fill_bytes(&mut nonce);
let payload_cid: [u8; 32] = *blake3::hash(&payload).as_bytes();
let to_sign = signing_message(&recipient, &nonce, sequence, &payload_cid);
let sig = signer.sign(&to_sign);
Self {
cert_chain,
recipient,
nonce,
sequence,
payload_cid,
payload: ByteBuf::from(payload),
agent_sig: SerdeSig(sig),
}
}
pub fn verify(&self) -> Result<()> {
self.cert_chain.verify()?;
let actual_cid: [u8; 32] = *blake3::hash(&self.payload).as_bytes();
if actual_cid != self.payload_cid {
return Err(MeshError::MalformedEnvelope("payload_cid mismatch".into()));
}
let agent_vk = VerifyingKey::from_bytes(&self.cert_chain.agent_pubkey)
.map_err(|e| MeshError::InvalidKey(e.to_string()))?;
let to_verify = signing_message(
&self.recipient,
&self.nonce,
self.sequence,
&self.payload_cid,
);
agent_vk
.verify(&to_verify, &self.agent_sig.0)
.map_err(|_| MeshError::BadSignature)?;
Ok(())
}
#[must_use]
pub fn sender_agent_fp(&self) -> Fingerprint {
self.cert_chain.agent_fingerprint()
}
#[must_use]
pub fn sender_user_fp(&self) -> Fingerprint {
self.cert_chain.user_fingerprint()
}
}
fn signing_message(
recipient: &Recipient,
nonce: &[u8; 24],
sequence: u64,
payload_cid: &[u8; 32],
) -> Vec<u8> {
let recipient_bytes =
serde_json::to_vec(recipient).expect("Recipient serializes deterministically");
let mut msg = Vec::with_capacity(ENVELOPE_TAG.len() + recipient_bytes.len() + 24 + 8 + 32);
msg.extend_from_slice(ENVELOPE_TAG);
msg.extend_from_slice(&recipient_bytes);
msg.extend_from_slice(nonce);
msg.extend_from_slice(&sequence.to_be_bytes());
msg.extend_from_slice(payload_cid);
msg
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent_key::AgentMetadata;
use crate::UserKey;
use std::collections::HashSet;
fn fixture_user_and_agent() -> (UserKey, AgentKey) {
let user = UserKey::generate();
let agent = AgentKey::issue(
&user,
AgentMetadata {
role: "worker".to_string(),
host: "test-host".to_string(),
capabilities: vec!["test".to_string()],
issued_at: "2026-05-28T12:00:00Z".to_string(),
expires_at: None,
caveats: crate::Caveats::top(),
},
);
(user, agent)
}
fn direct_recipient() -> Recipient {
Recipient::Direct {
agent_fp: Fingerprint::of_bytes(b"some peer"),
}
}
#[test]
fn roundtrip_envelope_verifies() {
let (_user, agent) = fixture_user_and_agent();
let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"hello".to_vec());
env.verify().expect("fresh envelope verifies");
}
#[test]
fn tampered_payload_fails_verify() {
let (_user, agent) = fixture_user_and_agent();
let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"original".to_vec());
env.payload = ByteBuf::from(b"tampered".to_vec());
let err = env.verify().unwrap_err();
match err {
MeshError::MalformedEnvelope(_) => {}
other => panic!("expected MalformedEnvelope, got {other:?}"),
}
}
#[test]
fn tampered_recipient_fails_verify() {
let (_user, agent) = fixture_user_and_agent();
let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
env.recipient = Recipient::Topic {
name: "other".to_string(),
};
assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
}
#[test]
fn tampered_sequence_fails_verify() {
let (_user, agent) = fixture_user_and_agent();
let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
env.sequence = 999;
assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
}
#[test]
fn tampered_nonce_fails_verify() {
let (_user, agent) = fixture_user_and_agent();
let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
env.nonce[0] ^= 0xff;
assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
}
#[test]
fn mismatched_payload_cid_fails() {
let (_user, agent) = fixture_user_and_agent();
let mut env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
env.payload_cid[0] ^= 0xff;
let err = env.verify().unwrap_err();
match err {
MeshError::MalformedEnvelope(_) => {}
other => panic!("expected MalformedEnvelope, got {other:?}"),
}
}
#[test]
fn serde_roundtrip_envelope() {
let (_user, agent) = fixture_user_and_agent();
let env = SignedEnvelope::new(&agent, direct_recipient(), 7, b"payload".to_vec());
let json = serde_json::to_string(&env).unwrap();
let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, env);
parsed.verify().expect("roundtripped envelope verifies");
}
#[test]
fn unique_nonces_across_envelopes() {
let (_user, agent) = fixture_user_and_agent();
let mut seen = HashSet::new();
for i in 0..100 {
let env = SignedEnvelope::new(&agent, direct_recipient(), i, b"x".to_vec());
assert!(seen.insert(env.nonce), "duplicate nonce after {i} draws");
}
}
#[test]
fn sender_fingerprints_match_cert_chain() {
let (user, agent) = fixture_user_and_agent();
let env = SignedEnvelope::new(&agent, direct_recipient(), 1, b"x".to_vec());
assert_eq!(env.sender_agent_fp(), agent.fingerprint());
assert_eq!(env.sender_user_fp(), user.fingerprint());
}
#[test]
fn topic_and_anycast_recipients_roundtrip() {
let (_user, agent) = fixture_user_and_agent();
for r in [
Recipient::Topic {
name: "drake/work".to_string(),
},
Recipient::Anycast {
capability: "ollama".to_string(),
},
] {
let env = SignedEnvelope::new(&agent, r.clone(), 1, b"x".to_vec());
env.verify().expect("verify");
let json = serde_json::to_string(&env).unwrap();
let parsed: SignedEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.recipient, r);
}
}
#[test]
fn empty_payload_is_legal() {
let (_user, agent) = fixture_user_and_agent();
let env = SignedEnvelope::new(&agent, direct_recipient(), 1, vec![]);
env.verify().expect("empty payload is fine");
}
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
struct ExternalSigner {
signing: SigningKey,
}
impl crate::signer::MeshSigner for ExternalSigner {
fn verifying_key(&self) -> VerifyingKey {
self.signing.verifying_key()
}
fn sign(&self, msg: &[u8]) -> Signature {
self.signing.sign(msg)
}
}
#[test]
fn external_signer_envelope_verifies_via_seam() {
let user = crate::UserKey::generate();
let worker = AgentKey::issue(
&user,
AgentMetadata {
role: "worker".into(),
host: "test-host".into(),
capabilities: vec!["test".into()],
issued_at: "2026-05-28T12:00:00Z".into(),
expires_at: None,
caveats: crate::Caveats::top(),
},
);
let phone_seed = [42u8; 32];
let phone = ExternalSigner {
signing: SigningKey::from_bytes(&phone_seed),
};
let challenge = worker.possession_challenge(*phone.verifying_key().as_bytes());
let proof = phone.signing.sign(&challenge.signing_bytes());
let phone_cert = worker
.delegate_external(
&challenge,
&proof,
AgentMetadata {
role: "phone".into(),
host: "phone-host".into(),
capabilities: vec!["chat".into()],
issued_at: "2026-05-28T12:00:00Z".into(),
expires_at: None,
caveats: crate::Caveats::top(),
},
)
.expect("external delegation");
let env = SignedEnvelope::new_with_signer(
&phone,
phone_cert,
direct_recipient(),
1,
b"seam".to_vec(),
);
env.verify().expect("seam-signed envelope verifies");
assert_eq!(env.sender_user_fp(), user.fingerprint());
}
#[test]
fn mismatched_signer_and_cert_fails_verify() {
let (_user, agent) = fixture_user_and_agent();
let stranger = ExternalSigner {
signing: SigningKey::from_bytes(&[7u8; 32]),
};
let env = SignedEnvelope::new_with_signer(
&stranger,
agent.cert().clone(),
direct_recipient(),
1,
b"forge".to_vec(),
);
assert!(matches!(env.verify().unwrap_err(), MeshError::BadSignature));
}
#[test]
fn new_is_thin_wrapper_over_new_with_signer() {
let (_user, agent) = fixture_user_and_agent();
let via_signer = SignedEnvelope::new_with_signer(
&agent,
agent.cert().clone(),
direct_recipient(),
5,
b"x".to_vec(),
);
via_signer.verify().expect("signer path verifies");
assert_eq!(via_signer.cert_chain, *agent.cert());
}
}