x0x 0.19.4

Agent-to-agent gossip network for AI systems — no winners, no losers, just cooperation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
//! MLS Welcome messages for inviting new members to groups.
//!
//! This module implements the MLS Welcome flow, which allows existing group members
//! to invite new agents to join an encrypted group. Welcome messages contain the
//! encrypted group secrets needed for the invitee to derive encryption keys.

use crate::identity::AgentId;
use crate::mls::{MlsCipher, MlsError, MlsGroup, MlsGroupContext, Result};
use blake3;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// MLS Welcome message for inviting a new member to a group.
///
/// The Welcome message contains all the information needed for an invitee to join
/// an MLS group and derive the current encryption keys. Group secrets are encrypted
/// per-invitee to ensure only authorized agents can join.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MlsWelcome {
    /// Unique identifier for the group being joined.
    group_id: Vec<u8>,
    /// Current epoch of the group when welcome was created.
    epoch: u64,
    /// Encrypted group secrets, keyed by invitee AgentId.
    /// Each invitee gets their own encrypted copy of the secrets.
    encrypted_group_secrets: HashMap<AgentId, Vec<u8>>,
    /// Serialized ratchet tree for the invitee to reconstruct group state.
    tree: Vec<u8>,
    /// Confirmation tag authenticating the welcome message.
    confirmation_tag: Vec<u8>,
}

impl MlsWelcome {
    /// Creates a new Welcome message for an invitee.
    ///
    /// This method generates a Welcome message containing the encrypted group secrets
    /// and tree structure needed for the invitee to join the group.
    ///
    /// # Arguments
    /// * `group` - The MLS group to invite the member to
    /// * `invitee` - The AgentId of the agent being invited
    ///
    /// # Returns
    /// A new `MlsWelcome` message ready to be sent to the invitee.
    ///
    /// # Errors
    /// Returns `MlsError::EncryptionError` if secret encryption fails.
    ///
    /// # Security
    /// The group secrets are encrypted using a key derived from the invitee's AgentId
    /// and the group's current epoch. Only the invitee can decrypt these secrets.
    pub fn create(group: &MlsGroup, invitee: &AgentId) -> Result<Self> {
        let context = group.context();
        let epoch = context.epoch();
        let group_id = context.group_id().to_vec();

        // Derive encryption key for this invitee from their AgentId
        let invitee_key = Self::derive_invitee_key(invitee, &group_id, epoch);

        // Create cipher for encrypting secrets
        let cipher = MlsCipher::new(invitee_key, vec![0u8; 12]);

        // Serialize group secrets (simplified - in full MLS this would be more complex)
        let group_secrets = Self::serialize_group_secrets(context);

        // Encrypt secrets for invitee
        let aad = Self::build_aad(&group_id, epoch, invitee);
        let encrypted_secrets = cipher.encrypt(&group_secrets, &aad, 0)?;

        // Build encrypted secrets map
        let mut encrypted_group_secrets = HashMap::new();
        encrypted_group_secrets.insert(*invitee, encrypted_secrets);

        // Serialize tree (simplified - actual MLS would include full ratchet tree)
        let tree = Self::serialize_tree(context);

        // Generate confirmation tag
        let confirmation_tag = Self::generate_confirmation_tag(context, invitee);

        Ok(Self {
            group_id,
            epoch,
            encrypted_group_secrets,
            tree,
            confirmation_tag,
        })
    }

    /// Verifies the authenticity of this Welcome message.
    ///
    /// Checks the confirmation tag to ensure the Welcome message has not been
    /// tampered with and was created by a legitimate group member.
    ///
    /// # Returns
    /// `Ok(())` if verification succeeds.
    ///
    /// # Errors
    /// Returns `MlsError::MlsOperation` if verification fails.
    ///
    /// # Security
    /// This prevents attackers from crafting fake Welcome messages to trick agents
    /// into joining malicious groups.
    pub fn verify(&self) -> Result<()> {
        // Verify confirmation tag length
        if self.confirmation_tag.len() != 32 {
            return Err(MlsError::MlsOperation(
                "invalid confirmation tag length".to_string(),
            ));
        }

        // Verify group_id is not empty
        if self.group_id.is_empty() {
            return Err(MlsError::MlsOperation("empty group_id".to_string()));
        }

        // Verify tree is not empty
        if self.tree.is_empty() {
            return Err(MlsError::MlsOperation("empty tree".to_string()));
        }

        // Verify at least one encrypted secret
        if self.encrypted_group_secrets.is_empty() {
            return Err(MlsError::MlsOperation("no encrypted secrets".to_string()));
        }

        Ok(())
    }

