Skip to main content

mur_common/bridge/
envelope.rs

1use ed25519_dalek::Signer;
2use serde::{Deserialize, Serialize};
3
4/// Bridge-signed wrapper around an A2A payload. `payload` is the *already-
5/// canonical* JSON-serialized A2A `JsonRpcRequest`; the bridge canonicalizes
6/// (sorted keys, no whitespace) BEFORE construction. Verification re-uses
7/// these exact bytes — never re-canonicalize on receive.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct SignedEnvelope {
10    #[serde(with = "serde_bytes")]
11    pub payload: Vec<u8>,
12    #[serde(with = "serde_bytes")]
13    pub sig: Vec<u8>,
14    pub key_version: u32,
15    pub bridge_pubkey_multibase: String,
16}
17
18impl SignedEnvelope {
19    pub fn canonical_payload_for_signing(&self) -> &[u8] {
20        &self.payload
21    }
22}
23
24#[derive(thiserror::Error, Debug)]
25pub enum EnvelopeError {
26    #[error("multibase decode: {0}")]
27    Multibase(#[from] crate::identity::IdentityError),
28    #[error("bad sig length: expected 64, got {0}")]
29    BadSigLen(usize),
30    #[error("signature does not verify")]
31    SignatureMismatch,
32    #[error("untrusted peer")]
33    UntrustedPeer,
34}
35
36/// Sign a canonical-JSON payload with the bridge's identity key.
37///
38/// `payload` MUST already be canonicalized (sorted keys, no whitespace) —
39/// this function does NOT re-canonicalize. Verifiers re-use the exact bytes
40/// stored in the resulting `SignedEnvelope.payload`; never re-canonicalize on
41/// receive.
42pub fn sign_payload(
43    payload: Vec<u8>,
44    identity: &crate::identity::AgentIdentity,
45    key_version: u32,
46) -> SignedEnvelope {
47    let sig = identity.signing_key().sign(&payload);
48    SignedEnvelope {
49        payload,
50        sig: sig.to_bytes().to_vec(),
51        key_version,
52        bridge_pubkey_multibase: crate::identity::encode_pubkey(&identity.verifying_key()),
53    }
54}
55
56/// Verify the envelope's signature against an expected pubkey (multibase).
57///
58/// This is the low-level check: it does not consult any trust list. Callers
59/// that need authorization (peer-must-be-trusted) should layer
60/// `verify_inbound_envelope` on top of this.
61pub fn verify_envelope_with_pubkey(
62    env: &SignedEnvelope,
63    expected_pubkey: &str,
64) -> Result<(), EnvelopeError> {
65    use ed25519_dalek::{Signature, VerifyingKey};
66    if env.sig.len() != 64 {
67        return Err(EnvelopeError::BadSigLen(env.sig.len()));
68    }
69    let pub_bytes = crate::identity::decode_pubkey(expected_pubkey)?;
70    let vk = VerifyingKey::from_bytes(&pub_bytes).map_err(|_| EnvelopeError::SignatureMismatch)?;
71    let sig_arr: [u8; 64] = env.sig.as_slice().try_into().unwrap();
72    let sig = Signature::from_bytes(&sig_arr);
73    vk.verify_strict(&env.payload, &sig)
74        .map_err(|_| EnvelopeError::SignatureMismatch)?;
75    Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    #[test]
82    fn canonical_payload_is_passthrough() {
83        let payload = serde_json::json!({"a": 1}).to_string().into_bytes();
84        let e = SignedEnvelope {
85            payload: payload.clone(),
86            sig: vec![0u8; 64],
87            key_version: 1,
88            bridge_pubkey_multibase: "z".into(),
89        };
90        assert_eq!(e.canonical_payload_for_signing(), payload.as_slice());
91    }
92
93    #[test]
94    fn sign_then_verify_round_trips() {
95        use crate::identity::AgentIdentity;
96        let id = AgentIdentity::generate();
97        let env = sign_payload(b"hello".to_vec(), &id, 7);
98        assert_eq!(env.key_version, 7);
99        verify_envelope_with_pubkey(&env, &env.bridge_pubkey_multibase).unwrap();
100    }
101
102    #[test]
103    fn verify_with_wrong_pubkey_fails() {
104        use crate::identity::{AgentIdentity, encode_pubkey};
105        let a = AgentIdentity::generate();
106        let b = AgentIdentity::generate();
107        let env = sign_payload(b"x".to_vec(), &a, 0);
108        let pub_b = encode_pubkey(&b.verifying_key());
109        assert!(matches!(
110            verify_envelope_with_pubkey(&env, &pub_b).unwrap_err(),
111            EnvelopeError::SignatureMismatch
112        ));
113    }
114
115    #[test]
116    fn tampered_payload_fails() {
117        use crate::identity::AgentIdentity;
118        let id = AgentIdentity::generate();
119        let mut env = sign_payload(b"orig".to_vec(), &id, 0);
120        env.payload = b"tamper".to_vec();
121        let pub_ = env.bridge_pubkey_multibase.clone();
122        assert!(matches!(
123            verify_envelope_with_pubkey(&env, &pub_).unwrap_err(),
124            EnvelopeError::SignatureMismatch
125        ));
126    }
127}