core_crypto/mls/credential/
mod.rs1use 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#[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 {
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 }
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 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 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 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 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 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}