core_crypto/mls/
external_proposal.rs

1use log::{trace, warn};
2use openmls::{
3    group::QueuedProposal,
4    prelude::{GroupEpoch, GroupId, JoinProposal, LeafNodeIndex, MlsMessageOut, Proposal, Sender},
5};
6use std::collections::HashSet;
7
8use crate::{
9    group_store::GroupStoreValue,
10    mls::{credential::typ::MlsCredentialType, ClientId, ConversationId},
11    prelude::{CoreCryptoCallbacks, CryptoError, CryptoResult, MlsCiphersuite, MlsConversation, MlsError},
12};
13
14use crate::context::CentralContext;
15
16impl MlsConversation {
17    /// Validates the proposal. If it is external and an `Add` proposal it will call the callback
18    /// interface to validate the proposal, otherwise it will succeed.
19    pub(crate) async fn validate_external_proposal(
20        &self,
21        proposal: &QueuedProposal,
22        parent_conversation: Option<&GroupStoreValue<MlsConversation>>,
23        callbacks: Option<&dyn CoreCryptoCallbacks>,
24    ) -> CryptoResult<()> {
25        let is_external_proposal = matches!(proposal.sender(), Sender::External(_) | Sender::NewMemberProposal);
26        if is_external_proposal {
27            if let Proposal::Add(add_proposal) = proposal.proposal() {
28                let callbacks = callbacks.ok_or(CryptoError::CallbacksNotSet)?;
29                let existing_clients = self.members_in_next_epoch();
30                let self_identity = add_proposal.key_package().leaf_node().credential().identity();
31                let parent_clients = if let Some(parent_conv) = parent_conversation {
32                    Some(
33                        parent_conv
34                            .read()
35                            .await
36                            .group
37                            .members()
38                            .map(|kp| kp.credential.identity().to_vec().into())
39                            .collect(),
40                    )
41                } else {
42                    None
43                };
44                let is_self_user_in_group = callbacks
45                    .client_is_existing_group_user(
46                        self.id.clone(),
47                        self_identity.into(),
48                        existing_clients,
49                        parent_clients,
50                    )
51                    .await;
52                if !is_self_user_in_group {
53                    return Err(CryptoError::UnauthorizedExternalAddProposal);
54                }
55            }
56        } else {
57            warn!("Not external proposal.");
58        }
59        Ok(())
60    }
61
62    /// Get actual group members and subtract pending remove proposals
63    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
64        let pending_removals = self.pending_removals();
65        let existing_clients = self
66            .group
67            .members()
68            .filter_map(|kp| {
69                if !pending_removals.contains(&kp.index) {
70                    Some(kp.credential.identity().into())
71                } else {
72                    trace!(client_index:% = kp.index; "Client is pending removal");
73                    None
74                }
75            })
76            .collect::<HashSet<_>>();
77        existing_clients.into_iter().collect()
78    }
79
80    /// Gather pending remove proposals
81    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
82        self.group
83            .pending_proposals()
84            .filter_map(|proposal| match proposal.proposal() {
85                Proposal::Remove(ref remove) => Some(remove.removed()),
86                _ => None,
87            })
88            .collect::<Vec<_>>()
89    }
90}
91
92impl CentralContext {
93    /// Crafts a new external Add proposal. Enables a client outside a group to request addition to this group.
94    /// For Wire only, the client must belong to an user already in the group
95    ///
96    /// # Arguments
97    /// * `conversation_id` - the group/conversation id
98    /// * `epoch` - the current epoch of the group. See [openmls::group::GroupEpoch]
99    /// * `ciphersuite` - of the new [openmls::prelude::KeyPackage] to create
100    /// * `credential_type` - of the new [openmls::prelude::KeyPackage] to create
101    ///
102    /// # Return type
103    /// Returns a message with the proposal to be add a new client
104    ///
105    /// # Errors
106    /// Errors resulting from the creation of the proposal within OpenMls.
107    /// Fails when `credential_type` is [MlsCredentialType::X509] and no Credential has been created
108    /// for it beforehand with [CentralContext::e2ei_mls_init_only] or variants.
109    #[cfg_attr(test, crate::dispotent)]
110    pub async fn new_external_add_proposal(
111        &self,
112        conversation_id: ConversationId,
113        epoch: GroupEpoch,
114        ciphersuite: MlsCiphersuite,
115        credential_type: MlsCredentialType,
116    ) -> CryptoResult<MlsMessageOut> {
117        let group_id = GroupId::from_slice(&conversation_id[..]);
118        let mls_provider = self.mls_provider().await?;
119
120        let client = self.mls_client().await?;
121        let cb = client
122            .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
123            .await;
124        let cb = match (cb, credential_type) {
125            (Ok(cb), _) => cb,
126            (Err(CryptoError::CredentialNotFound(_)), MlsCredentialType::Basic) => {
127                // If a Basic CredentialBundle does not exist, just create one instead of failing
128                client
129                    .init_basic_credential_bundle_if_missing(&mls_provider, ciphersuite.signature_algorithm())
130                    .await?;
131
132                client
133                    .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
134                    .await?
135            }
136            (Err(CryptoError::CredentialNotFound(_)), MlsCredentialType::X509) => {
137                return Err(CryptoError::E2eiEnrollmentNotDone)
138            }
139            (Err(e), _) => return Err(e),
140        };
141        let kp = client
142            .generate_one_keypackage_from_credential_bundle(&mls_provider, ciphersuite, &cb)
143            .await?;
144
145        Ok(JoinProposal::new(kp, group_id, epoch, &cb.signature_key).map_err(MlsError::from)?)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use wasm_bindgen_test::*;
152
153    use crate::{prelude::MlsCommitBundle, test_utils::*};
154
155    wasm_bindgen_test_configure!(run_in_browser);
156
157    mod add {
158        use super::*;
159
160        #[apply(all_cred_cipher)]
161        #[wasm_bindgen_test]
162        async fn guest_should_externally_propose_adding_itself_to_owner_group(case: TestCase) {
163            run_test_with_client_ids(
164                case.clone(),
165                ["owner", "guest"],
166                move |[owner_central, guest_central]| {
167                    Box::pin(async move {
168                        let id = conversation_id();
169                        owner_central
170                            .context
171                            .new_conversation(&id, case.credential_type, case.cfg.clone())
172                            .await
173                            .unwrap();
174                        let epoch = owner_central.get_conversation_unchecked(&id).await.group.epoch();
175
176                        // Craft an external proposal from guest
177                        let external_add = guest_central
178                            .context
179                            .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
180                            .await
181                            .unwrap();
182
183                        // Owner receives external proposal message from server
184                        let decrypted = owner_central
185                            .context
186                            .decrypt_message(&id, external_add.to_bytes().unwrap())
187                            .await
188                            .unwrap();
189                        // just owner for now
190                        assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 1);
191
192                        // verify Guest's (sender) identity
193                        guest_central.verify_sender_identity(&case, &decrypted).await;
194
195                        // simulate commit message reception from server
196                        let MlsCommitBundle { welcome, .. } = owner_central
197                            .context
198                            .commit_pending_proposals(&id)
199                            .await
200                            .unwrap()
201                            .unwrap();
202                        owner_central.context.commit_accepted(&id).await.unwrap();
203                        // guest joined the group
204                        assert_eq!(owner_central.get_conversation_unchecked(&id).await.members().len(), 2);
205
206                        guest_central
207                            .context
208                            .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
209                            .await
210                            .unwrap();
211                        assert_eq!(guest_central.get_conversation_unchecked(&id).await.members().len(), 2);
212                        // guest can send messages in the group
213                        assert!(guest_central.try_talk_to(&id, &owner_central).await.is_ok());
214                    })
215                },
216            )
217            .await
218        }
219    }
220
221    mod remove {
222        use super::*;
223        use crate::prelude::{CryptoError, MlsConversationCreationMessage, MlsConversationInitBundle, MlsError};
224        use openmls::prelude::{
225            ExternalProposal, GroupId, MlsMessageIn, ProcessMessageError, SenderExtensionIndex, ValidationError,
226        };
227
228        #[apply(all_cred_cipher)]
229        #[wasm_bindgen_test]
230        async fn ds_should_remove_guest_from_conversation(case: TestCase) {
231            run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
232                Box::pin(async move {
233                    let owner_central = &owner.context;
234                    let guest_central = &guest.context;
235                    let id = conversation_id();
236
237                    let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
238                    let mut cfg = case.cfg.clone();
239                    owner_central
240                        .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
241                        .await
242                        .unwrap();
243                    owner_central
244                        .new_conversation(&id, case.credential_type, cfg)
245                        .await
246                        .unwrap();
247
248                    owner.invite_all(&case, &id, [&guest]).await.unwrap();
249                    assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
250
251                    // now, as e.g. a Delivery Service, let's create an external remove proposal
252                    // and kick guest out of the conversation
253                    let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
254                    let sender_index = SenderExtensionIndex::new(0);
255
256                    let (sc, ct) = (case.signature_scheme(), case.credential_type);
257                    let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
258
259                    let group_id = GroupId::from_slice(&id[..]);
260                    let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
261                    let proposal =
262                        ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
263                            .unwrap();
264
265                    owner_central
266                        .decrypt_message(&id, proposal.to_bytes().unwrap())
267                        .await
268                        .unwrap();
269                    guest_central
270                        .decrypt_message(&id, proposal.to_bytes().unwrap())
271                        .await
272                        .unwrap();
273                    let MlsCommitBundle { commit, .. } =
274                        owner_central.commit_pending_proposals(&id).await.unwrap().unwrap();
275
276                    // before merging, commit is not applied
277                    assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
278                    owner_central.commit_accepted(&id).await.unwrap();
279                    assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 1);
280
281                    // guest can no longer participate
282                    guest_central
283                        .decrypt_message(&id, commit.to_bytes().unwrap())
284                        .await
285                        .unwrap();
286                    assert!(guest_central.get_conversation(&id).await.is_err());
287                    assert!(guest.try_talk_to(&id, &owner).await.is_err());
288                })
289            })
290            .await
291        }
292
293        #[apply(all_cred_cipher)]
294        #[wasm_bindgen_test]
295        async fn should_fail_when_invalid_external_sender(case: TestCase) {
296            run_test_with_client_ids(
297                case.clone(),
298                ["owner", "guest", "ds", "attacker"],
299                move |[owner, guest, ds, attacker]| {
300                    Box::pin(async move {
301                        let id = conversation_id();
302                        // Delivery service key is used in the group..
303                        let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
304                        let mut cfg = case.cfg.clone();
305                        owner
306                            .context
307                            .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
308                            .await
309                            .unwrap();
310                        owner
311                            .context
312                            .new_conversation(&id, case.credential_type, cfg)
313                            .await
314                            .unwrap();
315
316                        owner.invite_all(&case, &id, [&guest]).await.unwrap();
317                        assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
318
319                        // now, attacker will try to remove guest from the group, and should fail
320                        let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
321                        let sender_index = SenderExtensionIndex::new(1);
322
323                        let (sc, ct) = (case.signature_scheme(), case.credential_type);
324                        let cb = attacker.find_most_recent_credential_bundle(sc, ct).await.unwrap();
325                        let group_id = GroupId::from_slice(&id[..]);
326                        let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
327                        let proposal =
328                            ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
329                                .unwrap();
330
331                        let owner_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
332
333                        assert!(matches!(
334                            owner_decrypt.unwrap_err(),
335                            CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
336                                ValidationError::UnauthorizedExternalSender
337                            )))
338                        ));
339
340                        let guest_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
341                        assert!(matches!(
342                            guest_decrypt.unwrap_err(),
343                            CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::ValidationError(
344                                ValidationError::UnauthorizedExternalSender
345                            )))
346                        ));
347                    })
348                },
349            )
350            .await
351        }
352
353        #[apply(all_cred_cipher)]
354        #[wasm_bindgen_test]
355        async fn should_fail_when_wrong_signature_key(case: TestCase) {
356            run_test_with_client_ids(case.clone(), ["owner", "guest", "ds"], move |[owner, guest, ds]| {
357                Box::pin(async move {
358                    let id = conversation_id();
359
360                    // Here we're going to add the Delivery Service's (DS) signature key to the
361                    // external senders list. However, for the purpose of this test, we will
362                    // intentionally _not_ use that key when generating the remove proposal below.
363                    let key = ds.client_signature_key(&case).await.as_slice().to_vec();
364                    let mut cfg = case.cfg.clone();
365                    owner
366                        .context
367                        .set_raw_external_senders(&mut cfg, vec![key.as_slice().to_vec()])
368                        .await
369                        .unwrap();
370                    owner
371                        .context
372                        .new_conversation(&id, case.credential_type, cfg)
373                        .await
374                        .unwrap();
375
376                    owner.invite_all(&case, &id, [&guest]).await.unwrap();
377                    assert_eq!(owner.get_conversation_unchecked(&id).await.members().len(), 2);
378
379                    let to_remove = owner.index_of(&id, guest.get_client_id().await).await;
380                    let sender_index = SenderExtensionIndex::new(0);
381
382                    let (sc, ct) = (case.signature_scheme(), case.credential_type);
383                    // Intentionally use the guest's credential, and therefore the guest's signature
384                    // key when generating the proposal so that the signature verification fails.
385                    let cb = guest.find_most_recent_credential_bundle(sc, ct).await.unwrap();
386                    let group_id = GroupId::from_slice(&id[..]);
387                    let epoch = owner.get_conversation_unchecked(&id).await.group.epoch();
388                    let proposal =
389                        ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
390                            .unwrap();
391
392                    let owner_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
393                    assert!(matches!(
394                        owner_decrypt.unwrap_err(),
395                        CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidSignature))
396                    ));
397
398                    let guest_decrypt = owner.context.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
399                    assert!(matches!(
400                        guest_decrypt.unwrap_err(),
401                        CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidSignature))
402                    ));
403                })
404            })
405            .await
406        }
407
408        #[apply(all_cred_cipher)]
409        #[wasm_bindgen_test]
410        async fn joiners_from_welcome_can_accept_external_remove_proposals(case: TestCase) {
411            run_test_with_client_ids(
412                case.clone(),
413                ["alice", "bob", "charlie", "ds"],
414                move |[alice, bob, charlie, ds]| {
415                    Box::pin(async move {
416                        let alice_central = &alice.context;
417                        let bob_central = &bob.context;
418                        let charlie_central = &charlie.context;
419                        let id = conversation_id();
420
421                        let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
422                        let mut cfg = case.cfg.clone();
423                        alice_central
424                            .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
425                            .await
426                            .unwrap();
427
428                        alice_central
429                            .new_conversation(&id, case.credential_type, cfg)
430                            .await
431                            .unwrap();
432
433                        alice.invite_all(&case, &id, [&bob]).await.unwrap();
434                        assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
435
436                        // Charlie joins through a Welcome and should get external_senders from Welcome
437                        // message and not from configuration
438                        let charlie_kp = charlie.rand_key_package(&case).await;
439                        let MlsConversationCreationMessage { welcome, commit, .. } = alice_central
440                            .add_members_to_conversation(&id, vec![charlie_kp])
441                            .await
442                            .unwrap();
443                        alice_central.commit_accepted(&id).await.unwrap();
444                        bob_central
445                            .decrypt_message(&id, commit.to_bytes().unwrap())
446                            .await
447                            .unwrap();
448                        // Purposely have a configuration without `external_senders`
449                        charlie_central
450                            .process_welcome_message(MlsMessageIn::from(welcome), case.custom_cfg())
451                            .await
452                            .unwrap();
453                        assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
454                        assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
455                        assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
456
457                        // now, as e.g. a Delivery Service, let's create an external remove proposal
458                        // and kick Bob out of the conversation
459                        let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
460                        let sender_index = SenderExtensionIndex::new(0);
461                        let (sc, ct) = (case.signature_scheme(), case.credential_type);
462                        let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
463                        let group_id = GroupId::from_slice(&id[..]);
464                        let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
465                        let proposal =
466                            ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
467                                .unwrap();
468
469                        // joiner from Welcome should be able to verify the external remove proposal since
470                        // it has fetched back the external_sender from Welcome
471                        let charlie_can_verify_ext_proposal =
472                            charlie_central.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
473                        assert!(charlie_can_verify_ext_proposal.is_ok());
474
475                        alice_central
476                            .decrypt_message(&id, proposal.to_bytes().unwrap())
477                            .await
478                            .unwrap();
479                        bob_central
480                            .decrypt_message(&id, proposal.to_bytes().unwrap())
481                            .await
482                            .unwrap();
483
484                        let commit = charlie_central
485                            .commit_pending_proposals(&id)
486                            .await
487                            .unwrap()
488                            .unwrap()
489                            .commit;
490                        charlie_central.commit_accepted(&id).await.unwrap();
491                        assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
492
493                        alice_central
494                            .decrypt_message(&id, commit.to_bytes().unwrap())
495                            .await
496                            .unwrap();
497                        assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
498                        bob_central
499                            .decrypt_message(&id, commit.to_bytes().unwrap())
500                            .await
501                            .unwrap();
502                        assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
503                        assert!(alice.try_talk_to(&id, &bob).await.is_err());
504                    })
505                },
506            )
507            .await
508        }
509
510        #[apply(all_cred_cipher)]
511        #[wasm_bindgen_test]
512        async fn joiners_from_external_commit_can_accept_external_remove_proposals(case: TestCase) {
513            run_test_with_client_ids(
514                case.clone(),
515                ["alice", "bob", "charlie", "ds"],
516                move |[alice, bob, charlie, ds]| {
517                    Box::pin(async move {
518                        let alice_central = &alice.context;
519                        let bob_central = &bob.context;
520                        let charlie_central = &charlie.context;
521                        let id = conversation_id();
522
523                        let ds_signature_key = ds.client_signature_key(&case).await.as_slice().to_vec();
524                        let mut cfg = case.cfg.clone();
525                        alice_central
526                            .set_raw_external_senders(&mut cfg, vec![ds_signature_key])
527                            .await
528                            .unwrap();
529
530                        alice_central
531                            .new_conversation(&id, case.credential_type, cfg)
532                            .await
533                            .unwrap();
534
535                        alice.invite_all(&case, &id, [&bob]).await.unwrap();
536                        assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
537
538                        // Charlie joins through an external commit and should get external_senders
539                        // from PGS and not from configuration
540                        let public_group_state = alice.get_group_info(&id).await;
541                        let MlsConversationInitBundle { commit, .. } = charlie_central
542                            .join_by_external_commit(public_group_state, case.custom_cfg(), case.credential_type)
543                            .await
544                            .unwrap();
545
546                        // Purposely have a configuration without `external_senders`
547                        charlie_central
548                            .merge_pending_group_from_external_commit(&id)
549                            .await
550                            .unwrap();
551                        alice_central
552                            .decrypt_message(&id, commit.to_bytes().unwrap())
553                            .await
554                            .unwrap();
555                        bob_central
556                            .decrypt_message(&id, commit.to_bytes().unwrap())
557                            .await
558                            .unwrap();
559
560                        assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 3);
561                        assert!(charlie.try_talk_to(&id, &alice).await.is_ok());
562                        assert!(charlie.try_talk_to(&id, &bob).await.is_ok());
563
564                        // now, as e.g. a Delivery Service, let's create an external remove proposal
565                        // and kick Bob out of the conversation
566                        let to_remove = alice.index_of(&id, bob.get_client_id().await).await;
567                        let sender_index = SenderExtensionIndex::new(0);
568                        let (sc, ct) = (case.signature_scheme(), case.credential_type);
569                        let cb = ds.find_most_recent_credential_bundle(sc, ct).await.unwrap();
570                        let group_id = GroupId::from_slice(&id[..]);
571                        let epoch = alice.get_conversation_unchecked(&id).await.group.epoch();
572                        let proposal =
573                            ExternalProposal::new_remove(to_remove, group_id, epoch, &cb.signature_key, sender_index)
574                                .unwrap();
575
576                        // joiner from external commit should be able to verify the external remove proposal
577                        // since it has fetched back the external_sender from external commit
578                        let charlie_can_verify_ext_proposal =
579                            charlie_central.decrypt_message(&id, proposal.to_bytes().unwrap()).await;
580                        assert!(charlie_can_verify_ext_proposal.is_ok());
581
582                        alice_central
583                            .decrypt_message(&id, proposal.to_bytes().unwrap())
584                            .await
585                            .unwrap();
586                        bob_central
587                            .decrypt_message(&id, proposal.to_bytes().unwrap())
588                            .await
589                            .unwrap();
590
591                        let commit = charlie_central
592                            .commit_pending_proposals(&id)
593                            .await
594                            .unwrap()
595                            .unwrap()
596                            .commit;
597                        charlie_central.commit_accepted(&id).await.unwrap();
598                        assert_eq!(charlie.get_conversation_unchecked(&id).await.members().len(), 2);
599
600                        alice_central
601                            .decrypt_message(&id, commit.to_bytes().unwrap())
602                            .await
603                            .unwrap();
604                        assert_eq!(alice.get_conversation_unchecked(&id).await.members().len(), 2);
605                        bob_central
606                            .decrypt_message(&id, commit.to_bytes().unwrap())
607                            .await
608                            .unwrap();
609                        assert!(alice.try_talk_to(&id, &charlie).await.is_ok());
610                        assert!(alice.try_talk_to(&id, &bob).await.is_err());
611                    })
612                },
613            )
614            .await
615        }
616    }
617}