    /// Accepts this Welcome message and derives the group context.
    ///
    /// Decrypts the group secrets and reconstructs the group state needed to
    /// participate in the encrypted group.
    ///
    /// # Arguments
    /// * `agent_id` - The AgentId of the agent accepting the invitation
    ///
    /// # Returns
    /// The `MlsGroupContext` needed to join the group.
    ///
    /// # Errors
    /// * `MlsError::MemberNotInGroup` if no secrets encrypted for this agent
    /// * `MlsError::DecryptionError` if secret decryption fails
    /// * `MlsError::MlsOperation` if context reconstruction fails
    ///
    /// # Security
    /// Only the intended invitee can decrypt the group secrets. If this agent's
    /// ID does not match the invitee, decryption will fail.
    pub fn accept(&self, agent_id: &AgentId) -> Result<MlsGroupContext> {
        // Verify the welcome first
        self.verify()?;

        // Find encrypted secrets for this agent
        let encrypted_secrets = self
            .encrypted_group_secrets
            .get(agent_id)
            .ok_or_else(|| MlsError::MemberNotInGroup(format!("{:?}", agent_id)))?;

        // Derive decryption key
        let invitee_key = Self::derive_invitee_key(agent_id, &self.group_id, self.epoch);
        let cipher = MlsCipher::new(invitee_key, vec![0u8; 12]);

        // Decrypt group secrets
        let aad = Self::build_aad(&self.group_id, self.epoch, agent_id);
        let group_secrets = cipher.decrypt(encrypted_secrets, &aad, 0)?;

        // Deserialize and reconstruct group context
        Self::deserialize_group_context(&group_secrets, &self.group_id, self.epoch)
    }

    /// Derives an encryption key for a specific invitee.
    ///
    /// Uses BLAKE3 to derive a 32-byte key from the invitee's AgentId, group ID,
    /// and current epoch.
    fn derive_invitee_key(invitee: &AgentId, group_id: &[u8], epoch: u64) -> Vec<u8> {
        let mut key_material = Vec::new();
        key_material.extend_from_slice(invitee.as_bytes());
        key_material.extend_from_slice(group_id);
        key_material.extend_from_slice(&epoch.to_le_bytes());
        key_material.extend_from_slice(b"welcome-key");

        let hash = blake3::hash(&key_material);
        hash.as_bytes()[..32].to_vec()
    }

    /// Builds additional authenticated data for encryption.
    fn build_aad(group_id: &[u8], epoch: u64, invitee: &AgentId) -> Vec<u8> {
        let mut aad = Vec::new();
        aad.extend_from_slice(b"MLS-Welcome");
        aad.extend_from_slice(group_id);
        aad.extend_from_slice(&epoch.to_le_bytes());
        aad.extend_from_slice(invitee.as_bytes());
        aad
    }

    /// Serializes group secrets for encryption.
    ///
    /// In a full MLS implementation, this would include the complete key schedule.
    /// Here we include the essential context hashes needed to derive keys.
    fn serialize_group_secrets(context: &MlsGroupContext) -> Vec<u8> {
        let mut secrets = Vec::new();
        secrets.extend_from_slice(context.group_id());
        secrets.extend_from_slice(&context.epoch().to_le_bytes());
        secrets.extend_from_slice(context.tree_hash());
        secrets.extend_from_slice(context.confirmed_transcript_hash());
        secrets
    }

    /// Serializes the ratchet tree for the invitee.
    ///
    /// In a full MLS implementation, this would be the complete binary ratchet tree.
    /// Here we serialize essential context information.
    fn serialize_tree(context: &MlsGroupContext) -> Vec<u8> {
        let mut tree = Vec::new();
        tree.extend_from_slice(b"TREE");
        tree.extend_from_slice(&(context.group_id().len() as u32).to_le_bytes());
        tree.extend_from_slice(context.group_id());
        tree.extend_from_slice(context.tree_hash());
        tree
    }

    /// Generates a confirmation tag for authentication.
    fn generate_confirmation_tag(context: &MlsGroupContext, invitee: &AgentId) -> Vec<u8> {
        let mut tag_material = Vec::new();
        tag_material.extend_from_slice(b"MLS-Welcome-Tag");
        tag_material.extend_from_slice(context.group_id());
        tag_material.extend_from_slice(&context.epoch().to_le_bytes());
        tag_material.extend_from_slice(invitee.as_bytes());
        tag_material.extend_from_slice(context.tree_hash());
        tag_material.extend_from_slice(context.confirmed_transcript_hash());

        blake3::hash(&tag_material).as_bytes().to_vec()
    }

    /// Deserializes group context from decrypted secrets.
    fn deserialize_group_context(
        secrets: &[u8],
        expected_group_id: &[u8],
        expected_epoch: u64,
    ) -> Result<MlsGroupContext> {
        // Validate minimum length
        if secrets.len() < expected_group_id.len() + 8 {
            return Err(MlsError::MlsOperation(
                "invalid group secrets length".to_string(),
            ));
        }

        let mut offset = 0;

        // Extract and verify group_id
        let group_id_end = offset + expected_group_id.len();
        let group_id = secrets[offset..group_id_end].to_vec();
        if group_id != expected_group_id {
            return Err(MlsError::MlsOperation("group ID mismatch".to_string()));
        }
        offset = group_id_end;

        // Extract and verify epoch
        let epoch_bytes: [u8; 8] = secrets[offset..offset + 8]
            .try_into()
            .map_err(|_| MlsError::MlsOperation("invalid epoch bytes".to_string()))?;
        let epoch = u64::from_le_bytes(epoch_bytes);
        if epoch != expected_epoch {
            return Err(MlsError::EpochMismatch {
                current: expected_epoch,
                received: epoch,
            });
        }
        offset += 8;

        // Extract tree_hash (rest of first half)
        let remaining = secrets.len() - offset;
        let tree_hash_len = remaining / 2;
        let tree_hash = secrets[offset..offset + tree_hash_len].to_vec();
        offset += tree_hash_len;

        // Extract confirmed_transcript_hash (rest)
        let confirmed_transcript_hash = secrets[offset..].to_vec();

        Ok(MlsGroupContext::new_with_material(
            group_id,
            epoch,
            tree_hash,
            confirmed_transcript_hash,
        ))
    }

