Skip to main content

xmtp_proto/types/
backend_envelope.rs

1use prost::Message;
2use xmtp_common::sha256_array;
3
4use crate::xmtp::backend::v1::ClientEnvelope;
5
6/// Canonical outer encoding and its hash. Inner payload bytes are unchanged.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CanonicalEnvelope {
9    /// Prost's canonical field ordering for the outer envelope.
10    pub bytes: Vec<u8>,
11    /// SHA-256 of `bytes`, used as the publish idempotency key.
12    pub hash: [u8; 32],
13}
14
15/// Encode the outer envelope once for storage, retries, and hash matching.
16///
17/// Inner payload bytes are copied exactly as supplied. This function does not
18/// validate the payload or calculate the separate client MLS message ID.
19pub fn canonical_envelope(envelope: &ClientEnvelope) -> CanonicalEnvelope {
20    let bytes = envelope.encode_to_vec();
21    let hash = sha256_array(&bytes);
22    CanonicalEnvelope { bytes, hash }
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28    use crate::xmtp::{
29        backend::v1::{
30            CommitLogEntry, GroupMessage, KeyPackage, WelcomeMessage,
31            client_envelope::Payload,
32            welcome_message::{V1, Version},
33        },
34        identity::associations::{IdentityUpdate, RecoverableEd25519Signature},
35    };
36
37    /// P1-VAL-05, API-020/023/025, SEC-016. Same vector on native and wasm.
38    #[xmtp_common::test(unwrap_try = true)]
39    fn all_payloads_have_stable_canonical_bytes_and_distinct_outer_hashes() {
40        // Reordered inner fields and an unknown outer field are noncanonical.
41        let input = hex::decode("0a0b1801120201020a0300ff42800100")?;
42        let group_envelope = ClientEnvelope::decode(input.as_slice())?;
43        assert_eq!(
44            group_envelope.payload,
45            Some(Payload::GroupMessage(GroupMessage {
46                data: vec![0, 255, 66],
47                sender_hmac: vec![1, 2],
48                should_push: true,
49            }))
50        );
51
52        let cases = [
53            (
54                group_envelope.clone(),
55                "0a0b0a0300ff42120201021801",
56                "5fd2916dcdd5fe79bb09ab21d545d5637307ace6e5352d4c2d287dbdd9338888",
57            ),
58            (
59                ClientEnvelope {
60                    payload: Some(Payload::WelcomeMessage(WelcomeMessage {
61                        version: Some(Version::V1(V1 {
62                            installation_key: vec![0x10, 0x11],
63                            data: vec![0x12],
64                            hpke_public_key: vec![0x13, 0x14],
65                            wrapper_algorithm: 1,
66                            welcome_metadata: vec![0x15],
67                        })),
68                    })),
69                },
70                "12120a100a0210111201121a02131420012a0115",
71                "9a47fe38765eeebb1f272663381503c987fabbf9bb99b023fc88eaaffda60d5f",
72            ),
73            (
74                ClientEnvelope {
75                    payload: Some(Payload::KeyPackage(KeyPackage {
76                        key_package_tls_serialized: vec![0x20, 0x21, 0x22],
77                    })),
78                },
79                "1a050a03202122",
80                "3f46232f37d76bcc2e80d3c54c9f9d5a7273d78ef8f91336249ef79746fd06dc",
81            ),
82            (
83                ClientEnvelope {
84                    payload: Some(Payload::IdentityUpdate(IdentityUpdate {
85                        client_timestamp_ns: 7,
86                        inbox_id: "ab".into(),
87                        ..Default::default()
88                    })),
89                },
90                "220610071a026162",
91                "0aab40455ce3dacc36006ef421e2746016960606e24d125946e7a73dc9cf6f9b",
92            ),
93            (
94                ClientEnvelope {
95                    payload: Some(Payload::CommitLogEntry(CommitLogEntry {
96                        serialized_commit_log_entry: vec![0x30, 0x31],
97                        signature: Some(RecoverableEd25519Signature {
98                            bytes: vec![0x32],
99                            public_key: vec![0x33, 0x34],
100                        }),
101                    })),
102                },
103                "2a0d0a02303112070a013212023334",
104                "108bc3029f48dda98d9d43238154830ae0ee71e7d2e083264a2b07a3d736bee2",
105            ),
106        ];
107
108        for (envelope, expected_bytes, expected_hash) in cases {
109            let canonical = canonical_envelope(&envelope);
110            assert_eq!(hex::encode(&canonical.bytes), expected_bytes);
111            assert_eq!(hex::encode(canonical.hash), expected_hash);
112            let decoded = ClientEnvelope::decode(canonical.bytes.as_slice())?;
113            assert_eq!(decoded, envelope);
114            assert_eq!(canonical_envelope(&decoded), canonical);
115        }
116
117        let Some(Payload::GroupMessage(group)) = group_envelope.payload else {
118            unreachable!("first golden vector is a group message")
119        };
120        assert_eq!(
121            hex::encode(sha256_array(&group.data)),
122            "f803bec586282caafe409609aae90eb09f6d4cddb6e04431ddf76d22e7dcacd6"
123        );
124        assert_ne!(
125            canonical_envelope(&ClientEnvelope {
126                payload: Some(Payload::GroupMessage(group.clone())),
127            })
128            .hash,
129            sha256_array(&group.data)
130        );
131    }
132}