p2panda_encryption/message_scheme/test_utils/dcgka.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::collections::{HashMap, HashSet};
4
5use crate::Rng;
6use crate::crypto::x25519::SecretKey;
7use crate::key_bundle::Lifetime;
8use crate::key_manager::KeyManager;
9use crate::key_registry::KeyRegistry;
10use crate::message_scheme::dcgka::{
11 ControlMessage, Dcgka, DcgkaState, DirectMessage, DirectMessageType, OperationOutput,
12 ProcessOutput, UpdateSecret,
13};
14use crate::message_scheme::test_utils::dgm::AckedTestDgm;
15use crate::test_utils::{MemberId, MessageId};
16use crate::traits::{AckedGroupMembership, PreKeyManager};
17
18pub type TestDcgkaState = DcgkaState<
19 MemberId,
20 MessageId,
21 KeyRegistry<MemberId>,
22 AckedTestDgm<MemberId, MessageId>,
23 KeyManager,
24>;
25
26/// Helper method returning initialised DCGKA state for each member of a test group.
27///
28/// The method will automatically generate all required one-time pre-key bundles from each member
29/// and register them for each other.
30pub fn init_dcgka_state<const N: usize>(
31 member_ids: [MemberId; N],
32 rng: &Rng,
33) -> [TestDcgkaState; N] {
34 let mut key_bundles = HashMap::new();
35 let mut key_managers = HashMap::new();
36
37 // Generate a pre-key bundle for each other member of the group.
38 for id in member_ids {
39 let identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
40 let mut manager =
41 KeyManager::init_and_generate_prekey(&identity_secret, Lifetime::default(), rng)
42 .unwrap();
43
44 let mut bundle_list = Vec::with_capacity(member_ids.len());
45 for _ in member_ids {
46 let (manager_i, key_bundle) =
47 KeyManager::generate_onetime_bundle(manager, rng).unwrap();
48 bundle_list.push(key_bundle);
49 manager = manager_i;
50 }
51
52 key_bundles.insert(id, bundle_list);
53 key_managers.insert(id, manager);
54 }
55
56 // Register each other's pre-key bundles and initialise DCGKA state.
57 let mut result = Vec::with_capacity(member_ids.len());
58 for id in member_ids {
59 let dgm = AckedTestDgm::init(id);
60 let registry = {
61 let mut state = KeyRegistry::init();
62 for bundle_id in member_ids {
63 let bundle = key_bundles.get_mut(&bundle_id).unwrap().pop().unwrap();
64 let state_i = KeyRegistry::add_onetime_bundle(state, bundle_id, bundle).unwrap();
65 state = state_i;
66 }
67 state
68 };
69 let manager = key_managers.remove(&id).unwrap();
70 let dcgka: TestDcgkaState = Dcgka::init(id, manager, registry, dgm);
71 result.push(dcgka);
72 }
73
74 result.try_into().unwrap()
75}
76
77fn members_without(members: &[MemberId], without: &[MemberId]) -> Vec<MemberId> {
78 members
79 .iter()
80 .filter(|id| !without.contains(id))
81 .cloned()
82 .collect()
83}
84
85pub fn assert_direct_message(
86 direct_messages: &[DirectMessage<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>],
87 recipient: MemberId,
88) -> DirectMessage<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>> {
89 direct_messages
90 .iter()
91 .find(|message| message.recipient == recipient)
92 .cloned()
93 .unwrap_or_else(|| panic!("could not find direct message for {recipient:?}"))
94 .clone()
95}
96
97pub struct ExpectedMembers<'a> {
98 pub viewer: &'a [MemberId],
99 pub expected: &'a [MemberId],
100}
101
102pub fn assert_members_view(dcgka: &TestDcgkaState, assertions: &[ExpectedMembers]) {
103 for assertion in assertions {
104 for viewer in assertion.viewer {
105 assert_eq!(
106 AckedTestDgm::members_view(&dcgka.dgm, viewer).unwrap(),
107 HashSet::from_iter(assertion.expected.iter().cloned()),
108 "{} should have had members view {:?}",
109 viewer,
110 assertion.expected
111 );
112 }
113 }
114}
115
116/// Testing helper to verify DCGKA group operations and states.
117pub struct AssertableDcgka {
118 /// Update secrets the DCGKA exported for "local member -> remote member".
119 update_secrets: HashMap<(MemberId, MemberId), UpdateSecret>,
120}
121
122impl Default for AssertableDcgka {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl AssertableDcgka {
129 pub fn new() -> Self {
130 Self {
131 update_secrets: HashMap::new(),
132 }
133 }
134
135 /// Expected local state after a member created a group.
136 pub fn assert_create(
137 &mut self,
138 dcgka: &TestDcgkaState,
139 output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
140 creator_id: MemberId, // Group "creator"
141 expected_members: &[MemberId], // List of expected initial group members
142 seq: MessageId, // Id of "create" control message
143 ) {
144 // This is a local group operation and the group "creator" manages that state.
145 assert_eq!(dcgka.my_id, creator_id);
146
147 // Control messages
148 // ~~~~~~~~~~~~~~~~
149
150 // Group "creator" broadcasts a "create" control message to everyone.
151 let ControlMessage::Create {
152 ref initial_members,
153 } = output.control_message
154 else {
155 panic!("expected \"create\" control message");
156 };
157 assert_eq!(initial_members, expected_members);
158
159 // Direct messages
160 // ~~~~~~~~~~~~~~~
161
162 // Group "creator" sends direct 2SM messages to each other member of the group.
163 assert_eq!(output.direct_messages.len(), expected_members.len() - 1);
164 for (index, expected_member) in members_without(expected_members, &[creator_id])
165 .iter()
166 .enumerate()
167 {
168 assert_eq!(
169 output.direct_messages.get(index).unwrap().message_type(),
170 DirectMessageType::TwoParty,
171 );
172 assert_eq!(
173 output.direct_messages.get(index).unwrap().recipient,
174 *expected_member,
175 );
176 }
177
178 // Members view
179 // ~~~~~~~~~~~~
180
181 // Group "creator" considers that all members are part of the group now and every member
182 // has processed the "create" control message.
183 assert_members_view(
184 dcgka,
185 &[ExpectedMembers {
186 viewer: expected_members,
187 expected: expected_members,
188 }],
189 );
190
191 // Update Secrets
192 // ~~~~~~~~~~~~~~
193
194 // Group "creator" establishes the update secret for their own message ratchet.
195 assert!(output.me_update_secret.is_some());
196
197 // Remember group "creator's" update secret for later assertions.
198 self.update_secrets.insert(
199 (creator_id, creator_id),
200 output.me_update_secret.as_ref().unwrap().clone(),
201 );
202
203 // Key Material
204 // ~~~~~~~~~~~~
205
206 // Seed secret has been dropped after group got created (FS).
207 assert!(dcgka.next_seed.is_none());
208
209 // Group "creator" established member secrets for all expected members of the group.
210 assert_eq!(dcgka.member_secrets.len(), expected_members.len() - 1);
211 for member_id in members_without(expected_members, &[creator_id]) {
212 assert!(
213 dcgka
214 .member_secrets
215 .contains_key(&(creator_id, seq, member_id))
216 );
217 }
218
219 // Outer-Ratchet holds only the secret for the group "creator" so far.
220 assert_eq!(dcgka.ratchet.len(), 1);
221 assert!(dcgka.ratchet.contains_key(&creator_id));
222 }
223
224 /// Expected local state after an invited member processed a "create" control message.
225 pub fn assert_process_create(
226 &mut self,
227 dcgka: &TestDcgkaState,
228 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
229 processor_id: MemberId, // "Processor" who handles "create" control message
230 creator_id: MemberId, // Group "creator"
231 expected_members: &[MemberId], // List of expected members after processing "create"
232 seq: MessageId, // Id of "create" control message
233 ) {
234 // We're looking at the state of the "processor".
235 assert_eq!(dcgka.my_id, processor_id);
236 assert_ne!(creator_id, processor_id);
237
238 // Control messages
239 // ~~~~~~~~~~~~~~~~
240
241 // "Processing" member of "create" message broadcasts an "ack" control message to everyone.
242 let Some(ControlMessage::Ack {
243 ack_sender,
244 ack_seq,
245 }) = output.control_message
246 else {
247 panic!("expected \"ack\" control message");
248 };
249
250 // "Processing" member acknowledges the "create" message of the creator.
251 assert_eq!(ack_sender, creator_id);
252 assert_eq!(ack_seq, seq);
253
254 // Direct messages
255 // ~~~~~~~~~~~~~~~
256
257 // No direct messages.
258 assert!(output.direct_messages.is_empty());
259
260 // Members view
261 // ~~~~~~~~~~~~
262
263 // "Processor" of "create" considers all members part of the group now and every member has
264 // processed the "create" control message.
265 assert_members_view(
266 dcgka,
267 &[ExpectedMembers {
268 viewer: expected_members,
269 expected: expected_members,
270 }],
271 );
272
273 // Update Secrets
274 // ~~~~~~~~~~~~~~
275
276 // "Processor" establishes the update secret for their own message ratchet.
277 assert!(output.me_update_secret.is_some());
278
279 // Processor establishes the update secret for creator's message ratchet.
280 assert!(output.sender_update_secret.is_some());
281
282 // Remember "processor's" update secret for later assertions.
283 self.update_secrets.insert(
284 (processor_id, processor_id),
285 output.me_update_secret.as_ref().unwrap().clone(),
286 );
287
288 // Remember "creator's" update secret for later assertions.
289 self.update_secrets.insert(
290 (processor_id, creator_id),
291 output.sender_update_secret.as_ref().unwrap().clone(),
292 );
293
294 // Processor should be aware now of creator's update secret.
295 self.assert_update_secrets(processor_id, creator_id);
296
297 // Key Material
298 // ~~~~~~~~~~~~
299
300 // Seed was never used and should be none.
301 assert!(dcgka.next_seed.is_none());
302
303 // When joining a group freshly we have member secrets for every member who is not the
304 // "creator".
305 assert_eq!(
306 dcgka.member_secrets.len(),
307 members_without(expected_members, &[creator_id, processor_id]).len()
308 );
309
310 // Outer-Ratchet holds only the secret for the group "creator" and ourselves ("processor") so far.
311 assert_eq!(dcgka.ratchet.len(), 2);
312 assert!(dcgka.ratchet.contains_key(&creator_id));
313 assert!(dcgka.ratchet.contains_key(&processor_id));
314 }
315
316 /// Expected local state after a member processed an "ack" control message.
317 pub fn assert_process_ack(
318 &mut self,
319 dcgka: &TestDcgkaState,
320 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
321 processor_id: MemberId, // Member who "processes" the "ack" control message
322 acker_id: MemberId, // Author of the "ack" control message,
323 seq: MessageId, // Id of the "ack" message
324 ) {
325 // We're looking at the state of the "processor".
326 assert_eq!(dcgka.my_id, processor_id);
327 assert_ne!(acker_id, processor_id);
328
329 // Control messages
330 // ~~~~~~~~~~~~~~~~
331
332 // No control messages.
333 assert!(output.control_message.is_none());
334
335 // Direct messages
336 // ~~~~~~~~~~~~~~~
337
338 // No direct messages.
339 assert!(output.direct_messages.is_empty());
340
341 // Update Secrets
342 // ~~~~~~~~~~~~~~
343
344 // No new update secret for acking member.
345 assert!(output.me_update_secret.is_none());
346
347 // Processor establishes the update secret for acking member's message ratchet.
348 assert!(output.sender_update_secret.is_some());
349
350 // Remember ackers's update secret for later assertions.
351 self.update_secrets.insert(
352 (processor_id, acker_id),
353 output.sender_update_secret.as_ref().unwrap().clone(),
354 );
355
356 // Processor should be aware now of acker's update secret.
357 self.assert_update_secrets(processor_id, acker_id);
358
359 // Key Material
360 // ~~~~~~~~~~~~
361
362 // Seed was never used and should be none.
363 assert!(dcgka.next_seed.is_none());
364
365 // Member secrets for "acker" has to be removed (FS).
366 assert!(
367 !dcgka
368 .member_secrets
369 .contains_key(&(acker_id, seq, processor_id))
370 );
371
372 // Outer-Ratchet holds secrets for at least the "acker" and "processor" of the "ack".
373 assert!(dcgka.ratchet.contains_key(&acker_id));
374 assert!(dcgka.ratchet.contains_key(&processor_id));
375 }
376
377 /// Expected local state after an member was added to the group.
378 pub fn assert_add(
379 &mut self,
380 dcgka: &TestDcgkaState,
381 output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
382 adder_id: MemberId, // "Adder" who adds someone to the group
383 added_id: MemberId, // "Added" who will join the group
384 seq: MessageId, // Id of the "add" control message
385 ) {
386 // This is a local group operation, so we expect this to be the "adder".
387 assert_eq!(dcgka.my_id, adder_id);
388 assert_ne!(adder_id, added_id);
389
390 // Control messages
391 // ~~~~~~~~~~~~~~~~
392
393 // "Adder" broadcasts an "add" control message to everyone.
394 let ControlMessage::Add { added } = output.control_message else {
395 panic!("expected \"add\" control message");
396 };
397 assert_eq!(added, added_id);
398
399 // Direct messages
400 // ~~~~~~~~~~~~~~~
401
402 // One direct "welcome" message to "added" was generated.
403 assert_eq!(output.direct_messages.len(), 1);
404 assert_eq!(
405 output.direct_messages.first().unwrap().message_type(),
406 DirectMessageType::Welcome
407 );
408 assert_eq!(output.direct_messages.first().unwrap().recipient, added_id);
409
410 // Update Secrets
411 // ~~~~~~~~~~~~~~
412
413 // "Adder" establishes a new update secret for their own message ratchet.
414 assert!(output.me_update_secret.is_some());
415
416 // Remember "adders's" update secret for later assertions.
417 let previous = self.update_secrets.insert(
418 (adder_id, adder_id),
419 output.me_update_secret.as_ref().unwrap().clone(),
420 );
421
422 // The new update secret does not match the previous one.
423 assert_ne!(
424 previous.unwrap(),
425 output.me_update_secret.as_ref().unwrap().clone(),
426 );
427
428 // Key Material
429 // ~~~~~~~~~~~~
430
431 // Seed was never used and should be none.
432 assert!(dcgka.next_seed.is_none());
433
434 // Member secret for the "added" was established.
435 assert!(
436 dcgka
437 .member_secrets
438 .contains_key(&(adder_id, seq, added_id))
439 );
440
441 // Outer-Ratchet holds secrets for at least the "adder".
442 assert!(dcgka.ratchet.contains_key(&adder_id));
443
444 // The added doesn't have a ratchet secret yet.
445 assert!(!dcgka.ratchet.contains_key(&added_id));
446 }
447
448 /// Expected local state after an invited member "added" processes the "add" message with a
449 /// direct "welcome" message addressing them.
450 pub fn assert_process_welcome(
451 &mut self,
452 dcgka: &TestDcgkaState,
453 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
454 adder_id: MemberId, // Member who invited to the group
455 added_id: MemberId, // Member who was added to the group
456 expected_members: &[MemberId], // List of expected members after processing "add"
457 seq: MessageId, // Id of the "add" control message
458 ) {
459 // This control message is processed by the member who was added.
460 assert_eq!(dcgka.my_id, added_id);
461 assert_ne!(adder_id, added_id);
462
463 // Control messages
464 // ~~~~~~~~~~~~~~~~
465
466 // Added broadcasts an "ack" control message to everyone, no direct messages.
467 let Some(ControlMessage::Ack {
468 ack_sender,
469 ack_seq,
470 }) = output.control_message
471 else {
472 panic!("expected \"ack\" control message");
473 };
474
475 // "Added" acknowledges the "add" message of "adder".
476 assert_eq!(ack_sender, adder_id);
477 assert_eq!(ack_seq, seq);
478
479 // Direct messages
480 // ~~~~~~~~~~~~~~~
481
482 // No direct messages.
483 assert!(output.direct_messages.is_empty());
484
485 // Members view
486 // ~~~~~~~~~~~~
487
488 // "Added" considers all members as part of the group now and that "adder" has the same
489 // view as them.
490 assert_members_view(
491 dcgka,
492 &[ExpectedMembers {
493 viewer: &[added_id, adder_id],
494 expected: expected_members,
495 }],
496 );
497
498 // Update Secrets
499 // ~~~~~~~~~~~~~~
500
501 // Remember "added's" update secret for later assertions.
502 self.update_secrets.insert(
503 (added_id, added_id),
504 output.me_update_secret.as_ref().unwrap().clone(),
505 );
506
507 // Remember "adder's" update secret for later assertions.
508 self.update_secrets.insert(
509 (added_id, adder_id),
510 output.sender_update_secret.as_ref().unwrap().clone(),
511 );
512
513 // "Added" should be aware now of "adder's" update secret.
514 self.assert_update_secrets(added_id, adder_id);
515
516 // Key Material
517 // ~~~~~~~~~~~~
518
519 // Seed was never used and should be none.
520 assert!(dcgka.next_seed.is_none());
521
522 // Member secret for the "added" was dropped (FS).
523 assert!(
524 !dcgka
525 .member_secrets
526 .contains_key(&(adder_id, seq, added_id))
527 );
528
529 // Outer-Ratchet holds secrets for at least the "adder" and "added".
530 assert!(dcgka.ratchet.contains_key(&adder_id));
531 assert!(dcgka.ratchet.contains_key(&added_id));
532 }
533
534 /// Expected local state after a member who is _not_ invited processes an "add" control
535 /// message.
536 #[allow(clippy::too_many_arguments)]
537 pub fn assert_process_add(
538 &mut self,
539 dcgka: &TestDcgkaState,
540 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
541 processor_id: MemberId, // "Processor" of the "add" control message
542 adder_id: MemberId, // Id of the member who invited the new member
543 added_id: MemberId, // Id of the member which got added
544 seq: MessageId, // Id of the "add" message which is processed
545 ) {
546 // This control message is processed by every member who is _not_ the "added" and _not_ the
547 // adder.
548 assert_eq!(dcgka.my_id, processor_id);
549 assert_ne!(adder_id, processor_id);
550 assert_ne!(added_id, processor_id);
551
552 // Control messages
553 // ~~~~~~~~~~~~~~~~
554
555 // Processor broadcasts an "add-ack" control message to everyone.
556 let Some(ControlMessage::AddAck {
557 ack_sender,
558 ack_seq,
559 }) = output.control_message
560 else {
561 panic!("expected \"add-ack\" control message");
562 };
563
564 // "Processor" acknowledges the "add" message of the "adder".
565 assert_eq!(ack_sender, adder_id);
566 assert_eq!(ack_seq, seq);
567
568 // Direct messages
569 // ~~~~~~~~~~~~~~~
570
571 // "Processor" forwards a direct message to "added". It is required so the "added" member
572 // can decrypt subsequent messages of the "processing" member.
573 assert_eq!(output.direct_messages.len(), 1);
574 assert_eq!(output.direct_messages.first().unwrap().recipient, added_id);
575 assert_eq!(
576 output.direct_messages.first().unwrap().message_type(),
577 DirectMessageType::Forward
578 );
579
580 // Update Secrets
581 // ~~~~~~~~~~~~~~
582
583 // Remember "processor's" update secret for later assertions.
584 self.update_secrets.insert(
585 (processor_id, processor_id),
586 output.me_update_secret.as_ref().unwrap().clone(),
587 );
588
589 // Remember "adder's" update secret for later assertions.
590 self.update_secrets.insert(
591 (processor_id, adder_id),
592 output.sender_update_secret.as_ref().unwrap().clone(),
593 );
594
595 // "Processor" should be aware now of "adder's" update secret.
596 self.assert_update_secrets(processor_id, adder_id);
597
598 // Key Material
599 // ~~~~~~~~~~~~
600
601 // Seed was never used and should be none.
602 assert!(dcgka.next_seed.is_none());
603
604 // Member secret for the "added" was established.
605 assert!(
606 dcgka
607 .member_secrets
608 .contains_key(&(adder_id, seq, added_id))
609 );
610
611 // No ratchet secret exists yet for "added".
612 assert!(!dcgka.ratchet.contains_key(&added_id));
613 }
614
615 /// Expected local state after processing an "add-ack" control message.
616 pub fn assert_process_add_ack(
617 &mut self,
618 dcgka: &TestDcgkaState,
619 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
620 processor_id: MemberId, // "Processor" of the "add-ack" control message
621 add_acker_id: MemberId, // Sender of the "add-ack" control message
622 ) {
623 // The given state is from the "processor" and not the sender of the "add-ack" message.
624 assert_eq!(dcgka.my_id, processor_id);
625 assert_ne!(add_acker_id, processor_id);
626
627 // Control messages
628 // ~~~~~~~~~~~~~~~~
629
630 // No control messages.
631 assert!(output.control_message.is_none());
632
633 // Direct messages
634 // ~~~~~~~~~~~~~~~
635
636 // No direct messages.
637 assert!(output.direct_messages.is_empty());
638
639 // Update Secrets
640 // ~~~~~~~~~~~~~~
641
642 // No new update secret for "processor's" own message ratchet.
643 assert!(output.me_update_secret.is_none());
644
645 // "Processor" establishes the update secret for "add-acking" member's message ratchet.
646 assert!(output.sender_update_secret.is_some());
647
648 // Remember "add-ackers's" update secret for later assertions.
649 self.update_secrets.insert(
650 (processor_id, add_acker_id),
651 output.sender_update_secret.as_ref().unwrap().clone(),
652 );
653
654 // "Processor" should be aware now of "add-acker's" update secret.
655 self.assert_update_secrets(processor_id, add_acker_id);
656
657 // Key Material
658 // ~~~~~~~~~~~~
659
660 // Seed was never used and should be none.
661 assert!(dcgka.next_seed.is_none());
662
663 // Ratchet secret exists for "add-ack".
664 assert!(dcgka.ratchet.contains_key(&add_acker_id));
665 }
666
667 /// Expected local state after removing a member from the group.
668 pub fn assert_remove(
669 &mut self,
670 dcgka: &TestDcgkaState,
671 output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
672 remover_id: MemberId, // Author of the "remove" control message
673 removed_id: MemberId, // Member which gets "removed"
674 expected_members: &[MemberId], // List of expected members after removal
675 seq: MessageId, // Id of "remove" control message
676 ) {
677 // This is a local group operation, so we expect the state to be from the "remover".
678 assert_eq!(dcgka.my_id, remover_id);
679 assert_ne!(removed_id, remover_id);
680
681 // Control messages
682 // ~~~~~~~~~~~~~~~~
683
684 // "Remover" broadcasts a "remove" control message to everyone.
685 let ControlMessage::Remove { removed } = output.control_message else {
686 panic!("expected \"remove\" control message");
687 };
688 assert_eq!(removed, removed_id);
689
690 // Direct messages
691 // ~~~~~~~~~~~~~~~
692
693 // "Remover" sends a direct 2SM message to each other member of the group who is left.
694 assert_eq!(
695 output.direct_messages.len(),
696 members_without(expected_members, &[remover_id, removed_id]).len(),
697 );
698 for (index, expected_member) in members_without(expected_members, &[remover_id, removed_id])
699 .iter()
700 .enumerate()
701 {
702 assert_eq!(
703 output.direct_messages.get(index).unwrap().message_type(),
704 DirectMessageType::TwoParty,
705 "remove operation should yield a 2SM direct message"
706 );
707 assert_eq!(
708 output.direct_messages.get(index).unwrap().recipient,
709 *expected_member,
710 "direct message should address expected member",
711 );
712 }
713
714 // Update Secrets
715 // ~~~~~~~~~~~~~~
716
717 // "Remover" establishes a new update secret for their own message ratchet.
718 assert!(output.me_update_secret.is_some());
719
720 // Remember "remover's" update secret for later assertions.
721 self.update_secrets.insert(
722 (remover_id, remover_id),
723 output.me_update_secret.as_ref().unwrap().clone(),
724 );
725
726 // Key Material
727 // ~~~~~~~~~~~~
728
729 // Seed secret has been dropped after removal (FS).
730 assert!(dcgka.next_seed.is_none());
731
732 // "Remover" established member secrets for all expected members of the group.
733 assert_eq!(
734 dcgka.member_secrets.len(),
735 members_without(expected_members, &[remover_id, removed_id]).len()
736 );
737 for member_id in members_without(expected_members, &[remover_id, removed_id]) {
738 assert!(
739 dcgka
740 .member_secrets
741 .contains_key(&(remover_id, seq, member_id))
742 );
743 }
744 }
745
746 /// Expected local state after processing a "remove" control message.
747 pub fn assert_process_remove(
748 &mut self,
749 dcgka: &TestDcgkaState,
750 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
751 processor_id: MemberId,
752 remover_id: MemberId,
753 seq: MessageId,
754 ) {
755 // We're looking at the state of the processor.
756 assert_eq!(dcgka.my_id, processor_id);
757 assert_ne!(remover_id, processor_id);
758
759 // Control messages
760 // ~~~~~~~~~~~~~~~~
761
762 // "Processor" of "remove" message broadcasts an "ack" control message to everyone.
763 let Some(ControlMessage::Ack {
764 ack_sender,
765 ack_seq,
766 }) = output.control_message
767 else {
768 panic!("expected \"ack\" control message");
769 };
770
771 // "Processor" acknowledges the "remove" message of the "remover".
772 assert_eq!(ack_sender, remover_id);
773 assert_eq!(ack_seq, seq);
774
775 // Direct messages
776 // ~~~~~~~~~~~~~~~
777
778 // No direct messages.
779 assert!(output.direct_messages.is_empty());
780
781 // Update Secrets
782 // ~~~~~~~~~~~~~~
783
784 // "Processor" establishes the update secret for their own message ratchet.
785 assert!(output.me_update_secret.is_some());
786
787 // "Processor" establishes the update secret for "remover's" message ratchet.
788 assert!(output.sender_update_secret.is_some());
789
790 // Remember "processor's" update secret for later assertions.
791 self.update_secrets.insert(
792 (processor_id, processor_id),
793 output.me_update_secret.as_ref().unwrap().clone(),
794 );
795
796 // Remember "remover's" update secret for later assertions.
797 self.update_secrets.insert(
798 (processor_id, remover_id),
799 output.sender_update_secret.as_ref().unwrap().clone(),
800 );
801
802 // "Processor" should be aware now of "remover's" update secret.
803 self.assert_update_secrets(processor_id, remover_id);
804
805 // Key Material
806 // ~~~~~~~~~~~~
807
808 // Seed was never used and should be none.
809 assert!(dcgka.next_seed.is_none());
810
811 // Update secret of "remover" was dropped after use. (FS)
812 assert!(
813 !dcgka
814 .member_secrets
815 .contains_key(&(remover_id, seq, processor_id))
816 );
817
818 // Outer-Ratchet holds secrets for both "processor" and "remover".
819 assert!(dcgka.ratchet.contains_key(&processor_id));
820 assert!(dcgka.ratchet.contains_key(&remover_id));
821 }
822
823 /// Expected local state after a member updated the group.
824 pub fn assert_update(
825 &mut self,
826 dcgka: &TestDcgkaState,
827 output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
828 updater_id: MemberId, // Member updating the group
829 expected_members: &[MemberId], // List of expected members during update
830 seq: MessageId, // Id of the "update" control message
831 ) {
832 // This is a local group operation, so we expect the state to be from the "updater".
833 assert_eq!(dcgka.my_id, updater_id);
834
835 // Control messages
836 // ~~~~~~~~~~~~~~~~
837
838 // "Updater" broadcasts an "update" control message to everyone.
839 let ControlMessage::Update = output.control_message else {
840 panic!("expected \"update\" control message");
841 };
842
843 // "Updater" sends a direct 2SM message to each other member of the group.
844 assert_eq!(output.direct_messages.len(), expected_members.len() - 1);
845 for (index, expected_member) in members_without(expected_members, &[updater_id])
846 .iter()
847 .enumerate()
848 {
849 assert_eq!(
850 output.direct_messages.get(index).unwrap().message_type(),
851 DirectMessageType::TwoParty,
852 );
853 assert_eq!(
854 output.direct_messages.get(index).unwrap().recipient,
855 *expected_member,
856 );
857 }
858
859 // Update Secrets
860 // ~~~~~~~~~~~~~~
861
862 // "Updater" establishes a new update secret for their own message ratchet.
863 assert!(output.me_update_secret.is_some());
864
865 // Remember "updater's" update secret for later assertions.
866 self.update_secrets.insert(
867 (updater_id, updater_id),
868 output.me_update_secret.as_ref().unwrap().clone(),
869 );
870
871 // Key Material
872 // ~~~~~~~~~~~~
873
874 // Seed secret has been dropped after update (FS).
875 assert!(dcgka.next_seed.is_none());
876
877 // "Updater" established member secrets for all expected members of the group.
878 assert_eq!(
879 dcgka.member_secrets.len(),
880 members_without(expected_members, &[updater_id]).len()
881 );
882 for member_id in members_without(expected_members, &[updater_id]) {
883 assert!(
884 dcgka
885 .member_secrets
886 .contains_key(&(updater_id, seq, member_id))
887 );
888 }
889
890 // Outer-Ratchet contains secret from "updater".
891 assert!(dcgka.ratchet.contains_key(&updater_id));
892 }
893
894 /// Expected state after processing an "update" control message.
895 pub fn assert_process_update(
896 &mut self,
897 dcgka: &TestDcgkaState,
898 output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
899 processor_id: MemberId, // Member processing the "update" control message
900 updater_id: MemberId, // Member who updated the group
901 seq: MessageId, // Id of the "update" control message
902 ) {
903 // We're looking at the state of the processor.
904 assert_eq!(dcgka.my_id, processor_id);
905 assert_ne!(updater_id, processor_id);
906
907 // Control messages
908 // ~~~~~~~~~~~~~~~~
909
910 // Processor of "update" message broadcasts an "ack" control message to everyone.
911 let Some(ControlMessage::Ack {
912 ack_sender,
913 ack_seq,
914 }) = output.control_message
915 else {
916 panic!("expected \"ack\" control message");
917 };
918
919 // "Processor" acknowledges the "update" message of the "updater".
920 assert_eq!(ack_sender, updater_id);
921 assert_eq!(ack_seq, seq);
922
923 // Direct messages
924 // ~~~~~~~~~~~~~~~
925
926 // No direct messages.
927 assert!(output.direct_messages.is_empty());
928
929 // Update Secrets
930 // ~~~~~~~~~~~~~~
931
932 // "Processor" establishes the update secret for their own message ratchet.
933 assert!(output.me_update_secret.is_some());
934
935 // "Processor" establishes the update secret for "remover's" message ratchet.
936 assert!(output.sender_update_secret.is_some());
937
938 // Remember "processor's" update secret for later assertions.
939 self.update_secrets.insert(
940 (processor_id, processor_id),
941 output.me_update_secret.as_ref().unwrap().clone(),
942 );
943
944 // Remember "updater's" update secret for later assertions.
945 self.update_secrets.insert(
946 (processor_id, updater_id),
947 output.sender_update_secret.as_ref().unwrap().clone(),
948 );
949
950 // "Processor" should be aware now of "updater's" update secret.
951 self.assert_update_secrets(processor_id, updater_id);
952
953 // Key Material
954 // ~~~~~~~~~~~~
955
956 // Seed was never used and should be none.
957 assert!(dcgka.next_seed.is_none());
958
959 // Update secret of "updater" was dropped after use. (FS)
960 assert!(
961 !dcgka
962 .member_secrets
963 .contains_key(&(updater_id, seq, processor_id))
964 );
965
966 // Outer-Ratchet holds secrets for both "processor" and "updater".
967 assert!(dcgka.ratchet.contains_key(&processor_id));
968 assert!(dcgka.ratchet.contains_key(&updater_id));
969 }
970
971 /// Compare if member learned about the update secret from another member.
972 fn assert_update_secrets(&self, from: MemberId, to: MemberId) {
973 assert_eq!(
974 self.update_secrets.get(&(from, to)).unwrap().as_bytes(),
975 self.update_secrets.get(&(to, to)).unwrap().as_bytes(),
976 );
977 }
978}