Skip to main content

mesh_llm_identity/
envelope.rs

1use crypto_box::SalsaBox;
2use crypto_box::aead::{Aead, AeadCore, OsRng as CryptoOsRng};
3use ed25519_dalek::{Signer, Verifier};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7use super::error::CryptoError;
8use super::keys::OwnerKeypair;
9
10/// A signed-then-encrypted envelope for confidential control messages.
11#[derive(Serialize, Deserialize, Clone, Debug)]
12pub struct SignedEncryptedEnvelope {
13    pub version: u32,
14    pub sender_owner_id: String,
15    pub sender_sign_public_key: String,
16    pub sender_box_public_key: String,
17    pub recipient_box_public_key: String,
18    pub message_type: String,
19    pub timestamp_unix_ms: u64,
20    pub nonce: String,
21    pub ciphertext: String,
22}
23
24/// The decrypted and verified message contents.
25#[derive(Debug)]
26pub struct OpenedMessage {
27    pub sender_owner_id: String,
28    pub sender_sign_public_key: [u8; 32],
29    pub sender_box_public_key: [u8; 32],
30    pub message_type: String,
31    pub timestamp_unix_ms: u64,
32    pub payload: Vec<u8>,
33}
34
35/// Inner plaintext: payload + detached signature.
36#[derive(Serialize, Deserialize)]
37struct InnerPayload {
38    payload: Vec<u8>,
39    signature: Vec<u8>,
40}
41
42/// Build the canonical bytes that get signed.
43///
44/// Includes all metadata fields + a hash of the payload to bind the signature
45/// to both the envelope context and the message content.
46fn canonical_signed_bytes(
47    version: u32,
48    sender_owner_id: &str,
49    sender_box_public_key: &[u8],
50    recipient_box_public_key: &[u8],
51    message_type: &str,
52    timestamp_unix_ms: u64,
53    payload: &[u8],
54) -> Vec<u8> {
55    let mut buf = Vec::new();
56    let sender_owner_id_bytes = sender_owner_id.as_bytes();
57    let message_type_bytes = message_type.as_bytes();
58
59    // Domain separation tag to prevent cross-protocol signature reuse.
60    buf.extend_from_slice(b"mesh-llm-envelope-v1:");
61    buf.extend_from_slice(&version.to_le_bytes());
62    buf.extend_from_slice(&(sender_owner_id_bytes.len() as u64).to_le_bytes());
63    buf.extend_from_slice(sender_owner_id_bytes);
64    buf.extend_from_slice(sender_box_public_key);
65    buf.extend_from_slice(recipient_box_public_key);
66    buf.extend_from_slice(&(message_type_bytes.len() as u64).to_le_bytes());
67    buf.extend_from_slice(message_type_bytes);
68    buf.extend_from_slice(&timestamp_unix_ms.to_le_bytes());
69    // Include a hash of the payload rather than the raw payload to keep
70    // the signed data compact for large payloads.
71    let payload_hash = Sha256::digest(payload);
72    buf.extend_from_slice(&payload_hash);
73    buf
74}
75
76/// Sign and encrypt a message for a specific recipient.
77pub fn seal_message(
78    sender: &OwnerKeypair,
79    recipient_box_public_key: &crypto_box::PublicKey,
80    message_type: &str,
81    payload: &[u8],
82    timestamp_unix_ms: u64,
83) -> Result<SignedEncryptedEnvelope, CryptoError> {
84    let version = 1u32;
85    let sender_owner_id = sender.owner_id();
86    let sender_box_pk = sender.encryption_public_key();
87
88    // 1. Build canonical bytes and sign.
89    let signed_bytes = canonical_signed_bytes(
90        version,
91        &sender_owner_id,
92        sender_box_pk.as_bytes(),
93        recipient_box_public_key.as_bytes(),
94        message_type,
95        timestamp_unix_ms,
96        payload,
97    );
98    let signature = sender.signing.sign(&signed_bytes);
99
100    // 2. Build inner payload with detached signature.
101    let inner = InnerPayload {
102        payload: payload.to_vec(),
103        signature: signature.to_bytes().to_vec(),
104    };
105    let inner_bytes = serde_json::to_vec(&inner)?;
106
107    // 3. Encrypt with crypto_box (XSalsa20Poly1305).
108    let salsa_box = SalsaBox::new(recipient_box_public_key, &sender.encryption);
109    let nonce = SalsaBox::generate_nonce(&mut CryptoOsRng);
110    let ct = salsa_box
111        .encrypt(&nonce, inner_bytes.as_ref())
112        .map_err(|_| CryptoError::VerificationFailed {
113            reason: "encryption failed".into(),
114        })?;
115
116    Ok(SignedEncryptedEnvelope {
117        version,
118        sender_owner_id,
119        sender_sign_public_key: hex::encode(sender.verifying_key().as_bytes()),
120        sender_box_public_key: hex::encode(sender_box_pk.as_bytes()),
121        recipient_box_public_key: hex::encode(recipient_box_public_key.as_bytes()),
122        message_type: message_type.to_string(),
123        timestamp_unix_ms,
124        nonce: hex::encode(nonce),
125        ciphertext: hex::encode(ct),
126    })
127}
128
129/// Decrypt and verify an envelope addressed to this recipient.
130pub fn open_message(
131    recipient: &OwnerKeypair,
132    envelope: &SignedEncryptedEnvelope,
133) -> Result<OpenedMessage, CryptoError> {
134    // 0. Reject unknown envelope versions.
135    if envelope.version != 1 {
136        return Err(CryptoError::VerificationFailed {
137            reason: format!("unsupported envelope version: {}", envelope.version),
138        });
139    }
140
141    // 1. Parse sender public keys.
142    let sender_sign_pk_bytes: [u8; 32] = hex::decode(&envelope.sender_sign_public_key)
143        .map_err(|_| CryptoError::InvalidKeyMaterial {
144            reason: "bad sender signing key hex".into(),
145        })?
146        .try_into()
147        .map_err(|_| CryptoError::InvalidKeyMaterial {
148            reason: "sender signing key must be 32 bytes".into(),
149        })?;
150
151    let sender_box_pk_bytes: [u8; 32] = hex::decode(&envelope.sender_box_public_key)
152        .map_err(|_| CryptoError::InvalidKeyMaterial {
153            reason: "bad sender box key hex".into(),
154        })?
155        .try_into()
156        .map_err(|_| CryptoError::InvalidKeyMaterial {
157            reason: "sender box key must be 32 bytes".into(),
158        })?;
159
160    let recipient_box_pk_bytes: [u8; 32] = hex::decode(&envelope.recipient_box_public_key)
161        .map_err(|_| CryptoError::InvalidKeyMaterial {
162            reason: "bad recipient box key hex".into(),
163        })?
164        .try_into()
165        .map_err(|_| CryptoError::InvalidKeyMaterial {
166            reason: "recipient box key must be 32 bytes".into(),
167        })?;
168
169    let sender_box_pk = crypto_box::PublicKey::from(sender_box_pk_bytes);
170
171    // 2. Verify that the envelope's claimed recipient key matches the actual recipient.
172    // This prevents an attacker from encrypting to the correct recipient while claiming
173    // a different recipient in the signed metadata.
174    let actual_recipient_box_pk_bytes = *recipient.encryption_public_key().as_bytes();
175    if recipient_box_pk_bytes != actual_recipient_box_pk_bytes {
176        return Err(CryptoError::VerificationFailed {
177            reason: "recipient_box_public_key does not match recipient encryption public key"
178                .into(),
179        });
180    }
181
182    // 3. Verify sender_owner_id matches the signing key (prevents identity spoofing).
183    let sender_verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&sender_sign_pk_bytes)
184        .map_err(|_| CryptoError::InvalidSignature)?;
185    let expected_owner_id = crate::keys::owner_id_from_verifying_key(&sender_verifying_key);
186    if envelope.sender_owner_id != expected_owner_id {
187        return Err(CryptoError::VerificationFailed {
188            reason: "sender_owner_id does not match signing public key".into(),
189        });
190    }
191
192    // 4. Decrypt.
193    let nonce_bytes = hex::decode(&envelope.nonce).map_err(|_| CryptoError::DecryptionFailed)?;
194    if nonce_bytes.len() != 24 {
195        return Err(CryptoError::DecryptionFailed);
196    }
197    let nonce = crypto_box::Nonce::from_slice(&nonce_bytes);
198    let ct = hex::decode(&envelope.ciphertext).map_err(|_| CryptoError::DecryptionFailed)?;
199
200    let salsa_box = SalsaBox::new(&sender_box_pk, &recipient.encryption);
201    let inner_bytes = salsa_box
202        .decrypt(nonce, ct.as_ref())
203        .map_err(|_| CryptoError::DecryptionFailed)?;
204
205    // 5. Parse inner payload.
206    let inner: InnerPayload =
207        serde_json::from_slice(&inner_bytes).map_err(|_| CryptoError::DecryptionFailed)?;
208
209    // 6. Verify signature.
210    let signed_bytes = canonical_signed_bytes(
211        envelope.version,
212        &envelope.sender_owner_id,
213        &sender_box_pk_bytes,
214        &recipient_box_pk_bytes,
215        &envelope.message_type,
216        envelope.timestamp_unix_ms,
217        &inner.payload,
218    );
219
220    let sig_bytes: [u8; 64] = inner
221        .signature
222        .try_into()
223        .map_err(|_| CryptoError::InvalidSignature)?;
224    let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
225
226    sender_verifying_key
227        .verify(&signed_bytes, &signature)
228        .map_err(|_| CryptoError::InvalidSignature)?;
229
230    Ok(OpenedMessage {
231        sender_owner_id: expected_owner_id,
232        sender_sign_public_key: sender_sign_pk_bytes,
233        sender_box_public_key: sender_box_pk_bytes,
234        message_type: envelope.message_type.clone(),
235        timestamp_unix_ms: envelope.timestamp_unix_ms,
236        payload: inner.payload,
237    })
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn seal_open_round_trip() {
246        let sender = OwnerKeypair::generate();
247        let recipient = OwnerKeypair::generate();
248
249        let payload = b"hello, mesh-llm!";
250        let timestamp = 1_700_000_000_000u64;
251
252        let envelope = seal_message(
253            &sender,
254            &recipient.encryption_public_key(),
255            "test.message",
256            payload,
257            timestamp,
258        )
259        .unwrap();
260
261        let opened = open_message(&recipient, &envelope).unwrap();
262        assert_eq!(opened.payload, payload);
263        assert_eq!(opened.message_type, "test.message");
264        assert_eq!(opened.timestamp_unix_ms, timestamp);
265        assert_eq!(opened.sender_owner_id, sender.owner_id());
266    }
267
268    #[test]
269    fn wrong_recipient_cannot_decrypt() {
270        let sender = OwnerKeypair::generate();
271        let recipient = OwnerKeypair::generate();
272        let wrong_recipient = OwnerKeypair::generate();
273
274        let envelope = seal_message(
275            &sender,
276            &recipient.encryption_public_key(),
277            "secret",
278            b"classified",
279            0,
280        )
281        .unwrap();
282
283        let result = open_message(&wrong_recipient, &envelope);
284        assert!(result.is_err(), "wrong recipient should fail to decrypt");
285    }
286
287    #[test]
288    fn tampered_ciphertext_fails() {
289        let sender = OwnerKeypair::generate();
290        let recipient = OwnerKeypair::generate();
291
292        let mut envelope = seal_message(
293            &sender,
294            &recipient.encryption_public_key(),
295            "test",
296            b"payload",
297            0,
298        )
299        .unwrap();
300
301        // Flip a byte in the ciphertext.
302        let mut ct_bytes = hex::decode(&envelope.ciphertext).unwrap();
303        if let Some(byte) = ct_bytes.last_mut() {
304            *byte ^= 0xff;
305        }
306        envelope.ciphertext = hex::encode(&ct_bytes);
307
308        let result = open_message(&recipient, &envelope);
309        assert!(result.is_err(), "tampered ciphertext should fail");
310    }
311
312    #[test]
313    fn spoofed_owner_id_rejected() {
314        let sender = OwnerKeypair::generate();
315        let recipient = OwnerKeypair::generate();
316
317        let mut envelope = seal_message(
318            &sender,
319            &recipient.encryption_public_key(),
320            "test",
321            b"payload",
322            0,
323        )
324        .unwrap();
325
326        // Spoof the owner_id to a different value.
327        envelope.sender_owner_id =
328            "0000000000000000000000000000000000000000000000000000000000000000".into();
329
330        let result = open_message(&recipient, &envelope);
331        assert!(
332            matches!(result, Err(CryptoError::VerificationFailed { .. })),
333            "spoofed owner_id should be rejected"
334        );
335    }
336
337    #[test]
338    fn unknown_envelope_version_rejected() {
339        let sender = OwnerKeypair::generate();
340        let recipient = OwnerKeypair::generate();
341
342        let mut envelope = seal_message(
343            &sender,
344            &recipient.encryption_public_key(),
345            "test",
346            b"payload",
347            0,
348        )
349        .unwrap();
350
351        envelope.version = 99;
352
353        let result = open_message(&recipient, &envelope);
354        assert!(
355            matches!(result, Err(CryptoError::VerificationFailed { .. })),
356            "unknown version should be rejected"
357        );
358    }
359
360    #[test]
361    fn mismatched_recipient_key_rejected() {
362        let sender = OwnerKeypair::generate();
363        let recipient = OwnerKeypair::generate();
364
365        let mut envelope = seal_message(
366            &sender,
367            &recipient.encryption_public_key(),
368            "test",
369            b"payload",
370            0,
371        )
372        .unwrap();
373
374        // Claim a different recipient key in the envelope metadata.
375        let other = OwnerKeypair::generate();
376        envelope.recipient_box_public_key = hex::encode(other.encryption_public_key().as_bytes());
377
378        let result = open_message(&recipient, &envelope);
379        assert!(
380            matches!(result, Err(CryptoError::VerificationFailed { .. })),
381            "mismatched recipient key should be rejected"
382        );
383    }
384
385    #[test]
386    fn canonical_bytes_length_prefix_variable_fields() {
387        let sender_box_key = [7u8; 32];
388        let recipient_box_key = [9u8; 32];
389
390        let left = canonical_signed_bytes(
391            1,
392            "ab",
393            &sender_box_key,
394            &recipient_box_key,
395            "c",
396            42,
397            b"payload",
398        );
399        let right = canonical_signed_bytes(
400            1,
401            "a",
402            &sender_box_key,
403            &recipient_box_key,
404            "bc",
405            42,
406            b"payload",
407        );
408
409        assert_ne!(left, right, "variable-length fields must be unambiguous");
410    }
411}