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, OpenMlsError, RecursiveError, mls_provider::CRYPTO};
26
27#[derive(core_crypto_macros::Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct Credential {
36 pub(crate) cipher_suite: 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 cipher_suite = CipherSuite::try_from(stored_credential.ciphersuite)
61 .map_err(RecursiveError::mls("loading cipher suite from db"))?;
62 let signature_key_pair = openmls_basic_credential::SignatureKeyPair::from_raw(
63 cipher_suite.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 cipher_suite,
74 signature_key_pair,
75 credential_type,
76 mls_credential,
77 earliest_validity,
78 })
79 }
80}
81
82impl Credential {
83 pub fn basic(cipher_suite: CipherSuite, client_id: ClientId) -> Result<Self> {
90 let signature_scheme = cipher_suite.signature_algorithm();
91 let (private_key, public_key) = CRYPTO
92 .signature_key_gen(signature_scheme)
93 .map_err(OpenMlsError::wrap("generating signature key"))?;
94 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
95
96 Ok(Self {
97 cipher_suite,
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_key_bytes(&self) -> &[u8] {
125 self.signature_key_pair.private()
126 }
127
128 pub fn signature_scheme(&self) -> SignatureScheme {
130 self.signature_key_pair.signature_scheme()
131 }
132
133 pub fn cipher_suite(&self) -> CipherSuite {
135 self.cipher_suite
136 }
137
138 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
140 CredentialWithKey {
141 credential: self.mls_credential.clone(),
142 signature_key: self.signature_key_pair.to_public_vec().into(),
143 }
144 }
145
146 pub fn earliest_validity(&self) -> u64 {
154 self.earliest_validity
155 }
156
157 pub fn client_id(&self) -> &ClientIdRef {
159 self.mls_credential.identity().into()
160 }
161}
162
163impl Credential {
164 #[cfg_attr(not(test), expect(dead_code))]
167 pub(crate) fn from_identifier(identifier: &ClientIdentifier, cipher_suite: CipherSuite) -> Result<Self> {
168 match identifier {
169 ClientIdentifier::Basic(client_id) => Self::basic(cipher_suite, client_id.clone()),
170 ClientIdentifier::X509(certs) => {
171 let signature_scheme = cipher_suite.signature_algorithm();
172 let cert = certs
173 .get(&signature_scheme)
174 .ok_or(Error::SignatureSchemeNotPresentInX509Identity(signature_scheme))?;
175 Self::x509(cipher_suite, cert.clone())
176 }
177 }
178 }
179}
180
181impl From<Credential> for CredentialWithKey {
182 fn from(cb: Credential) -> Self {
183 Self {
184 credential: cb.mls_credential,
185 signature_key: cb.signature_key_pair.public().into(),
186 }
187 }
188}
189
190impl Eq for Credential {}
191impl PartialEq for Credential {
192 fn eq(&self, other: &Self) -> bool {
193 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
194 let sk = &self.signature_key_pair;
195 let ok = &other.signature_key_pair;
196 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
197 }
199 }
200}
201
202#[cfg(test)]
205mod tests {
206 use std::collections::HashMap;
207
208 use super::{x509::CertificateBundle, *};
209 use crate::{
210 ClientIdentifier, CredentialType, E2eiConversationState,
211 mls::credential::x509::CertificatePrivateKey,
212 mls_provider::PkiKeypair,
213 test_utils::{
214 x509::{CertificateParams, X509TestChain},
215 *,
216 },
217 };
218
219 #[apply(all_cred_cipher)]
220 async fn basic_clients_can_send_messages(case: TestContext) {
221 if !case.is_basic() {
222 return;
223 }
224 let [alice, bob] = case.sessions_basic().await;
225 let conversation = case.create_conversation([&alice, &bob]).await;
226 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
227 }
228
229 #[apply(all_cred_cipher)]
230 async fn certificate_clients_can_send_messages(case: TestContext) {
231 if !case.is_x509() {
232 return;
233 }
234 let [alice, bob] = case.sessions_x509().await;
235 let conversation = case.create_conversation([&alice, &bob]).await;
236 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
237 }
238
239 #[ignore]
241 #[apply(all_cred_cipher)]
242 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
243 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
245
246 let (alice, bob) = match case.credential_type {
248 CredentialType::Basic => (x509_session, basic_session),
249 CredentialType::X509 => (basic_session, x509_session),
250 };
251
252 let conversation = case.create_conversation([&alice, &bob]).await;
253 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
254 }
255
256 #[ignore]
258 #[apply(all_cred_cipher)]
259 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
260 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
261
262 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
263
264 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
265 certs.certificate_chain = vec![];
266 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
267
268 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
269 .await
270 .unwrap_err();
271 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
272 }
273
274 #[apply(all_cred_cipher)]
275 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
276 if !case.is_x509() {
277 return;
278 }
279 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
280 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
281
282 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
283 let new_pki_kp = PkiKeypair::rand(case.signature_scheme(), CRYPTO.as_ref()).unwrap();
284
285 let eve_key = CertificatePrivateKey::new(new_pki_kp.signing_key_bytes());
286 let cb = CertificateBundle {
287 certificate_chain: certs.certificate_chain,
288 private_key: eve_key,
289 signature_scheme: case.cipher_suite().signature_algorithm(),
290 };
291 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
292
293 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
294 .await
295 .unwrap_err();
296 assert!(innermost_source_matches!(
297 err,
298 crate::OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
299 ));
300 }
301
302 #[apply(all_cred_cipher)]
303 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
304 if !case.is_x509() {
305 return;
306 }
307 Box::pin(async move {
308 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
309
310 let expiration_time = core::time::Duration::from_secs(14);
311 let start = web_time::Instant::now();
312
313 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
314 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
315 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
316 .await
317 .unwrap();
318 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
319 .await
320 .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: String = crate::test_utils::x509::qualified_e2ei_cid_with_domain("wire.com")
405 .try_into()
406 .unwrap();
407 local_ca.create_and_sign_end_identity(CertificateParams {
408 common_name: Some(common_name.clone()),
409 handle: Some(handle.clone()),
410 client_id: Some(client_id.clone()),
411 validity_start: Some(tomorrow),
412 ..Default::default()
413 })
414 };
415 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
416 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
417 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
418 .await
419 .unwrap_err();
420
421 assert!(innermost_source_matches!(
422 err,
423 OpenMlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
424 ))
425 }
426
427 pub(crate) fn now_std() -> std::time::Duration {
433 let now = web_time::SystemTime::now();
434 now.duration_since(web_time::UNIX_EPOCH).unwrap()
435 }
436}