core_crypto/mls/credential/
mod.rs1pub(crate) mod credential_ref;
6pub(crate) mod credential_type;
7pub(crate) mod crl;
8mod error;
9pub(crate) mod ext;
10mod persistence;
11pub(crate) mod x509;
12
13use core_crypto_keystore::entities::StoredCredential;
14use openmls::prelude::{Credential as MlsCredential, CredentialWithKey, MlsCredentialType, SignatureScheme};
15use openmls_basic_credential::SignatureKeyPair;
16use openmls_traits::crypto::OpenMlsCrypto;
17use tls_codec::Deserialize as _;
18
19pub(crate) use self::error::Result;
20pub use self::{
21 credential_ref::{CredentialRef, FindFilters, FindFiltersBuilder},
22 credential_type::CredentialType,
23 error::Error,
24};
25use crate::{
26 Ciphersuite, ClientId, ClientIdRef, ClientIdentifier, MlsError, RecursiveError,
27 mls::credential::{error::CredentialValidationError, ext::CredentialExt as _},
28};
29
30#[derive(core_crypto_macros::Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct Credential {
50 pub(crate) ciphersuite: Ciphersuite,
52 pub(crate) credential_type: CredentialType,
54 pub(crate) mls_credential: MlsCredential,
56 #[sensitive]
58 pub(crate) signature_key_pair: SignatureKeyPair,
59 pub(crate) earliest_validity: u64,
66}
67
68impl TryFrom<&StoredCredential> for Credential {
69 type Error = Error;
70
71 fn try_from(stored_credential: &StoredCredential) -> Result<Credential> {
72 let mls_credential = MlsCredential::tls_deserialize(&mut stored_credential.credential.as_slice())
73 .map_err(Error::tls_deserialize("mls credential"))?;
74 let ciphersuite = Ciphersuite::try_from(stored_credential.ciphersuite)
75 .map_err(RecursiveError::mls("loading ciphersuite from db"))?;
76 let signature_key_pair = openmls_basic_credential::SignatureKeyPair::from_raw(
77 ciphersuite.signature_algorithm(),
78 stored_credential.private_key.to_owned(),
79 stored_credential.public_key.to_owned(),
80 );
81 let credential_type = mls_credential
82 .credential_type()
83 .try_into()
84 .map_err(RecursiveError::mls_credential("loading credential from db"))?;
85 let earliest_validity = stored_credential.created_at;
86 Ok(Credential {
87 ciphersuite,
88 signature_key_pair,
89 credential_type,
90 mls_credential,
91 earliest_validity,
92 })
93 }
94}
95
96impl Credential {
97 pub(crate) fn validate_mls_credential(
99 mls_credential: &MlsCredential,
100 client_id: &ClientIdRef,
101 signature_key: &SignatureKeyPair,
102 ) -> Result<(), CredentialValidationError> {
103 match mls_credential.mls_credential() {
104 MlsCredentialType::Basic(_) => {
105 if client_id.as_slice() != mls_credential.identity() {
106 return Err(CredentialValidationError::WrongCredential);
107 }
108 }
109 MlsCredentialType::X509(cert) => {
110 let certificate_public_key = cert
111 .extract_public_key()
112 .map_err(RecursiveError::mls_credential(
113 "extracting public key from certificate in credential validation",
114 ))?
115 .ok_or(CredentialValidationError::NoPublicKey)?;
116 if signature_key.public() != certificate_public_key {
117 return Err(CredentialValidationError::WrongCredential);
118 }
119 }
120 }
121 Ok(())
122 }
123
124 pub fn basic(ciphersuite: Ciphersuite, client_id: ClientId, crypto: impl OpenMlsCrypto) -> Result<Self> {
131 let signature_scheme = ciphersuite.signature_algorithm();
132 let (private_key, public_key) = crypto
133 .signature_key_gen(signature_scheme)
134 .map_err(MlsError::wrap("generating signature key"))?;
135 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
136
137 Ok(Self {
138 ciphersuite,
139 credential_type: CredentialType::Basic,
140 mls_credential: MlsCredential::new_basic(client_id.into_inner()),
141 signature_key_pair,
142 earliest_validity: 0,
143 })
144 }
145
146 pub fn mls_credential(&self) -> &MlsCredential {
150 &self.mls_credential
151 }
152
153 pub fn credential_type(&self) -> CredentialType {
155 self.credential_type
156 }
157
158 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
160 &self.signature_key_pair
161 }
162
163 pub fn signature_scheme(&self) -> SignatureScheme {
165 self.signature_key_pair.signature_scheme()
166 }
167
168 pub fn ciphersuite(&self) -> Ciphersuite {
170 self.ciphersuite
171 }
172
173 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
175 CredentialWithKey {
176 credential: self.mls_credential.clone(),
177 signature_key: self.signature_key_pair.to_public_vec().into(),
178 }
179 }
180
181 pub fn earliest_validity(&self) -> u64 {
189 self.earliest_validity
190 }
191
192 pub fn client_id(&self) -> &ClientIdRef {
194 self.mls_credential.identity().into()
195 }
196}
197
198impl Credential {
199 #[cfg_attr(not(test), expect(dead_code))]
202 pub(crate) fn from_identifier(
203 identifier: &ClientIdentifier,
204 ciphersuite: Ciphersuite,
205 crypto: impl OpenMlsCrypto,
206 ) -> Result<Self> {
207 match identifier {
208 ClientIdentifier::Basic(client_id) => Self::basic(ciphersuite, client_id.clone(), crypto),
209 ClientIdentifier::X509(certs) => {
210 let signature_scheme = ciphersuite.signature_algorithm();
211 let cert = certs
212 .get(&signature_scheme)
213 .ok_or(Error::SignatureSchemeNotPresentInX509Identity(signature_scheme))?;
214 Self::x509(ciphersuite, cert.clone())
215 }
216 }
217 }
218}
219
220impl From<Credential> for CredentialWithKey {
221 fn from(cb: Credential) -> Self {
222 Self {
223 credential: cb.mls_credential,
224 signature_key: cb.signature_key_pair.public().into(),
225 }
226 }
227}
228
229impl Eq for Credential {}
230impl PartialEq for Credential {
231 fn eq(&self, other: &Self) -> bool {
232 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
233 let sk = &self.signature_key_pair;
234 let ok = &other.signature_key_pair;
235 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
236 }
238 }
239}
240
241#[cfg(test)]
244mod tests {
245 use std::collections::HashMap;
246
247 use mls_crypto_provider::PkiKeypair;
248
249 use super::{x509::CertificateBundle, *};
250 use crate::{
251 ClientIdentifier, CredentialType, E2eiConversationState,
252 mls::{conversation::Conversation as _, credential::x509::CertificatePrivateKey},
253 test_utils::{
254 x509::{CertificateParams, X509TestChain},
255 *,
256 },
257 };
258
259 #[apply(all_cred_cipher)]
260 async fn basic_clients_can_send_messages(case: TestContext) {
261 if !case.is_basic() {
262 return;
263 }
264 let [alice, bob] = case.sessions_basic().await;
265 let conversation = case.create_conversation([&alice, &bob]).await;
266 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
267 }
268
269 #[apply(all_cred_cipher)]
270 async fn certificate_clients_can_send_messages(case: TestContext) {
271 if !case.is_x509() {
272 return;
273 }
274 let [alice, bob] = case.sessions_x509().await;
275 let conversation = case.create_conversation([&alice, &bob]).await;
276 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
277 }
278
279 #[apply(all_cred_cipher)]
280 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
281 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
283 let (alice, bob, alice_credential_type) = match case.credential_type {
285 CredentialType::Basic => (x509_session, basic_session, CredentialType::X509),
286 CredentialType::X509 => (basic_session, x509_session, CredentialType::Basic),
287 };
288
289 let conversation = case
290 .create_heterogeneous_conversation(alice_credential_type, case.credential_type, [&alice, &bob])
291 .await;
292 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
293 }
294
295 #[apply(all_cred_cipher)]
296 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
297 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
298
299 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
300
301 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
302 certs.certificate_chain = vec![];
303 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
304
305 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
306 .await
307 .unwrap_err();
308 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
309 }
310
311 #[apply(all_cred_cipher)]
312 async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestContext) {
313 use crate::MlsErrorKind;
314
315 if !case.is_x509() {
316 return;
317 }
318 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
319
320 let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
321
322 let new_cert = alice_cert
323 .pki_keypair
324 .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
325 .unwrap();
326 let mut alice_cert = alice_cert.clone();
327 alice_cert.certificate = new_cert;
328 let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
329 let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
330
331 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
332 .await
333 .unwrap();
334 let [bob] = case.sessions_x509().await;
335 let bob_key_package = bob.new_keypackage(&case).await;
336 let conversation = case.create_conversation([&alice]).await;
337 let err = conversation
338 .guard()
339 .await
340 .add_members([bob_key_package.into()].into())
341 .await
342 .unwrap_err();
343 assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
344 }
345
346 #[apply(all_cred_cipher)]
347 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
348 if !case.is_x509() {
349 return;
350 }
351 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
352 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
353
354 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
355 let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
356
357 let eve_key = CertificatePrivateKey::new(new_pki_kp.signing_key_bytes());
358 let cb = CertificateBundle {
359 certificate_chain: certs.certificate_chain,
360 private_key: eve_key,
361 signature_scheme: case.ciphersuite().signature_algorithm(),
362 };
363 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
364
365 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
366 .await
367 .unwrap_err();
368 assert!(innermost_source_matches!(
369 err,
370 crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
371 ));
372 }
373
374 #[apply(all_cred_cipher)]
375 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
376 if !case.is_x509() {
377 return;
378 }
379 Box::pin(async move {
380 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
381
382 let expiration_time = core::time::Duration::from_secs(14);
383 let start = web_time::Instant::now();
384
385 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
386 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
387 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
388 .await
389 .unwrap();
390 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
391 .await
392 .unwrap();
393
394 let conversation = case.create_conversation([&alice, &bob]).await;
395 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
397
398 assert_eq!(
399 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
400 E2eiConversationState::Verified
401 );
402
403 let elapsed = start.elapsed();
404 if expiration_time > elapsed {
406 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
407 }
408
409 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
410 assert_eq!(
411 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
412 E2eiConversationState::NotVerified
413 );
414 })
415 .await;
416 }
417
418 #[apply(all_cred_cipher)]
419 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
420 if !case.is_x509() {
421 return;
422 }
423 Box::pin(async {
424 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
425 let conversation = case.create_conversation([&alice, &bob]).await;
426
427 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
429 assert_eq!(
430 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
431 E2eiConversationState::Verified
432 );
433 assert_eq!(
434 conversation
435 .guard_of(&bob)
436 .await
437 .e2ei_conversation_state()
438 .await
439 .unwrap(),
440 E2eiConversationState::Verified
441 );
442
443 let conversation = conversation
445 .invite_with_credential_type_notify(CredentialType::Basic, [&charlie])
446 .await;
447
448 assert_eq!(
449 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
450 E2eiConversationState::NotVerified
451 );
452 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
453 assert_eq!(
454 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
455 E2eiConversationState::NotVerified
456 );
457 })
458 .await;
459 }
460
461 #[apply(all_cred_cipher)]
462 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
463 use crate::MlsErrorKind;
464
465 if !case.is_x509() {
466 return;
467 }
468 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
469
470 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
471 let local_ca = x509_test_chain.find_local_intermediate_ca();
472 let alice_cert = {
473 let name = "alice";
474 let common_name = format!("{name} Smith");
475 let handle = format!("{}_wire", name.to_lowercase());
476 let client_id: String = crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
477 .try_into()
478 .unwrap();
479 local_ca.create_and_sign_end_identity(CertificateParams {
480 common_name: Some(common_name.clone()),
481 handle: Some(handle.clone()),
482 client_id: Some(client_id.clone()),
483 validity_start: Some(tomorrow),
484 ..Default::default()
485 })
486 };
487 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
488 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
489 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
490 .await
491 .unwrap_err();
492
493 assert!(innermost_source_matches!(
494 err,
495 MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
496 ))
497 }
498
499 pub(crate) fn now_std() -> std::time::Duration {
505 let now = web_time::SystemTime::now();
506 now.duration_since(web_time::UNIX_EPOCH).unwrap()
507 }
508}