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