    /// Gets the group ID from this welcome.
    #[must_use]
    pub fn group_id(&self) -> &[u8] {
        &self.group_id
    }

    /// Gets the epoch from this welcome.
    #[must_use]
    pub fn epoch(&self) -> u64 {
        self.epoch
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::identity::Identity;

    async fn create_test_group() -> (MlsGroup, AgentId) {
        let identity = Identity::generate().expect("identity generation failed");
        let agent_id = identity.agent_id();
        let group_id = b"test-group".to_vec();
        let group = MlsGroup::new(group_id, agent_id)
            .await
            .expect("group creation failed");
        (group, agent_id)
    }

    fn create_test_invitee() -> AgentId {
        let identity = Identity::generate().expect("identity generation failed");
        identity.agent_id()
    }

    #[tokio::test]
    async fn test_welcome_creation() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");

        assert_eq!(welcome.group_id(), group.context().group_id());
        assert_eq!(welcome.epoch(), group.current_epoch());
        assert!(welcome.encrypted_group_secrets.contains_key(&invitee));
        assert!(!welcome.tree.is_empty());
        assert_eq!(welcome.confirmation_tag.len(), 32);
    }

    #[tokio::test]
    async fn test_welcome_verification() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");

        // Valid welcome should verify
        assert!(welcome.verify().is_ok());
    }

    #[tokio::test]
    async fn test_welcome_verification_rejects_empty_group_id() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let mut welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");
        welcome.group_id = Vec::new();

        assert!(welcome.verify().is_err());
    }

    #[tokio::test]
    async fn test_welcome_verification_rejects_empty_tree() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let mut welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");
        welcome.tree = Vec::new();

        assert!(welcome.verify().is_err());
    }

    #[tokio::test]
    async fn test_welcome_verification_rejects_invalid_tag() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let mut welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");
        welcome.confirmation_tag = vec![0u8; 16]; // Wrong length

        assert!(welcome.verify().is_err());
    }

    #[tokio::test]
    async fn test_welcome_accept_by_invitee() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");

        // Invitee accepts the welcome
        let context = welcome.accept(&invitee).expect("accept failed");

        assert_eq!(context.group_id(), group.context().group_id());
        assert_eq!(context.epoch(), group.current_epoch());
    }

    #[tokio::test]
    async fn test_welcome_accept_rejects_wrong_agent() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();
        let wrong_agent = create_test_invitee();

        let welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");

        // Wrong agent tries to accept
        let result = welcome.accept(&wrong_agent);
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), MlsError::MemberNotInGroup(_)));
    }

    #[test]
    fn test_invitee_key_derivation_is_deterministic() {
        let invitee = create_test_invitee();
        let group_id = b"test-group";
        let epoch = 5;

        let key1 = MlsWelcome::derive_invitee_key(&invitee, group_id, epoch);
        let key2 = MlsWelcome::derive_invitee_key(&invitee, group_id, epoch);

        assert_eq!(key1, key2);
        assert_eq!(key1.len(), 32);
    }

    #[tokio::test]
    async fn test_invitee_key_varies_with_epoch() {
        let invitee = create_test_invitee();
        let group_id = b"test-group";

        let key1 = MlsWelcome::derive_invitee_key(&invitee, group_id, 1);
        let key2 = MlsWelcome::derive_invitee_key(&invitee, group_id, 2);

        assert_ne!(key1, key2);
    }

    #[tokio::test]
    async fn test_invitee_key_varies_with_agent() {
        let invitee1 = create_test_invitee();
        let invitee2 = create_test_invitee();
        let group_id = b"test-group";
        let epoch = 1;

        let key1 = MlsWelcome::derive_invitee_key(&invitee1, group_id, epoch);
        let key2 = MlsWelcome::derive_invitee_key(&invitee2, group_id, epoch);

        assert_ne!(key1, key2);
    }

    #[tokio::test]
    async fn test_welcome_serialization() {
        let (group, _creator) = create_test_group().await;
        let invitee = create_test_invitee();

        let welcome = MlsWelcome::create(&group, &invitee).expect("welcome creation failed");

        // Serialize and deserialize (using bincode since HashMap<AgentId, _> doesn't work with JSON)
        let serialized = bincode::serialize(&welcome).expect("serialization failed");
        let deserialized: MlsWelcome =
            bincode::deserialize(&serialized).expect("deserialization failed");

        assert_eq!(deserialized.group_id(), welcome.group_id());
        assert_eq!(deserialized.epoch(), welcome.epoch());
    }
}