core_crypto/mls/credential/
mod.rs1pub(crate) mod credential_ref;
6pub(crate) mod credential_type;
7pub(crate) mod crl;
8mod error;
9mod export_pem;
10pub(crate) mod ext;
11mod persistence;
12pub(crate) mod x509;
13
14use std::sync::Arc;
15
16use core_crypto_keystore::entities::StoredCredential;
17use openmls::prelude::{Credential as MlsCredential, CredentialWithKey, KeyPackage, SignatureScheme};
18use openmls_basic_credential::SignatureKeyPair;
19use openmls_traits::crypto::OpenMlsCrypto;
20use tls_codec::Deserialize as _;
21
22pub(crate) use self::error::Result;
23pub use self::{
24 credential_ref::{CredentialRef, FindFilters, FindFiltersBuilder},
25 credential_type::CredentialType,
26 error::Error,
27};
28use crate::{CipherSuite, ClientId, ClientIdRef, OpenMlsError, RecursiveError, TlsCodecError, mls_provider::CRYPTO};
29
30#[derive(core_crypto_macros::Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct Credential {
39 pub(crate) cipher_suite: CipherSuite,
41 pub(crate) credential_type: CredentialType,
43 pub(crate) mls_credential: MlsCredential,
45 #[sensitive]
47 pub(crate) signature_key_pair: SignatureKeyPair,
48 pub(crate) earliest_validity: u64,
53}
54
55impl TryFrom<Arc<StoredCredential>> for Credential {
56 type Error = Error;
57
58 fn try_from(stored_credential: Arc<StoredCredential>) -> Result<Credential> {
59 let mls_credential = MlsCredential::tls_deserialize(&mut stored_credential.credential.as_slice())
60 .map_err(TlsCodecError::deserialize("mls credential"))?;
61 let cipher_suite = CipherSuite::try_from(stored_credential.ciphersuite)?;
62 let signature_key_pair = openmls_basic_credential::SignatureKeyPair::from_raw(
63 cipher_suite.signature_algorithm(),
64 stored_credential.private_key.to_owned(),
65 stored_credential.public_key.to_owned(),
66 );
67 let credential_type = mls_credential
68 .credential_type()
69 .try_into()
70 .map_err(RecursiveError::context("loading credential from db"))?;
71 let earliest_validity = stored_credential.created_at;
72 Ok(Credential {
73 cipher_suite,
74 signature_key_pair,
75 credential_type,
76 mls_credential,
77 earliest_validity,
78 })
79 }
80}
81
82impl Credential {
83 pub fn basic(cipher_suite: CipherSuite, client_id: ClientId) -> Result<Self> {
90 let signature_scheme = cipher_suite.signature_algorithm();
91 let (private_key, public_key) = CRYPTO
92 .signature_key_gen(signature_scheme)
93 .map_err(OpenMlsError::wrap("generating signature key"))?;
94 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
95
96 Ok(Self {
97 cipher_suite,
98 credential_type: CredentialType::Basic,
99 mls_credential: MlsCredential::new_basic(client_id.into_inner()),
100 signature_key_pair,
101 earliest_validity: 0,
102 })
103 }
104
105 pub(crate) fn public_only(key_package: &KeyPackage) -> Result<Self> {
112 let cipher_suite = CipherSuite::from(key_package.ciphersuite());
113 let leaf_node = key_package.leaf_node();
114 let mls_credential = leaf_node.credential().clone();
115 let credential_type = mls_credential.credential_type().try_into()?;
116 let signature_key_pair = SignatureKeyPair::from_raw(
117 cipher_suite.signature_algorithm(),
118 Vec::new(),
119 leaf_node.signature_key().as_slice().to_vec(),
120 );
121
122 Ok(Self {
123 cipher_suite,
124 credential_type,
125 mls_credential,
126 signature_key_pair,
127 earliest_validity: 0,
128 })
129 }
130
131 pub fn mls_credential(&self) -> &MlsCredential {
135 &self.mls_credential
136 }
137
138 pub fn credential_type(&self) -> CredentialType {
140 self.credential_type
141 }
142
143 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
145 &self.signature_key_pair
146 }
147
148 pub fn signature_key_bytes(&self) -> &[u8] {
150 self.signature_key_pair.private()
151 }
152
153 pub fn signature_scheme(&self) -> SignatureScheme {
155 self.signature_key_pair.signature_scheme()
156 }
157
158 pub fn cipher_suite(&self) -> CipherSuite {
160 self.cipher_suite
161 }
162
163 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
165 CredentialWithKey {
166 credential: self.mls_credential.clone(),
167 signature_key: self.signature_key_pair.to_public_vec().into(),
168 }
169 }
170
171 pub fn earliest_validity(&self) -> u64 {
176 self.earliest_validity
177 }
178
179 pub fn client_id(&self) -> &ClientIdRef {
181 self.mls_credential.identity().into()
182 }
183}
184
185impl From<Credential> for CredentialWithKey {
186 fn from(cb: Credential) -> Self {
187 Self {
188 credential: cb.mls_credential,
189 signature_key: cb.signature_key_pair.public().into(),
190 }
191 }
192}
193
194impl Eq for Credential {}
195impl PartialEq for Credential {
196 fn eq(&self, other: &Self) -> bool {
197 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
198 let sk = &self.signature_key_pair;
199 let ok = &other.signature_key_pair;
200 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
201 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use rusty_jwt_tools::jsonwebtoken::DecodingKey;
209 use wire_e2e_identity::CiphersuiteExt as _;
210
211 use super::{x509::CertificateBundle, *};
212 use crate::{
213 CredentialType, E2eiConversationState, MlsCiphersuite,
214 mls::credential::x509::CertificatePrivateKey,
215 test_utils::{
216 x509::{CertificateParams, PkiKeypair, X509TestChain},
217 *,
218 },
219 };
220
221 #[apply(all_cred_cipher)]
222 async fn basic_clients_can_send_messages(case: TestContext) {
223 if !case.is_basic() {
224 return;
225 }
226 let [alice, bob] = case.sessions_basic().await;
227 let conversation = case.create_conversation([&alice, &bob]).await;
228 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
229 }
230
231 #[apply(all_cred_cipher)]
232 async fn certificate_clients_can_send_messages(case: TestContext) {
233 if !case.is_x509() {
234 return;
235 }
236 let [alice, bob] = case.sessions_x509().await;
237 let conversation = case.create_conversation([&alice, &bob]).await;
238 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
239 }
240
241 #[apply(all_cred_cipher)]
243 async fn signature_key_should_convert_to_pem(case: TestContext) {
244 if !case.is_basic() {
245 return;
246 }
247 let Some(alg) = MlsCiphersuite::from(case.cipher_suite()).jws_algorithm() else {
249 return;
250 };
251 let [client_id] = case.client_ids();
252 let credential = Credential::basic(case.cipher_suite(), client_id).unwrap();
253
254 let pem = alg.keypair_from_bytes(credential.signature_key_bytes()).unwrap();
255 let jwk = alg.public_jwk(&pem).unwrap();
256 let public_key = DecodingKey::from_jwk(&jwk).unwrap();
257 assert_eq!(
258 public_key.try_get_as_bytes().unwrap(),
259 credential.signature_key().public()
260 );
261 }
262
263 #[apply(all_cred_cipher)]
264 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
265 if !case.supports_x509() {
266 return;
267 }
268
269 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
271
272 let (alice, bob) = match case.credential_type {
274 CredentialType::Basic => (x509_session, basic_session),
275 CredentialType::X509 => (basic_session, x509_session),
276 };
277
278 let conversation = case.create_conversation([&alice, &bob]).await;
279 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
280 }
281
282 #[apply(all_cred_cipher)]
283 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
284 if !case.supports_x509() {
285 return;
286 }
287 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
288
289 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
290
291 let mut cert = CertificateBundle::rand(&"alice".into(), x509_intermediate);
292 cert.certificate_chain = vec![];
293 let err = Credential::x509(case.cipher_suite(), cert).unwrap_err();
294
295 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
296 }
297
298 #[apply(all_cred_cipher)]
299 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
300 if !case.is_x509() {
301 return;
302 }
303 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
304 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
305
306 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
307 let new_keypair = CRYPTO.signature_key_gen(case.signature_scheme()).unwrap();
308 let new_pki_kp = PkiKeypair::new(case.signature_scheme(), new_keypair.0).unwrap();
309
310 let eve_key = CertificatePrivateKey::new(new_pki_kp.signing_key_bytes());
311 let cb = CertificateBundle {
312 certificate_chain: certs.certificate_chain,
313 private_key: eve_key,
314 signature_scheme: case.cipher_suite().signature_algorithm(),
315 };
316 let err = Credential::x509(case.cipher_suite(), cb).unwrap_err();
317
318 assert!(innermost_source_matches!(
319 err,
320 crate::OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
321 ));
322 }
323
324 #[apply(all_cred_cipher)]
325 async fn should_fail_when_certificate_signature_doesnt_match_ciphersuite(case: TestContext) {
326 use openmls::prelude::Ciphersuite;
327 if !case.is_x509() {
328 return;
329 }
330
331 let [alice] = case.sessions_x509().await;
332 let conversation = case.create_conversation([&alice]).await;
333
334 let other_cipher_suite = match case.cipher_suite() {
335 CipherSuite(Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) => {
336 CipherSuite(Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256)
337 }
338 _ => CipherSuite(Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519),
339 };
340
341 let mixed_cipher_suite_case = TestContext::new(CredentialType::X509, *other_cipher_suite);
342 let [bob] = mixed_cipher_suite_case.sessions_x509().await;
343
344 let bob_kp = bob
345 .transaction
346 .generate_key_package(&bob.initial_credential, None)
347 .await
348 .unwrap()
349 .into();
350
351 assert!(conversation.guard().await.add_members(vec![bob_kp]).await.is_err());
352 }
353
354 #[apply(all_cred_cipher)]
355 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
356 if !case.is_x509() {
357 return;
358 }
359 let mut x509_test_chain = case.set_test_chain(&[], &[], None).await;
360 let expiration_time = core::time::Duration::from_secs(8);
361 let start = web_time::Instant::now();
362
363 let alice_cert = x509_test_chain.issue_simple_certificate_bundle("alice", None);
364 let alice_cred = Credential::x509(case.cipher_suite(), alice_cert).unwrap();
365 let bob_cert = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
366 let bob_cred = Credential::x509(case.cipher_suite(), bob_cert).unwrap();
367 let alice = SessionContext::new_with_credential(&case, alice_cred, case.sessions_in_memory)
368 .await
369 .unwrap();
370 let bob = SessionContext::new_with_credential(&case, bob_cred, case.sessions_in_memory)
371 .await
372 .unwrap();
373
374 let conversation = case.create_conversation([&alice, &bob]).await;
375 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
377
378 assert_eq!(
379 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
380 E2eiConversationState::Verified
381 );
382
383 let elapsed = start.elapsed();
384 if expiration_time > elapsed {
386 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
387 }
388
389 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
390 assert_eq!(
391 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
392 E2eiConversationState::NotVerified
393 );
394 }
395
396 #[apply(all_cred_cipher)]
397 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
398 if !case.is_x509() {
399 return;
400 }
401 Box::pin(async {
402 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
403 let conversation = case.create_conversation([&alice, &bob]).await;
404
405 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
407 assert_eq!(
408 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
409 E2eiConversationState::Verified
410 );
411 assert_eq!(
412 conversation
413 .guard_of(&bob)
414 .await
415 .e2ei_conversation_state()
416 .await
417 .unwrap(),
418 E2eiConversationState::Verified
419 );
420
421 let conversation = conversation
423 .invite_with_credential_notify([(&charlie, &charlie.initial_credential)])
424 .await;
425
426 assert_eq!(
427 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
428 E2eiConversationState::NotVerified
429 );
430 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
431 assert_eq!(
432 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
433 E2eiConversationState::NotVerified
434 );
435 })
436 .await;
437 }
438
439 #[apply(all_cred_cipher)]
445 async fn saving_an_x509_credential_keeps_the_certificate_validity(case: TestContext) {
446 if !case.is_x509() {
447 return;
448 }
449 let [alice] = case.sessions().await;
450 Box::pin(async move {
451 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
452 let local_ca = x509_test_chain.find_local_intermediate_ca();
453
454 let an_hour_ago = now_std() - core::time::Duration::from_secs(3600);
456 let client_id = alice.get_client_id().await;
459 let cert = local_ca.create_and_sign_end_identity(CertificateParams {
460 common_name: Some("Alice Smith".to_string()),
461 handle: Some("alice_wire".to_string()),
462 client_id: Some(client_id),
463 validity_start: Some(an_hour_ago),
464 ..Default::default()
465 });
466 let bundle = CertificateBundle::from_certificate_and_issuer(&cert, local_ca);
467 let credential = Credential::x509(case.cipher_suite(), bundle).unwrap();
468
469 let from_certificate = credential.earliest_validity();
472 assert!(
473 from_certificate.abs_diff(an_hour_ago.as_secs()) <= 5,
474 "the credential must start out carrying the certificate's `not_before`: \
475 got {from_certificate}, expected about {}",
476 an_hour_ago.as_secs()
477 );
478
479 let credential_ref = alice
480 .add_credential(credential)
481 .await
482 .expect("adding the credential should succeed");
483
484 assert_eq!(
485 credential_ref.earliest_validity(),
486 from_certificate,
487 "persisting must not replace the certificate claim with the insertion time"
488 );
489 })
490 .await;
491 }
492
493 #[apply(all_cred_cipher)]
494 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
495 use crate::OpenMlsErrorKind;
496
497 if !case.is_x509() {
498 return;
499 }
500 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
501
502 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
503 let local_ca = x509_test_chain.find_local_intermediate_ca();
504 let alice_cert = {
505 let name = "alice";
506 let common_name = format!("{name} Smith");
507 let handle = format!("{}_wire", name.to_lowercase());
508 let [client_id] = case.client_ids();
509 local_ca.create_and_sign_end_identity(CertificateParams {
510 common_name: Some(common_name.clone()),
511 handle: Some(handle.clone()),
512 client_id: Some(client_id.clone()),
513 validity_start: Some(tomorrow),
514 ..Default::default()
515 })
516 };
517 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
518 let err = Credential::x509(case.cipher_suite(), cb).unwrap_err();
519
520 assert!(innermost_source_matches!(
521 err,
522 OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
523 ))
524 }
525
526 pub(crate) fn now_std() -> std::time::Duration {
532 let now = web_time::SystemTime::now();
533 now.duration_since(web_time::UNIX_EPOCH).unwrap()
534 }
535}