core_crypto/mls/credential/
mod.rs

1use openmls::prelude::{Credential, CredentialWithKey};
2use openmls_basic_credential::SignatureKeyPair;
3use std::cmp::Ordering;
4use std::hash::{Hash, Hasher};
5
6pub(crate) mod crl;
7mod error;
8pub(crate) mod ext;
9pub(crate) mod typ;
10pub(crate) mod x509;
11
12pub(crate) use error::{Error, Result};
13
14#[derive(Debug)]
15pub struct CredentialBundle {
16    pub(crate) credential: Credential,
17    pub(crate) signature_key: SignatureKeyPair,
18    pub(crate) created_at: u64,
19}
20
21impl CredentialBundle {
22    pub fn credential(&self) -> &Credential {
23        &self.credential
24    }
25
26    pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
27        &self.signature_key
28    }
29
30    pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
31        CredentialWithKey {
32            credential: self.credential.clone(),
33            signature_key: self.signature_key.to_public_vec().into(),
34        }
35    }
36}
37
38impl From<CredentialBundle> for CredentialWithKey {
39    fn from(cb: CredentialBundle) -> Self {
40        Self {
41            credential: cb.credential,
42            signature_key: cb.signature_key.public().into(),
43        }
44    }
45}
46
47impl Clone for CredentialBundle {
48    fn clone(&self) -> Self {
49        Self {
50            credential: self.credential.clone(),
51            signature_key: SignatureKeyPair::from_raw(
52                self.signature_key.signature_scheme(),
53                self.signature_key.private().to_vec(),
54                self.signature_key.to_public_vec(),
55            ),
56            created_at: self.created_at,
57        }
58    }
59}
60
61impl Eq for CredentialBundle {}
62impl PartialEq for CredentialBundle {
63    fn eq(&self, other: &Self) -> bool {
64        self.credential.eq(&other.credential)
65            && self.created_at.eq(&other.created_at)
66            && self
67                .signature_key
68                .signature_scheme()
69                .eq(&other.signature_key.signature_scheme())
70            && self.signature_key.public().eq(other.signature_key.public())
71    }
72}
73
74impl Hash for CredentialBundle {
75    fn hash<H: Hasher>(&self, state: &mut H) {
76        self.created_at.hash(state);
77        self.signature_key.signature_scheme().hash(state);
78        self.signature_key.public().hash(state);
79        self.credential().identity().hash(state);
80        match self.credential().mls_credential() {
81            openmls::prelude::MlsCredentialType::X509(cert) => {
82                cert.certificates.hash(state);
83            }
84            openmls::prelude::MlsCredentialType::Basic(_) => {}
85        };
86    }
87}
88
89impl Ord for CredentialBundle {
90    fn cmp(&self, other: &Self) -> Ordering {
91        self.created_at.cmp(&other.created_at)
92    }
93}
94
95impl PartialOrd for CredentialBundle {
96    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
97        Some(self.cmp(other))
98    }
99}
100
101// TODO: ensure certificate signature must match the group's ciphersuite ; fails otherwise. Tracking issue: WPB-9632
102// Requires more than 1 ciphersuite supported at the moment.
103#[cfg(test)]
104mod tests {
105    use mls_crypto_provider::PkiKeypair;
106    use std::collections::HashMap;
107    use wasm_bindgen_test::*;
108
109    use super::x509::CertificateBundle;
110    use super::*;
111    use crate::mls::conversation::Conversation as _;
112    use crate::{
113        RecursiveError,
114        mls::credential::x509::CertificatePrivateKey,
115        prelude::{ClientIdentifier, ConversationId, E2eiConversationState, MlsCredentialType},
116        test_utils::{
117            x509::{CertificateParams, X509TestChain},
118            *,
119        },
120    };
121
122    wasm_bindgen_test_configure!(run_in_browser);
123
124    #[apply(all_cred_cipher)]
125    #[wasm_bindgen_test]
126    async fn basic_clients_can_send_messages(case: TestContext) {
127        if case.is_basic() {
128            let alice_identifier = ClientIdentifier::Basic("alice".into());
129            let bob_identifier = ClientIdentifier::Basic("bob".into());
130            assert!(try_talk(&case, None, alice_identifier, bob_identifier).await.is_ok());
131        }
132    }
133
134    #[apply(all_cred_cipher)]
135    #[wasm_bindgen_test]
136    async fn certificate_clients_can_send_messages(case: TestContext) {
137        if case.is_x509() {
138            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
139
140            let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
141            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
142            assert!(
143                try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
144                    .await
145                    .is_ok()
146            );
147        }
148    }
149
150    #[apply(all_cred_cipher)]
151    #[wasm_bindgen_test]
152    async fn heterogeneous_clients_can_send_messages(case: TestContext) {
153        let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
154
155        // check that both credentials can initiate/join a group
156        {
157            let alice_identifier = ClientIdentifier::Basic("alice".into());
158            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
159            assert!(
160                try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
161                    .await
162                    .is_ok()
163            );
164            // drop alice & bob key stores
165        }
166        {
167            let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
168            let bob_identifier = ClientIdentifier::Basic("bob".into());
169            assert!(
170                try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
171                    .await
172                    .is_ok()
173            );
174        }
175    }
176
177    #[apply(all_cred_cipher)]
178    #[wasm_bindgen_test]
179    async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
180        let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
181
182        let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
183
184        let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
185        certs.certificate_chain = vec![];
186        let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
187
188        let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
189        let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
190            .await
191            .unwrap_err();
192        assert!(innermost_source_matches!(err, Error::InvalidIdentity));
193    }
194
195    #[apply(all_cred_cipher)]
196    #[wasm_bindgen_test]
197    async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestContext) {
198        use crate::MlsErrorKind;
199
200        if case.is_x509() {
201            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
202
203            let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
204
205            let new_cert = alice_cert
206                .pki_keypair
207                .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
208                .unwrap();
209            let mut alice_cert = alice_cert.clone();
210            alice_cert.certificate = new_cert;
211            let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
212            let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
213
214            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
215            let err = try_talk(&case, Some(&x509_test_chain), bob_identifier, alice_identifier)
216                .await
217                .unwrap_err();
218            assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
219        }
220    }
221
222    #[apply(all_cred_cipher)]
223    #[wasm_bindgen_test]
224    async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
225        if case.is_x509() {
226            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
227            let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
228
229            let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
230            let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
231
232            let eve_key = CertificatePrivateKey {
233                value: new_pki_kp.signing_key_bytes(),
234                signature_scheme: case.ciphersuite().signature_algorithm(),
235            };
236            let cb = CertificateBundle {
237                certificate_chain: certs.certificate_chain,
238                private_key: eve_key,
239            };
240            let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
241
242            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
243            let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
244                .await
245                .unwrap_err();
246            assert!(innermost_source_matches!(
247                err,
248                crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
249            ));
250        }
251    }
252
253    #[apply(all_cred_cipher)]
254    #[wasm_bindgen_test]
255    async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
256        if !case.is_x509() {
257            return;
258        }
259        Box::pin(async move {
260            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
261
262            let expiration_time = core::time::Duration::from_secs(14);
263            let start = web_time::Instant::now();
264
265            let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
266            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
267
268            // this should work since the certificate is not yet expired
269            let (alice_central, bob_central, id) =
270                try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
271                    .await
272                    .unwrap();
273
274            assert_eq!(
275                alice_central
276                    .transaction
277                    .conversation(&id)
278                    .await
279                    .unwrap()
280                    .e2ei_conversation_state()
281                    .await
282                    .unwrap(),
283                E2eiConversationState::Verified
284            );
285
286            let elapsed = start.elapsed();
287            // Give time to the certificate to expire
288            if expiration_time > elapsed {
289                async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
290            }
291
292            alice_central.try_talk_to(&id, &bob_central).await.unwrap();
293            assert_eq!(
294                alice_central
295                    .transaction
296                    .conversation(&id)
297                    .await
298                    .unwrap()
299                    .e2ei_conversation_state()
300                    .await
301                    .unwrap(),
302                E2eiConversationState::NotVerified
303            );
304        })
305        .await;
306    }
307
308    #[apply(all_cred_cipher)]
309    #[wasm_bindgen_test]
310    async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
311        if !case.is_x509() {
312            return;
313        }
314        Box::pin(async {
315            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
316
317            let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
318            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
319
320            // this should work since the certificate is not yet expired
321            let (alice_central, bob_central, id) =
322                try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
323                    .await
324                    .unwrap();
325
326            assert_eq!(
327                alice_central
328                    .transaction
329                    .conversation(&id)
330                    .await
331                    .unwrap()
332                    .e2ei_conversation_state()
333                    .await
334                    .unwrap(),
335                E2eiConversationState::Verified
336            );
337
338            assert_eq!(
339                bob_central
340                    .transaction
341                    .conversation(&id)
342                    .await
343                    .unwrap()
344                    .e2ei_conversation_state()
345                    .await
346                    .unwrap(),
347                E2eiConversationState::Verified
348            );
349
350            alice_central.try_talk_to(&id, &bob_central).await.unwrap();
351            assert_eq!(
352                alice_central
353                    .transaction
354                    .conversation(&id)
355                    .await
356                    .unwrap()
357                    .e2ei_conversation_state()
358                    .await
359                    .unwrap(),
360                E2eiConversationState::Verified
361            );
362
363            assert_eq!(
364                bob_central
365                    .transaction
366                    .conversation(&id)
367                    .await
368                    .unwrap()
369                    .e2ei_conversation_state()
370                    .await
371                    .unwrap(),
372                E2eiConversationState::Verified
373            );
374
375            // Charlie is a basic client that tries to join (i.e. emulates guest links in Wire)
376            let charlie_identifier = ClientIdentifier::Basic("charlie".into());
377            let charlie_context = SessionContext::new_with_identifier(&case, charlie_identifier, None)
378                .await
379                .unwrap();
380
381            let charlie_kp = charlie_context
382                .rand_key_package_of_type(&case, MlsCredentialType::Basic)
383                .await;
384
385            alice_central
386                .invite_all_members(&case, &id, [(&charlie_context, charlie_kp)])
387                .await
388                .unwrap();
389
390            assert_eq!(
391                alice_central
392                    .transaction
393                    .conversation(&id)
394                    .await
395                    .unwrap()
396                    .e2ei_conversation_state()
397                    .await
398                    .unwrap(),
399                E2eiConversationState::NotVerified
400            );
401
402            alice_central.try_talk_to(&id, &charlie_context).await.unwrap();
403
404            assert_eq!(
405                alice_central
406                    .transaction
407                    .conversation(&id)
408                    .await
409                    .unwrap()
410                    .e2ei_conversation_state()
411                    .await
412                    .unwrap(),
413                E2eiConversationState::NotVerified
414            );
415        })
416        .await;
417    }
418
419    #[apply(all_cred_cipher)]
420    #[wasm_bindgen_test]
421    async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
422        use crate::MlsErrorKind;
423
424        if case.is_x509() {
425            let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
426
427            let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
428            let local_ca = x509_test_chain.find_local_intermediate_ca();
429            let alice_cert = {
430                let name = "alice";
431                let common_name = format!("{name} Smith");
432                let handle = format!("{}_wire", name.to_lowercase());
433                let client_id: String =
434                    crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
435                        .try_into()
436                        .unwrap();
437                local_ca.create_and_sign_end_identity(CertificateParams {
438                    common_name: Some(common_name.clone()),
439                    handle: Some(handle.clone()),
440                    client_id: Some(client_id.clone()),
441                    validity_start: Some(tomorrow),
442                    ..Default::default()
443                })
444            };
445            let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
446            let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
447
448            let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", None);
449
450            let err = try_talk(&case, Some(&x509_test_chain), alice_identifier, bob_identifier)
451                .await
452                .unwrap_err();
453
454            assert!(innermost_source_matches!(
455                err,
456                MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
457            ))
458        }
459    }
460
461    /// In order to be WASM-compatible
462    // pub fn now() -> wire_e2e_identity::prelude::OffsetDateTime {
463    //     let now_since_epoch = now_std().as_secs() as i64;
464    //     wire_e2e_identity::prelude::OffsetDateTime::from_unix_timestamp(now_since_epoch).unwrap()
465    // }
466    pub(crate) fn now_std() -> std::time::Duration {
467        let now = web_time::SystemTime::now();
468        now.duration_since(web_time::UNIX_EPOCH).unwrap()
469    }
470
471    async fn try_talk(
472        case: &TestContext,
473        x509_test_chain: Option<&X509TestChain>,
474        creator_identifier: ClientIdentifier,
475        guest_identifier: ClientIdentifier,
476    ) -> Result<(SessionContext, SessionContext, ConversationId)> {
477        let id = conversation_id();
478
479        let creator_ct = match &creator_identifier {
480            ClientIdentifier::Basic(_) => MlsCredentialType::Basic,
481            ClientIdentifier::X509(_) => MlsCredentialType::X509,
482        };
483        let guest_ct = match &guest_identifier {
484            ClientIdentifier::Basic(_) => MlsCredentialType::Basic,
485            ClientIdentifier::X509(_) => MlsCredentialType::X509,
486        };
487
488        let creator = SessionContext::new_with_identifier(
489            case,
490            creator_identifier,
491            x509_test_chain.map(|chain| X509SessionParameters {
492                chain,
493                certificate_source: Default::default(),
494            }),
495        )
496        .await
497        .map_err(RecursiveError::root("new session context"))?;
498
499        let guest = SessionContext::new_with_identifier(
500            case,
501            guest_identifier,
502            x509_test_chain.map(|chain| X509SessionParameters {
503                chain,
504                certificate_source: Default::default(),
505            }),
506        )
507        .await
508        .map_err(RecursiveError::root("new session context"))?;
509
510        creator
511            .transaction
512            .new_conversation(&id, creator_ct, case.cfg.clone())
513            .await
514            .map_err(RecursiveError::transaction("creating new transaction"))?;
515
516        let guest_kp = guest.rand_key_package_of_type(case, guest_ct).await;
517        creator
518            .invite_all_members(case, &id, [(&guest, guest_kp)])
519            .await
520            .map_err(RecursiveError::test())?;
521
522        creator.try_talk_to(&id, &guest).await.map_err(RecursiveError::test())?;
523        Ok((creator, guest, id))
524    }
525}