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,
55}
56
57impl TryFrom<Arc<StoredCredential>> for Credential {
58 type Error = Error;
59
60 fn try_from(stored_credential: Arc<StoredCredential>) -> Result<Credential> {
61 let mls_credential = MlsCredential::tls_deserialize(&mut stored_credential.credential.as_slice())
62 .map_err(TlsCodecError::deserialize("mls credential"))?;
63 let cipher_suite = CipherSuite::try_from(stored_credential.ciphersuite)?;
64 let signature_key_pair = openmls_basic_credential::SignatureKeyPair::from_raw(
65 cipher_suite.signature_algorithm(),
66 stored_credential.private_key.to_owned(),
67 stored_credential.public_key.to_owned(),
68 );
69 let credential_type = mls_credential
70 .credential_type()
71 .try_into()
72 .map_err(RecursiveError::context("loading credential from db"))?;
73 let earliest_validity = stored_credential.created_at;
74 Ok(Credential {
75 cipher_suite,
76 signature_key_pair,
77 credential_type,
78 mls_credential,
79 earliest_validity,
80 })
81 }
82}
83
84impl Credential {
85 pub fn basic(cipher_suite: CipherSuite, client_id: ClientId) -> Result<Self> {
92 let signature_scheme = cipher_suite.signature_algorithm();
93 let (private_key, public_key) = CRYPTO
94 .signature_key_gen(signature_scheme)
95 .map_err(OpenMlsError::wrap("generating signature key"))?;
96 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
97
98 Ok(Self {
99 cipher_suite,
100 credential_type: CredentialType::Basic,
101 mls_credential: MlsCredential::new_basic(client_id.into_inner()),
102 signature_key_pair,
103 earliest_validity: 0,
104 })
105 }
106
107 pub(crate) fn public_only(key_package: &KeyPackage) -> Result<Self> {
114 let cipher_suite = CipherSuite::from(key_package.ciphersuite());
115 let leaf_node = key_package.leaf_node();
116 let mls_credential = leaf_node.credential().clone();
117 let credential_type = mls_credential.credential_type().try_into()?;
118 let signature_key_pair = SignatureKeyPair::from_raw(
119 cipher_suite.signature_algorithm(),
120 Vec::new(),
121 leaf_node.signature_key().as_slice().to_vec(),
122 );
123
124 Ok(Self {
125 cipher_suite,
126 credential_type,
127 mls_credential,
128 signature_key_pair,
129 earliest_validity: 0,
130 })
131 }
132
133 pub fn mls_credential(&self) -> &MlsCredential {
137 &self.mls_credential
138 }
139
140 pub fn credential_type(&self) -> CredentialType {
142 self.credential_type
143 }
144
145 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
147 &self.signature_key_pair
148 }
149
150 pub fn signature_key_bytes(&self) -> &[u8] {
153 self.signature_key_pair.private()
154 }
155
156 pub fn signature_scheme(&self) -> SignatureScheme {
158 self.signature_key_pair.signature_scheme()
159 }
160
161 pub fn cipher_suite(&self) -> CipherSuite {
163 self.cipher_suite
164 }
165
166 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
168 CredentialWithKey {
169 credential: self.mls_credential.clone(),
170 signature_key: self.signature_key_pair.to_public_vec().into(),
171 }
172 }
173
174 pub fn earliest_validity(&self) -> u64 {
182 self.earliest_validity
183 }
184
185 pub fn client_id(&self) -> &ClientIdRef {
187 self.mls_credential.identity().into()
188 }
189}
190
191impl From<Credential> for CredentialWithKey {
192 fn from(cb: Credential) -> Self {
193 Self {
194 credential: cb.mls_credential,
195 signature_key: cb.signature_key_pair.public().into(),
196 }
197 }
198}
199
200impl Eq for Credential {}
201impl PartialEq for Credential {
202 fn eq(&self, other: &Self) -> bool {
203 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
204 let sk = &self.signature_key_pair;
205 let ok = &other.signature_key_pair;
206 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
207 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::{x509::CertificateBundle, *};
215 use crate::{
216 CredentialType, E2eiConversationState,
217 mls::credential::x509::CertificatePrivateKey,
218 mls_provider::PkiKeypair,
219 test_utils::{
220 x509::{CertificateParams, X509TestChain},
221 *,
222 },
223 };
224
225 #[apply(all_cred_cipher)]
226 async fn basic_clients_can_send_messages(case: TestContext) {
227 if !case.is_basic() {
228 return;
229 }
230 let [alice, bob] = case.sessions_basic().await;
231 let conversation = case.create_conversation([&alice, &bob]).await;
232 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
233 }
234
235 #[apply(all_cred_cipher)]
236 async fn certificate_clients_can_send_messages(case: TestContext) {
237 if !case.is_x509() {
238 return;
239 }
240 let [alice, bob] = case.sessions_x509().await;
241 let conversation = case.create_conversation([&alice, &bob]).await;
242 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
243 }
244
245 #[apply(all_cred_cipher)]
246 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
247 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
249
250 let (alice, bob) = match case.credential_type {
252 CredentialType::Basic => (x509_session, basic_session),
253 CredentialType::X509 => (basic_session, x509_session),
254 };
255
256 let conversation = case.create_conversation([&alice, &bob]).await;
257 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
258 }
259
260 #[apply(all_cred_cipher)]
261 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
262 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
263
264 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
265
266 let mut cert = CertificateBundle::rand(&"alice".into(), x509_intermediate);
267 cert.certificate_chain = vec![];
268 let err = Credential::x509(case.cipher_suite(), cert).unwrap_err();
269
270 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
271 }
272
273 #[apply(all_cred_cipher)]
274 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
275 if !case.is_x509() {
276 return;
277 }
278 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
279 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
280
281 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
282 let new_pki_kp = PkiKeypair::rand(case.signature_scheme(), CRYPTO.as_ref()).unwrap();
283
284 let eve_key = CertificatePrivateKey::new(new_pki_kp.signing_key_bytes());
285 let cb = CertificateBundle {
286 certificate_chain: certs.certificate_chain,
287 private_key: eve_key,
288 signature_scheme: case.cipher_suite().signature_algorithm(),
289 };
290 let err = Credential::x509(case.cipher_suite(), cb).unwrap_err();
291
292 assert!(innermost_source_matches!(
293 err,
294 crate::OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
295 ));
296 }
297
298 #[apply(all_cred_cipher)]
299 async fn should_fail_when_certificate_signature_doesnt_match_ciphersuite(case: TestContext) {
300 use openmls::prelude::Ciphersuite;
301 if !case.is_x509() {
302 return;
303 }
304
305 let [alice] = case.sessions_x509().await;
306 let conversation = case.create_conversation([&alice]).await;
307
308 let other_cipher_suite = match case.cipher_suite() {
309 CipherSuite(Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) => {
310 CipherSuite(Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256)
311 }
312 _ => CipherSuite(Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519),
313 };
314
315 let mixed_cipher_suite_case = TestContext::new(CredentialType::X509, *other_cipher_suite);
316 let [bob] = mixed_cipher_suite_case.sessions_x509().await;
317
318 let bob_kp = bob
319 .transaction
320 .generate_key_package(&bob.initial_credential, None)
321 .await
322 .unwrap()
323 .into();
324
325 assert!(conversation.guard().await.add_members(vec![bob_kp]).await.is_err());
326 }
327
328 #[apply(all_cred_cipher)]
329 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
330 if !case.is_x509() {
331 return;
332 }
333 Box::pin(async move {
334 let mut x509_test_chain = case.set_test_chain(&[], &[], None).await;
335 let expiration_time = core::time::Duration::from_secs(5);
336 let start = web_time::Instant::now();
337
338 let alice_cert = x509_test_chain.issue_simple_certificate_bundle("alice", None);
339 let alice_cred = Credential::x509(case.cipher_suite(), alice_cert).unwrap();
340 let bob_cert = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
341 let bob_cred = Credential::x509(case.cipher_suite(), bob_cert).unwrap();
342 let alice = SessionContext::new_with_credential(&case, alice_cred, case.sessions_in_memory)
343 .await
344 .unwrap();
345 let bob = SessionContext::new_with_credential(&case, bob_cred, case.sessions_in_memory)
346 .await
347 .unwrap();
348
349 let conversation = case.create_conversation([&alice, &bob]).await;
350 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
352
353 assert_eq!(
354 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
355 E2eiConversationState::Verified
356 );
357
358 let elapsed = start.elapsed();
359 if expiration_time > elapsed {
361 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
362 }
363
364 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
365 assert_eq!(
366 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
367 E2eiConversationState::NotVerified
368 );
369 })
370 .await;
371 }
372
373 #[apply(all_cred_cipher)]
374 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
375 if !case.is_x509() {
376 return;
377 }
378 Box::pin(async {
379 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
380 let conversation = case.create_conversation([&alice, &bob]).await;
381
382 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
384 assert_eq!(
385 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
386 E2eiConversationState::Verified
387 );
388 assert_eq!(
389 conversation
390 .guard_of(&bob)
391 .await
392 .e2ei_conversation_state()
393 .await
394 .unwrap(),
395 E2eiConversationState::Verified
396 );
397
398 let conversation = conversation
400 .invite_with_credential_notify([(&charlie, &charlie.initial_credential)])
401 .await;
402
403 assert_eq!(
404 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
405 E2eiConversationState::NotVerified
406 );
407 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
408 assert_eq!(
409 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
410 E2eiConversationState::NotVerified
411 );
412 })
413 .await;
414 }
415
416 #[apply(all_cred_cipher)]
417 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
418 use crate::OpenMlsErrorKind;
419
420 if !case.is_x509() {
421 return;
422 }
423 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
424
425 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
426 let local_ca = x509_test_chain.find_local_intermediate_ca();
427 let alice_cert = {
428 let name = "alice";
429 let common_name = format!("{name} Smith");
430 let handle = format!("{}_wire", name.to_lowercase());
431 let [client_id] = case.client_ids();
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 err = Credential::x509(case.cipher_suite(), cb).unwrap_err();
442
443 assert!(innermost_source_matches!(
444 err,
445 OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
446 ))
447 }
448
449 pub(crate) fn now_std() -> std::time::Duration {
455 let now = web_time::SystemTime::now();
456 now.duration_since(web_time::UNIX_EPOCH).unwrap()
457 }
458}