core_crypto/mls/credential/
mod.rs

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// TODO: ensure certificate signature must match the group's ciphersuite ; fails otherwise. Tracking issue: WPB-9632
147// Requires more than 1 ciphersuite supported at the moment.
148#[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        // check that both credentials can initiate/join a group
204        {
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            // drop alice & bob key stores
213        }
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            // this should work since the certificate is not yet expired
317            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            // Give time to the certificate to expire
336            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            // this should work since the certificate is not yet expired
369            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            // Charlie is a basic client that tries to join (i.e. emulates guest links in Wire)
424            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    /// In order to be WASM-compatible
542    // pub fn now() -> wire_e2e_identity::prelude::OffsetDateTime {
543    //     let now_since_epoch = now_std().as_secs() as i64;
544    //     wire_e2e_identity::prelude::OffsetDateTime::from_unix_timestamp(now_since_epoch).unwrap()
545    // }
546    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}