1use mls_crypto_provider::MlsCryptoProvider;
2use openmls::prelude::{Credential, CredentialWithKey, OpenMlsCrypto};
3use openmls_basic_credential::SignatureKeyPair;
4use openmls_traits::{OpenMlsCryptoProvider, types::SignatureScheme};
5use openmls_x509_credential::CertificateKeyPair;
6use std::cmp::Ordering;
7use std::hash::{Hash, Hasher};
8
9pub(crate) mod crl;
10mod error;
11pub(crate) mod ext;
12pub(crate) mod typ;
13pub(crate) mod x509;
14
15use crate::MlsError;
16use crate::prelude::{CertificateBundle, Client, ClientId};
17pub(crate) use error::{Error, Result};
18
19#[derive(Debug)]
20pub struct CredentialBundle {
21 pub(crate) credential: Credential,
22 pub(crate) signature_key: SignatureKeyPair,
23 pub(crate) created_at: u64,
24}
25
26impl CredentialBundle {
27 pub fn credential(&self) -> &Credential {
28 &self.credential
29 }
30
31 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
32 &self.signature_key
33 }
34
35 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
36 CredentialWithKey {
37 credential: self.credential.clone(),
38 signature_key: self.signature_key.to_public_vec().into(),
39 }
40 }
41}
42
43impl From<CredentialBundle> for CredentialWithKey {
44 fn from(cb: CredentialBundle) -> Self {
45 Self {
46 credential: cb.credential,
47 signature_key: cb.signature_key.public().into(),
48 }
49 }
50}
51
52impl Clone for CredentialBundle {
53 fn clone(&self) -> Self {
54 Self {
55 credential: self.credential.clone(),
56 signature_key: SignatureKeyPair::from_raw(
57 self.signature_key.signature_scheme(),
58 self.signature_key.private().to_vec(),
59 self.signature_key.to_public_vec(),
60 ),
61 created_at: self.created_at,
62 }
63 }
64}
65
66impl Eq for CredentialBundle {}
67impl PartialEq for CredentialBundle {
68 fn eq(&self, other: &Self) -> bool {
69 self.credential.eq(&other.credential)
70 && self.created_at.eq(&other.created_at)
71 && self
72 .signature_key
73 .signature_scheme()
74 .eq(&other.signature_key.signature_scheme())
75 && self.signature_key.public().eq(other.signature_key.public())
76 }
77}
78
79impl Hash for CredentialBundle {
80 fn hash<H: Hasher>(&self, state: &mut H) {
81 self.created_at.hash(state);
82 self.signature_key.signature_scheme().hash(state);
83 self.signature_key.public().hash(state);
84 self.credential().identity().hash(state);
85 match self.credential().mls_credential() {
86 openmls::prelude::MlsCredentialType::X509(cert) => {
87 cert.certificates.hash(state);
88 }
89 openmls::prelude::MlsCredentialType::Basic(_) => {}
90 };
91 }
92}
93
94impl Ord for CredentialBundle {
95 fn cmp(&self, other: &Self) -> Ordering {
96 self.created_at.cmp(&other.created_at)
97 }
98}
99
100impl PartialOrd for CredentialBundle {
101 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
102 Some(self.cmp(other))
103 }
104}
105
106impl Client {
107 pub(crate) fn new_basic_credential_bundle(
108 id: &ClientId,
109 sc: SignatureScheme,
110 backend: &MlsCryptoProvider,
111 ) -> Result<CredentialBundle> {
112 let (sk, pk) = backend
113 .crypto()
114 .signature_key_gen(sc)
115 .map_err(MlsError::wrap("generating a signature key"))?;
116
117 let signature_key = SignatureKeyPair::from_raw(sc, sk, pk);
118 let credential = Credential::new_basic(id.to_vec());
119 let cb = CredentialBundle {
120 credential,
121 signature_key,
122 created_at: 0,
123 };
124
125 Ok(cb)
126 }
127
128 pub(crate) fn new_x509_credential_bundle(cert: CertificateBundle) -> Result<CredentialBundle> {
129 let created_at = cert.get_created_at()?;
130 let (sk, ..) = cert.private_key.into_parts();
131 let chain = cert.certificate_chain;
132
133 let kp = CertificateKeyPair::new(sk, chain.clone()).map_err(MlsError::wrap("creating certificate key pair"))?;
134
135 let credential = Credential::new_x509(chain).map_err(MlsError::wrap("creating x509 credential"))?;
136
137 let cb = CredentialBundle {
138 credential,
139 signature_key: kp.0,
140 created_at,
141 };
142 Ok(cb)
143 }
144}
145
146#[cfg(test)]
149mod tests {
150 use mls_crypto_provider::PkiKeypair;
151 use std::collections::HashMap;
152 use std::sync::Arc;
153 use wasm_bindgen_test::*;
154
155 use super::*;
156 use crate::mls::conversation::Conversation as _;
157 use crate::{
158 CoreCrypto, RecursiveError,
159 mls::credential::x509::CertificatePrivateKey,
160 prelude::{
161 ClientIdentifier, ConversationId, E2eiConversationState, INITIAL_KEYING_MATERIAL_COUNT, MlsCentral,
162 MlsCentralConfiguration, MlsCredentialType,
163 },
164 test_utils::{
165 x509::{CertificateParams, X509TestChain},
166 *,
167 },
168 };
169
170 wasm_bindgen_test_configure!(run_in_browser);
171
172 #[apply(all_cred_cipher)]
173 #[wasm_bindgen_test]
174 async fn basic_clients_can_send_messages(case: TestCase) {
175 if case.is_basic() {
176 let alice_identifier = ClientIdentifier::Basic("alice".into());
177 let bob_identifier = ClientIdentifier::Basic("bob".into());
178 assert!(try_talk(&case, None, alice_identifier, bob_identifier).await.is_ok());
179 }
180 }
181
182 #[apply(all_cred_cipher)]
183 #[wasm_bindgen_test]
184 async fn certificate_clients_can_send_messages(case: TestCase) {
185 if case.is_x509() {
186 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
187
188 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
189 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
190 assert!(
191 try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
192 .await
193 .is_ok()
194 );
195 }
196 }
197
198 #[apply(all_cred_cipher)]
199 #[wasm_bindgen_test]
200 async fn heterogeneous_clients_can_send_messages(case: TestCase) {
201 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
202
203 {
205 let alice_identifier = ClientIdentifier::Basic("alice".into());
206 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
207 assert!(
208 try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
209 .await
210 .is_ok()
211 );
212 }
214 {
215 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
216 let bob_identifier = ClientIdentifier::Basic("bob".into());
217 assert!(
218 try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
219 .await
220 .is_ok()
221 );
222 }
223 }
224
225 #[apply(all_cred_cipher)]
226 #[wasm_bindgen_test]
227 async fn should_fail_when_certificate_chain_is_empty(case: TestCase) {
228 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
229
230 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
231
232 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
233 certs.certificate_chain = vec![];
234 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
235
236 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
237 let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
238 .await
239 .unwrap_err();
240 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
241 }
242
243 #[apply(all_cred_cipher)]
244 #[wasm_bindgen_test]
245 async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestCase) {
246 use crate::MlsErrorKind;
247
248 if case.is_x509() {
249 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
250
251 let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
252
253 let new_cert = alice_cert
254 .pki_keypair
255 .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
256 .unwrap();
257 let mut alice_cert = alice_cert.clone();
258 alice_cert.certificate = new_cert;
259 let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
260 let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
261
262 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
263 let err = try_talk(&case, Some(&x509_test_chain), bob_identifier, alice_identifier)
264 .await
265 .unwrap_err();
266 assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
267 }
268 }
269
270 #[apply(all_cred_cipher)]
271 #[wasm_bindgen_test]
272 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestCase) {
273 if case.is_x509() {
274 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
275 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
276
277 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
278 let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
279
280 let eve_key = CertificatePrivateKey {
281 value: new_pki_kp.signing_key_bytes(),
282 signature_scheme: case.ciphersuite().signature_algorithm(),
283 };
284 let cb = CertificateBundle {
285 certificate_chain: certs.certificate_chain,
286 private_key: eve_key,
287 };
288 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
289
290 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
291 let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
292 .await
293 .unwrap_err();
294 assert!(innermost_source_matches!(
295 err,
296 crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
297 ));
298 }
299 }
300
301 #[apply(all_cred_cipher)]
302 #[wasm_bindgen_test]
303 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestCase) {
304 if !case.is_x509() {
305 return;
306 }
307 Box::pin(async move {
308 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
309
310 let expiration_time = core::time::Duration::from_secs(14);
311 let start = web_time::Instant::now();
312
313 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
314 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
315
316 let (alice_central, bob_central, id) =
318 try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
319 .await
320 .unwrap();
321
322 assert_eq!(
323 alice_central
324 .context
325 .conversation(&id)
326 .await
327 .unwrap()
328 .e2ei_conversation_state()
329 .await
330 .unwrap(),
331 E2eiConversationState::Verified
332 );
333
334 let elapsed = start.elapsed();
335 if expiration_time > elapsed {
337 async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
338 }
339
340 alice_central.try_talk_to(&id, &bob_central).await.unwrap();
341 assert_eq!(
342 alice_central
343 .context
344 .conversation(&id)
345 .await
346 .unwrap()
347 .e2ei_conversation_state()
348 .await
349 .unwrap(),
350 E2eiConversationState::NotVerified
351 );
352 })
353 .await;
354 }
355
356 #[apply(all_cred_cipher)]
357 #[wasm_bindgen_test]
358 async fn should_not_fail_but_degrade_when_basic_joins(case: TestCase) {
359 if !case.is_x509() {
360 return;
361 }
362 Box::pin(async {
363 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
364
365 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
366 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
367
368 let (alice_central, bob_central, id) =
370 try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
371 .await
372 .unwrap();
373
374 assert_eq!(
375 alice_central
376 .context
377 .conversation(&id)
378 .await
379 .unwrap()
380 .e2ei_conversation_state()
381 .await
382 .unwrap(),
383 E2eiConversationState::Verified
384 );
385
386 assert_eq!(
387 bob_central
388 .context
389 .conversation(&id)
390 .await
391 .unwrap()
392 .e2ei_conversation_state()
393 .await
394 .unwrap(),
395 E2eiConversationState::Verified
396 );
397
398 alice_central.try_talk_to(&id, &bob_central).await.unwrap();
399 assert_eq!(
400 alice_central
401 .context
402 .conversation(&id)
403 .await
404 .unwrap()
405 .e2ei_conversation_state()
406 .await
407 .unwrap(),
408 E2eiConversationState::Verified
409 );
410
411 assert_eq!(
412 bob_central
413 .context
414 .conversation(&id)
415 .await
416 .unwrap()
417 .e2ei_conversation_state()
418 .await
419 .unwrap(),
420 E2eiConversationState::Verified
421 );
422
423 let charlie_identifier = ClientIdentifier::Basic("charlie".into());
425 let charlie_path = tmp_db_file();
426
427 let ciphersuites = vec![case.ciphersuite()];
428
429 let charlie_central = MlsCentral::try_new(
430 MlsCentralConfiguration::try_new(
431 charlie_path.0,
432 "charlie".into(),
433 None,
434 ciphersuites.clone(),
435 None,
436 Some(INITIAL_KEYING_MATERIAL_COUNT),
437 )
438 .unwrap(),
439 )
440 .await
441 .unwrap();
442 let cc = CoreCrypto::from(charlie_central);
443 let charlie_transaction = cc.new_transaction().await.unwrap();
444 let charlie_central = cc.mls;
445 charlie_transaction
446 .mls_init(
447 charlie_identifier,
448 ciphersuites.clone(),
449 Some(INITIAL_KEYING_MATERIAL_COUNT),
450 )
451 .await
452 .unwrap();
453
454 let charlie_context = ClientContext {
455 context: charlie_transaction,
456 central: charlie_central,
457 mls_transport: Arc::<CoreCryptoTransportSuccessProvider>::default(),
458 x509_test_chain: Arc::new(Some(x509_test_chain)),
459 };
460
461 let charlie_kp = charlie_context
462 .rand_key_package_of_type(&case, MlsCredentialType::Basic)
463 .await;
464
465 alice_central
466 .invite_all_members(&case, &id, [(&charlie_context, charlie_kp)])
467 .await
468 .unwrap();
469
470 assert_eq!(
471 alice_central
472 .context
473 .conversation(&id)
474 .await
475 .unwrap()
476 .e2ei_conversation_state()
477 .await
478 .unwrap(),
479 E2eiConversationState::NotVerified
480 );
481
482 alice_central.try_talk_to(&id, &charlie_context).await.unwrap();
483
484 assert_eq!(
485 alice_central
486 .context
487 .conversation(&id)
488 .await
489 .unwrap()
490 .e2ei_conversation_state()
491 .await
492 .unwrap(),
493 E2eiConversationState::NotVerified
494 );
495 })
496 .await;
497 }
498
499 #[apply(all_cred_cipher)]
500 #[wasm_bindgen_test]
501 async fn should_fail_when_certificate_not_valid_yet(case: TestCase) {
502 use crate::MlsErrorKind;
503
504 if case.is_x509() {
505 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
506
507 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
508 let local_ca = x509_test_chain.find_local_intermediate_ca();
509 let alice_cert = {
510 let name = "alice";
511 let common_name = format!("{name} Smith");
512 let handle = format!("{}_wire", name.to_lowercase());
513 let client_id: String =
514 crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
515 .try_into()
516 .unwrap();
517 local_ca.create_and_sign_end_identity(CertificateParams {
518 common_name: Some(common_name.clone()),
519 handle: Some(handle.clone()),
520 client_id: Some(client_id.clone()),
521 validity_start: Some(tomorrow),
522 ..Default::default()
523 })
524 };
525 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
526 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
527
528 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
529
530 let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
531 .await
532 .unwrap_err();
533
534 assert!(innermost_source_matches!(
535 err,
536 MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
537 ))
538 }
539 }
540
541 pub(crate) fn now_std() -> std::time::Duration {
547 let now = web_time::SystemTime::now();
548 now.duration_since(web_time::UNIX_EPOCH).unwrap()
549 }
550
551 async fn try_talk(
552 case: &TestCase,
553 x509_test_chain: Option<&X509TestChain>,
554 creator_identifier: ClientIdentifier,
555 guest_identifier: ClientIdentifier,
556 ) -> Result<(ClientContext, ClientContext, ConversationId)> {
557 let id = conversation_id();
558 let ciphersuites = vec![case.ciphersuite()];
559
560 let creator_ct = match creator_identifier {
561 ClientIdentifier::Basic(_) => MlsCredentialType::Basic,
562 ClientIdentifier::X509(_) => MlsCredentialType::X509,
563 };
564 let guest_ct = match guest_identifier {
565 ClientIdentifier::Basic(_) => MlsCredentialType::Basic,
566 ClientIdentifier::X509(_) => MlsCredentialType::X509,
567 };
568
569 let creator_path = tmp_db_file();
570
571 let creator_cfg = MlsCentralConfiguration::try_new(
572 creator_path.0,
573 "alice".into(),
574 None,
575 ciphersuites.clone(),
576 None,
577 Some(INITIAL_KEYING_MATERIAL_COUNT),
578 )
579 .map_err(RecursiveError::mls("making creator config"))?;
580
581 let creator_central = MlsCentral::try_new(creator_cfg)
582 .await
583 .map_err(RecursiveError::mls("creating mls central"))?;
584 let creator_transport = Arc::<CoreCryptoTransportSuccessProvider>::default();
585 creator_central.provide_transport(creator_transport.clone()).await;
586 let cc = CoreCrypto::from(creator_central);
587 let creator_transaction = cc
588 .new_transaction()
589 .await
590 .map_err(RecursiveError::root("creating new transaction"))?;
591 let creator_central = cc.mls;
592
593 if let Some(x509_test_chain) = &x509_test_chain {
594 x509_test_chain.register_with_central(&creator_transaction).await;
595 }
596 let creator_client_context = ClientContext {
597 context: creator_transaction.clone(),
598 central: creator_central,
599 mls_transport: creator_transport.clone(),
600 x509_test_chain: Arc::new(x509_test_chain.cloned()),
601 };
602
603 creator_transaction
604 .mls_init(
605 creator_identifier,
606 ciphersuites.clone(),
607 Some(INITIAL_KEYING_MATERIAL_COUNT),
608 )
609 .await
610 .map_err(RecursiveError::mls("initializing mls"))?;
611
612 let guest_path = tmp_db_file();
613 let guest_cfg = MlsCentralConfiguration::try_new(
614 guest_path.0,
615 "bob".into(),
616 None,
617 ciphersuites.clone(),
618 None,
619 Some(INITIAL_KEYING_MATERIAL_COUNT),
620 )
621 .map_err(RecursiveError::mls("creating mls config"))?;
622
623 let guest_central = MlsCentral::try_new(guest_cfg)
624 .await
625 .map_err(RecursiveError::mls("creating mls central"))?;
626 let guest_transport = Arc::<CoreCryptoTransportSuccessProvider>::default();
627 guest_central.provide_transport(guest_transport.clone()).await;
628 let cc = CoreCrypto::from(guest_central);
629 let guest_transaction = cc
630 .new_transaction()
631 .await
632 .map_err(RecursiveError::root("creating new transaction"))?;
633 let guest_central = cc.mls;
634 if let Some(x509_test_chain) = &x509_test_chain {
635 x509_test_chain.register_with_central(&guest_transaction).await;
636 }
637 guest_transaction
638 .mls_init(
639 guest_identifier,
640 ciphersuites.clone(),
641 Some(INITIAL_KEYING_MATERIAL_COUNT),
642 )
643 .await
644 .map_err(RecursiveError::mls("initializing mls guest transaction"))?;
645
646 creator_transaction
647 .new_conversation(&id, creator_ct, case.cfg.clone())
648 .await
649 .map_err(RecursiveError::mls("creating new transaction"))?;
650
651 let guest_client_context = ClientContext {
652 context: guest_transaction.clone(),
653 central: guest_central,
654 mls_transport: guest_transport.clone(),
655 x509_test_chain: Arc::new(x509_test_chain.cloned()),
656 };
657
658 let guest = guest_client_context.rand_key_package_of_type(case, guest_ct).await;
659 creator_client_context
660 .invite_all_members(case, &id, [(&guest_client_context, guest)])
661 .await
662 .map_err(RecursiveError::test())?;
663
664 creator_client_context
665 .try_talk_to(&id, &guest_client_context)
666 .await
667 .map_err(RecursiveError::test())?;
668 Ok((creator_client_context, guest_client_context, id))
669 }
670}