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, ClientIdentifier, 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 Credential {
165 #[cfg_attr(not(test), expect(dead_code))]
168 pub(crate) fn from_identifier(identifier: &ClientIdentifier, cipher_suite: CipherSuite) -> Result<Self> {
169 match identifier {
170 ClientIdentifier::Basic(client_id) => Self::basic(cipher_suite, client_id.clone()),
171 ClientIdentifier::X509(certs) => {
172 let signature_scheme = cipher_suite.signature_algorithm();
173 let cert = certs
174 .get(&signature_scheme)
175 .ok_or(Error::SignatureSchemeNotPresentInX509Identity(signature_scheme))?;
176 Self::x509(cipher_suite, cert.clone())
177 }
178 }
179 }
180}
181
182impl From<Credential> for CredentialWithKey {
183 fn from(cb: Credential) -> Self {
184 Self {
185 credential: cb.mls_credential,
186 signature_key: cb.signature_key_pair.public().into(),
187 }
188 }
189}
190
191impl Eq for Credential {}
192impl PartialEq for Credential {
193 fn eq(&self, other: &Self) -> bool {
194 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
195 let sk = &self.signature_key_pair;
196 let ok = &other.signature_key_pair;
197 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
198 }
200 }
201}
202
203#[cfg(test)]
206mod tests {
207 use std::collections::HashMap;
208
209 use super::{x509::CertificateBundle, *};
210 use crate::{
211 ClientIdentifier, CredentialType, E2eiConversationState,
212 mls::credential::x509::CertificatePrivateKey,
213 mls_provider::PkiKeypair,
214 test_utils::{
215 x509::{CertificateParams, X509TestChain},
216 *,
217 },
218 };
219
220 #[apply(all_cred_cipher)]
221 async fn basic_clients_can_send_messages(case: TestContext) {
222 if !case.is_basic() {
223 return;
224 }
225 let [alice, bob] = case.sessions_basic().await;
226 let conversation = case.create_conversation([&alice, &bob]).await;
227 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
228 }
229
230 #[apply(all_cred_cipher)]
231 async fn certificate_clients_can_send_messages(case: TestContext) {
232 if !case.is_x509() {
233 return;
234 }
235 let [alice, bob] = case.sessions_x509().await;
236 let conversation = case.create_conversation([&alice, &bob]).await;
237 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
238 }
239
240 #[apply(all_cred_cipher)]
241 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
242 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
244
245 let (alice, bob) = match case.credential_type {
247 CredentialType::Basic => (x509_session, basic_session),
248 CredentialType::X509 => (basic_session, x509_session),
249 };
250
251 let conversation = case.create_conversation([&alice, &bob]).await;
252 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
253 }
254
255 #[apply(all_cred_cipher)]
256 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
257 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
258
259 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
260
261 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
262 certs.certificate_chain = vec![];
263 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
264
265 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
266 .await
267 .unwrap_err();
268 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
269 }
270
271 #[apply(all_cred_cipher)]
272 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
273 if !case.is_x509() {
274 return;
275 }
276 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
277 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
278
279 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
280 let new_pki_kp = PkiKeypair::rand(case.signature_scheme(), CRYPTO.as_ref()).unwrap();
281
282 let eve_key = CertificatePrivateKey::new(new_pki_kp.signing_key_bytes());
283 let cb = CertificateBundle {
284 certificate_chain: certs.certificate_chain,
285 private_key: eve_key,
286 signature_scheme: case.cipher_suite().signature_algorithm(),
287 };
288 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
289
290 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
291 .await
292 .unwrap_err();
293 assert!(innermost_source_matches!(
294 err,
295 crate::OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
296 ));
297 }
298
299 #[apply(all_cred_cipher)]
300 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
301 if !case.is_x509() {
302 return;
303 }
304 Box::pin(async move {
305 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
306
307 let expiration_time = core::time::Duration::from_secs(14);
308 let start = web_time::Instant::now();
309
310 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
311 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
312 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
313 .await
314 .unwrap();
315 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
316 .await
317 .unwrap();
318
319 let conversation = case.create_conversation([&alice, &bob]).await;
320 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
322
323 assert_eq!(
324 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
325 E2eiConversationState::Verified
326 );
327
328 let elapsed = start.elapsed();
329 if expiration_time > elapsed {
331 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
332 }
333
334 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
335 assert_eq!(
336 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
337 E2eiConversationState::NotVerified
338 );
339 })
340 .await;
341 }
342
343 #[apply(all_cred_cipher)]
344 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
345 if !case.is_x509() {
346 return;
347 }
348 Box::pin(async {
349 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
350 let conversation = case.create_conversation([&alice, &bob]).await;
351
352 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
354 assert_eq!(
355 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
356 E2eiConversationState::Verified
357 );
358 assert_eq!(
359 conversation
360 .guard_of(&bob)
361 .await
362 .e2ei_conversation_state()
363 .await
364 .unwrap(),
365 E2eiConversationState::Verified
366 );
367
368 let conversation = conversation
370 .invite_with_credential_notify([(&charlie, &charlie.initial_credential)])
371 .await;
372
373 assert_eq!(
374 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
375 E2eiConversationState::NotVerified
376 );
377 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
378 assert_eq!(
379 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
380 E2eiConversationState::NotVerified
381 );
382 })
383 .await;
384 }
385
386 #[apply(all_cred_cipher)]
387 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
388 use crate::OpenMlsErrorKind;
389
390 if !case.is_x509() {
391 return;
392 }
393 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
394
395 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
396 let local_ca = x509_test_chain.find_local_intermediate_ca();
397 let alice_cert = {
398 let name = "alice";
399 let common_name = format!("{name} Smith");
400 let handle = format!("{}_wire", name.to_lowercase());
401 let client_id = crate::test_utils::x509::qualified_e2ei_cid_with_domain("wire.com");
402 local_ca.create_and_sign_end_identity(CertificateParams {
403 common_name: Some(common_name.clone()),
404 handle: Some(handle.clone()),
405 client_id: Some(client_id.clone()),
406 validity_start: Some(tomorrow),
407 ..Default::default()
408 })
409 };
410 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
411 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
412 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
413 .await
414 .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}