diff --git a/core/account/src/account.rs b/core/account/src/account.rs index 8e34fc5..65c3ef5 100644 --- a/core/account/src/account.rs +++ b/core/account/src/account.rs @@ -20,6 +20,12 @@ impl TestLogosAccount { verifying_key, } } + + /// This account's cryptographic id: the hex of its verifying key. This is + /// what a credential carries and what `validate_sender` resolves to + pub fn iden_id(&self) -> IdentId { + IdentId::new(hex::encode(self.verifying_key.as_ref())) + } } impl IdentityProvider for TestLogosAccount { diff --git a/core/conversations/src/outcomes.rs b/core/conversations/src/outcomes.rs index c970c4c..fd2c5c9 100644 --- a/core/conversations/src/outcomes.rs +++ b/core/conversations/src/outcomes.rs @@ -20,12 +20,6 @@ pub struct Content { pub struct ConvoOutcome { pub convo_id: ConversationId, pub content: Option, - /// The *unvalidated* sender credential for `content`: the claimed Account - /// and the device (LocalIdentity) it was sent from. The device key is - /// MLS-authenticated, but the account claim must be validated against an - /// [`AccountService`](logos_account::AccountService) before it is trusted. - /// `None` for control messages (e.g. MLS commits) carrying no application - /// content, and for conversation types that don't yet surface a credential. pub credential: Option, } diff --git a/core/integration_tests_core/src/test_client.rs b/core/integration_tests_core/src/test_client.rs index 1064515..1c91331 100644 --- a/core/integration_tests_core/src/test_client.rs +++ b/core/integration_tests_core/src/test_client.rs @@ -41,13 +41,15 @@ pub struct ReceivedMessage { pub struct TestClient { inner: ClientType, received_messages: Vec>>, + sender_identity: MessageSender, } impl TestClient { - fn init(client: ClientType) -> Self { + fn init(client: ClientType, sender_identity: MessageSender) -> Self { Self { inner: client, received_messages: vec![], + sender_identity, } } @@ -55,6 +57,10 @@ impl TestClient { self.inner.ident_id().clone() } + pub fn as_sender(&self) -> MessageSender { + self.sender_identity.clone() + } + fn drain_outcomes(&mut self) -> Vec { let mut messages = vec![]; while let Some(data) = self.inner.ds().poll() { @@ -102,9 +108,9 @@ impl TestClient { &self.received_messages } - pub fn check(&self, convo_id: &str, content: &[u8]) -> bool { + pub fn check(&self, convo_id: &str, content: &[u8], sender: Option) -> bool { for msg in &self.received_messages { - if msg.convo_id == convo_id && msg.contents == content { + if msg.convo_id == convo_id && msg.contents == content && msg.sender == sender { return true; } } @@ -172,11 +178,16 @@ impl TestHarness { let ident = TestLogosAccount::new(Self::names(i)); addresses.insert(i, ident.id().clone()); + let iden_id = ident.iden_id(); + let sender_identity = MessageSender { + account: iden_id.clone(), + local_identity: iden_id, + }; let core_client = ClientType::new_with_name(ident, ds.clone(), rs.clone(), wp, MemStore::new()) .unwrap(); - let client = TestClient::init(core_client); + let client = TestClient::init(core_client, sender_identity); clients.push(client); } @@ -348,6 +359,8 @@ mod tests { harness.process(Duration::from_millis(200)); - assert!(harness.raya().check(&convo_id, b"Hello")) + // GroupV2 (de-mls) carries no account-bound credential yet, so the + // sender can't be validated — it resolves to `None`. + assert!(harness.raya().check(&convo_id, b"Hello", None)) } } diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs index e017b9e..ac2bc62 100644 --- a/core/integration_tests_core/tests/mls_integration.rs +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -49,35 +49,36 @@ fn create_group() { .expect("Pax send"); harness.process(Duration::from_millis(500)); - assert!(harness.saro().check(&convo_id, M_R1)); - assert!(harness.saro().check(&convo_id, M_P1)); + // The sender a recipient resolves each author to — the key-based identity, + // captured from the account (not the display name in `ident_id`). + let raya_sender = harness.raya().as_sender(); + let pax_sender = harness.pax().as_sender(); - assert!(!harness.raya().check(&convo_id, M_R1)); - assert!(harness.raya().check(&convo_id, M_P1)); + // Each message must arrive *and* carry the validated sender of its author: + // M_R1 from Raya, M_P1 from Pax. + assert!( + harness + .saro() + .check(&convo_id, M_R1, Some(raya_sender.clone())) + ); + assert!(harness.saro().check(&convo_id, M_P1, Some(pax_sender.clone()))); - assert!(!harness.pax().check(&convo_id, M_R1)); - assert!(!harness.pax().check(&convo_id, M_P1)); + assert!( + !harness + .raya() + .check(&convo_id, M_R1, Some(raya_sender.clone())) + ); + assert!(harness.raya().check(&convo_id, M_P1, Some(pax_sender.clone()))); - // Every delivered message carries a verified sender: both the Account and - // the LocalIdentity it was sent from. On testnet each identity is its own - // single-device account, so the two resolve to the same key. - let raya_sender = harness - .saro() - .sender_of(&convo_id, M_R1) - .expect("Saro should see who sent Raya's message") - .clone(); + assert!(!harness.pax().check(&convo_id, M_R1, Some(raya_sender.clone()))); + assert!(!harness.pax().check(&convo_id, M_P1, Some(pax_sender.clone()))); + + // Single-key testnet account: account and local identity are the same key. assert_eq!( raya_sender.account, raya_sender.local_identity, "single-key testnet account resolves Account == LocalIdentity" ); - - let pax_sender = harness - .saro() - .sender_of(&convo_id, M_P1) - .expect("Saro should see who sent Pax's message") - .clone(); - - // Distinct identities resolve to distinct senders — the basis for telling + // Distinct identities resolve to distinct accounts — the basis for telling // group members apart and for collapsing an account's devices to one Account. assert_ne!( raya_sender.account, pax_sender.account, diff --git a/core/integration_tests_core/tests/test_group_v2.rs b/core/integration_tests_core/tests/test_group_v2.rs index f000423..2dfac0c 100644 --- a/core/integration_tests_core/tests/test_group_v2.rs +++ b/core/integration_tests_core/tests/test_group_v2.rs @@ -32,12 +32,12 @@ fn groupv2_2way_roundtrip() { .send_content(&convo_id, S_M1) .expect("saro send"); - harness.process_until(|h| h.raya().check(&convo_id, S_M1)); + harness.process_until(|h| h.raya().check(&convo_id, S_M1, None)); // Raya replies; settle until Saro receives it. info!(target: "chat", "Raya -> sending:{R_M1:?}"); harness.raya().send_content(&convo_id, R_M1).unwrap(); - harness.process_until(|h| h.saro().check(&convo_id, R_M1)); + harness.process_until(|h| h.saro().check(&convo_id, R_M1, None)); } #[test] @@ -70,7 +70,7 @@ fn core_client() { .send_content(&convo_id, S_M1) .expect("saro send"); - harness.process_until_label("Recv S_M1", |h| h.raya().check(&convo_id, S_M1)); + harness.process_until_label("Recv S_M1", |h| h.raya().check(&convo_id, S_M1, None)); // Raya replies; settle until Saro receives it. info!(target: "chat", "Raya -> sending: {R_M1:?}"); @@ -79,7 +79,7 @@ fn core_client() { .send_content(&convo_id, R_M1) .expect("raya send"); - harness.process_until_label("Recv R_M1", |h| h.saro().check(&convo_id, R_M1)); + harness.process_until_label("Recv R_M1", |h| h.saro().check(&convo_id, R_M1, None)); // Raya (a non-creator) invites Pax; settle until Pax has joined. let particpants = &[&harness.pax().addr()]; @@ -96,7 +96,7 @@ fn core_client() { harness.saro().send_content(&convo_id, S_M2).unwrap(); harness.process_until_label("epoch check", |h| { - h.raya().check(&convo_id, S_M2) && h.pax().check(&convo_id, S_M2) + h.raya().check(&convo_id, S_M2, None) && h.pax().check(&convo_id, S_M2, None) }); } @@ -171,8 +171,8 @@ fn core_client_four_members_two_epochs() { .expect("Saro send"); harness.process_until_label("all chats converge", |h| { - h.raya().check(&convo_id, MSG) - && h.pax().check(&convo_id, MSG) - && h.mira().check(&convo_id, MSG) + h.raya().check(&convo_id, MSG, None) + && h.pax().check(&convo_id, MSG, None) + && h.mira().check(&convo_id, MSG, None) }); }