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, 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::{Ciphersuite, ClientId, ClientIdRef, ClientIdentifier, MlsError, RecursiveError, mls_provider::CRYPTO};
26
27#[derive(core_crypto_macros::Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct Credential {
36 pub(crate) ciphersuite: Ciphersuite,
38 pub(crate) credential_type: CredentialType,
40 pub(crate) mls_credential: MlsCredential,
42 #[sensitive]
44 pub(crate) signature_key_pair: SignatureKeyPair,
45 pub(crate) earliest_validity: u64,
52}
53
54impl TryFrom<&StoredCredential> for Credential {
55 type Error = Error;
56
57 fn try_from(stored_credential: &StoredCredential) -> Result<Credential> {
58 let mls_credential = MlsCredential::tls_deserialize(&mut stored_credential.credential.as_slice())
59 .map_err(Error::tls_deserialize("mls credential"))?;
60 let ciphersuite = Ciphersuite::try_from(stored_credential.ciphersuite)
61 .map_err(RecursiveError::mls("loading ciphersuite from db"))?;
62 let signature_key_pair = openmls_basic_credential::SignatureKeyPair::from_raw(
63 ciphersuite.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::mls_credential("loading credential from db"))?;
71 let earliest_validity = stored_credential.created_at;
72 Ok(Credential {
73 ciphersuite,
74 signature_key_pair,
75 credential_type,
76 mls_credential,
77 earliest_validity,
78 })
79 }
80}
81
82impl Credential {
83 pub fn basic(ciphersuite: Ciphersuite, client_id: ClientId) -> Result<Self> {
90 let signature_scheme = ciphersuite.signature_algorithm();
91 let (private_key, public_key) = CRYPTO
92 .signature_key_gen(signature_scheme)
93 .map_err(MlsError::wrap("generating signature key"))?;
94 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
95
96 Ok(Self {
97 ciphersuite,
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 fn mls_credential(&self) -> &MlsCredential {
109 &self.mls_credential
110 }
111
112 pub fn credential_type(&self) -> CredentialType {
114 self.credential_type
115 }
116
117 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
119 &self.signature_key_pair
120 }
121
122 pub fn signature_scheme(&self) -> SignatureScheme {
124 self.signature_key_pair.signature_scheme()
125 }
126
127 pub fn ciphersuite(&self) -> Ciphersuite {
129 self.ciphersuite
130 }
131
132 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
134 CredentialWithKey {
135 credential: self.mls_credential.clone(),
136 signature_key: self.signature_key_pair.to_public_vec().into(),
137 }
138 }
139
140 pub fn earliest_validity(&self) -> u64 {
148 self.earliest_validity
149 }
150
151 pub fn client_id(&self) -> &ClientIdRef {
153 self.mls_credential.identity().into()
154 }
155}
156
157impl Credential {
158 #[cfg_attr(not(test), expect(dead_code))]
161 pub(crate) fn from_identifier(identifier: &ClientIdentifier, ciphersuite: Ciphersuite) -> Result<Self> {
162 match identifier {
163 ClientIdentifier::Basic(client_id) => Self::basic(ciphersuite, client_id.clone()),
164 ClientIdentifier::X509(certs) => {
165 let signature_scheme = ciphersuite.signature_algorithm();
166 let cert = certs
167 .get(&signature_scheme)
168 .ok_or(Error::SignatureSchemeNotPresentInX509Identity(signature_scheme))?;
169 Self::x509(ciphersuite, cert.clone())
170 }
171 }
172 }
173}
174
175impl From<Credential> for CredentialWithKey {
176 fn from(cb: Credential) -> Self {
177 Self {
178 credential: cb.mls_credential,
179 signature_key: cb.signature_key_pair.public().into(),
180 }
181 }
182}
183
184impl Eq for Credential {}
185impl PartialEq for Credential {
186 fn eq(&self, other: &Self) -> bool {
187 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
188 let sk = &self.signature_key_pair;
189 let ok = &other.signature_key_pair;
190 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
191 }
193 }
194}
195
196#[cfg(test)]
199mod tests {
200 use std::collections::HashMap;
201
202 use super::{x509::CertificateBundle, *};
203 use crate::{
204 ClientIdentifier, CredentialType, E2eiConversationState,
205 mls::{conversation::Conversation as _, credential::x509::CertificatePrivateKey},
206 mls_provider::PkiKeypair,
207 test_utils::{
208 x509::{CertificateParams, X509TestChain},
209 *,
210 },
211 };
212
213 #[apply(all_cred_cipher)]
214 async fn basic_clients_can_send_messages(case: TestContext) {
215 if !case.is_basic() {
216 return;
217 }
218 let [alice, bob] = case.sessions_basic().await;
219 let conversation = case.create_conversation([&alice, &bob]).await;
220 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
221 }
222
223 #[apply(all_cred_cipher)]
224 async fn certificate_clients_can_send_messages(case: TestContext) {
225 if !case.is_x509() {
226 return;
227 }
228 let [alice, bob] = case.sessions_x509().await;
229 let conversation = case.create_conversation([&alice, &bob]).await;
230 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
231 }
232
233 #[apply(all_cred_cipher)]
234 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
235 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
237
238 let (alice, bob) = match case.credential_type {
240 CredentialType::Basic => (x509_session, basic_session),
241 CredentialType::X509 => (basic_session, x509_session),
242 };
243
244 let conversation = case.create_conversation([&alice, &bob]).await;
245 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
246 }
247
248 #[apply(all_cred_cipher)]
249 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
250 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
251
252 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
253
254 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
255 certs.certificate_chain = vec![];
256 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
257
258 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
259 .await
260 .unwrap_err();
261 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
262 }
263
264 #[apply(all_cred_cipher)]
265 async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestContext) {
266 use crate::MlsErrorKind;
267
268 if !case.is_x509() {
269 return;
270 }
271 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
272
273 let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
274
275 let new_cert = alice_cert
276 .pki_keypair
277 .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
278 .unwrap();
279 let mut alice_cert = alice_cert.clone();
280 alice_cert.certificate = new_cert;
281 let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
282 let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
283
284 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
285 .await
286 .unwrap();
287 let [bob] = case.sessions_x509().await;
288 let bob_key_package = bob.new_keypackage(&case).await;
289 let conversation = case.create_conversation([&alice]).await;
290 let err = conversation
291 .guard()
292 .await
293 .add_members([bob_key_package.into()].into())
294 .await
295 .unwrap_err();
296 assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
297 }
298
299 #[apply(all_cred_cipher)]
300 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
301 if !case.is_x509() {
302 return;
303 }
304 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
305 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
306
307 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
308 let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
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.ciphersuite().signature_algorithm(),
315 };
316 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
317
318 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
319 .await
320 .unwrap_err();
321 assert!(innermost_source_matches!(
322 err,
323 crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
324 ));
325 }
326
327 #[apply(all_cred_cipher)]
328 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
329 if !case.is_x509() {
330 return;
331 }
332 Box::pin(async move {
333 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
334
335 let expiration_time = core::time::Duration::from_secs(14);
336 let start = web_time::Instant::now();
337
338 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
339 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
340 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
341 .await
342 .unwrap();
343 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
344 .await
345 .unwrap();
346
347 let conversation = case.create_conversation([&alice, &bob]).await;
348 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
350
351 assert_eq!(
352 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
353 E2eiConversationState::Verified
354 );
355
356 let elapsed = start.elapsed();
357 if expiration_time > elapsed {
359 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
360 }
361
362 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
363 assert_eq!(
364 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
365 E2eiConversationState::NotVerified
366 );
367 })
368 .await;
369 }
370
371 #[apply(all_cred_cipher)]
372 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
373 if !case.is_x509() {
374 return;
375 }
376 Box::pin(async {
377 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
378 let conversation = case.create_conversation([&alice, &bob]).await;
379
380 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
382 assert_eq!(
383 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
384 E2eiConversationState::Verified
385 );
386 assert_eq!(
387 conversation
388 .guard_of(&bob)
389 .await
390 .e2ei_conversation_state()
391 .await
392 .unwrap(),
393 E2eiConversationState::Verified
394 );
395
396 let conversation = conversation
398 .invite_with_credential_notify([(&charlie, &charlie.initial_credential)])
399 .await;
400
401 assert_eq!(
402 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
403 E2eiConversationState::NotVerified
404 );
405 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
406 assert_eq!(
407 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
408 E2eiConversationState::NotVerified
409 );
410 })
411 .await;
412 }
413
414 #[apply(all_cred_cipher)]
415 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
416 use crate::MlsErrorKind;
417
418 if !case.is_x509() {
419 return;
420 }
421 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
422
423 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
424 let local_ca = x509_test_chain.find_local_intermediate_ca();
425 let alice_cert = {
426 let name = "alice";
427 let common_name = format!("{name} Smith");
428 let handle = format!("{}_wire", name.to_lowercase());
429 let client_id: String = crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
430 .try_into()
431 .unwrap();
432 local_ca.create_and_sign_end_identity(CertificateParams {
433 common_name: Some(common_name.clone()),
434 handle: Some(handle.clone()),
435 client_id: Some(client_id.clone()),
436 validity_start: Some(tomorrow),
437 ..Default::default()
438 })
439 };
440 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
441 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
442 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
443 .await
444 .unwrap_err();
445
446 assert!(innermost_source_matches!(
447 err,
448 MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
449 ))
450 }
451
452 pub(crate) fn now_std() -> std::time::Duration {
458 let now = web_time::SystemTime::now();
459 now.duration_since(web_time::UNIX_EPOCH).unwrap()
460 }
461}