core_crypto/mls/credential/
mod.rs

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// TODO: ensure certificate signature must match the group's ciphersuite ; fails otherwise. Tracking issue: WPB-9632
141// Requires more than 1 ciphersuite supported at the moment.
142#[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        // check that both credentials can initiate/join a group
198        {
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            // drop alice & bob key stores
207        }
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            // this should work since the certificate is not yet expired
312            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            // Give time to the certificate to expire
324            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            // this should work since the certificate is not yet expired
350            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            // Charlie is a basic client that tries to join (i.e. emulates guest links in Wire)
377            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    /// In order to be WASM-compatible
477    // pub fn now() -> wire_e2e_identity::prelude::OffsetDateTime {
478    //     let now_since_epoch = now_std().as_secs() as i64;
479    //     wire_e2e_identity::prelude::OffsetDateTime::from_unix_timestamp(now_since_epoch).unwrap()
480    // }
481
482    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}