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