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