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 openmls::prelude::{Credential as MlsCredential, CredentialWithKey, MlsCredentialType, SignatureScheme};
14use openmls_basic_credential::SignatureKeyPair;
15use openmls_traits::crypto::OpenMlsCrypto;
16
17pub(crate) use self::error::Result;
18pub use self::{
19 credential_ref::{CredentialRef, FindFilters, FindFiltersBuilder},
20 credential_type::CredentialType,
21 error::Error,
22};
23use crate::{
24 ClientId, ClientIdRef, ClientIdentifier, MlsError, RecursiveError,
25 mls::credential::{error::CredentialValidationError, ext::CredentialExt as _},
26};
27
28#[derive(core_crypto_macros::Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct Credential {
48 pub(crate) credential_type: CredentialType,
50 pub(crate) mls_credential: MlsCredential,
52 #[sensitive]
54 pub(crate) signature_key_pair: SignatureKeyPair,
55 pub(crate) earliest_validity: u64,
62}
63
64impl Credential {
65 pub(crate) fn validate_mls_credential(
67 mls_credential: &MlsCredential,
68 client_id: &ClientIdRef,
69 signature_key: &SignatureKeyPair,
70 ) -> Result<(), CredentialValidationError> {
71 match mls_credential.mls_credential() {
72 MlsCredentialType::Basic(_) => {
73 if client_id.as_slice() != mls_credential.identity() {
74 return Err(CredentialValidationError::WrongCredential);
75 }
76 }
77 MlsCredentialType::X509(cert) => {
78 let certificate_public_key = cert
79 .extract_public_key()
80 .map_err(RecursiveError::mls_credential(
81 "extracting public key from certificate in credential validation",
82 ))?
83 .ok_or(CredentialValidationError::NoPublicKey)?;
84 if signature_key.public() != certificate_public_key {
85 return Err(CredentialValidationError::WrongCredential);
86 }
87 }
88 }
89 Ok(())
90 }
91
92 pub fn basic(signature_scheme: SignatureScheme, client_id: ClientId, crypto: impl OpenMlsCrypto) -> Result<Self> {
96 let (private_key, public_key) = crypto
97 .signature_key_gen(signature_scheme)
98 .map_err(MlsError::wrap("generating signature key"))?;
99 let signature_key_pair = SignatureKeyPair::from_raw(signature_scheme, private_key, public_key);
100
101 Ok(Self {
102 credential_type: CredentialType::Basic,
103 mls_credential: MlsCredential::new_basic(client_id.into_inner()),
104 signature_key_pair,
105 earliest_validity: 0,
106 })
107 }
108
109 pub fn mls_credential(&self) -> &MlsCredential {
113 &self.mls_credential
114 }
115
116 pub fn credential_type(&self) -> CredentialType {
118 self.credential_type
119 }
120
121 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
123 &self.signature_key_pair
124 }
125
126 pub fn signature_scheme(&self) -> SignatureScheme {
128 self.signature_key_pair.signature_scheme()
129 }
130
131 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
133 CredentialWithKey {
134 credential: self.mls_credential.clone(),
135 signature_key: self.signature_key_pair.to_public_vec().into(),
136 }
137 }
138
139 pub fn earliest_validity(&self) -> u64 {
147 self.earliest_validity
148 }
149
150 pub fn client_id(&self) -> &ClientIdRef {
152 self.mls_credential.identity().into()
153 }
154}
155
156impl Credential {
157 #[cfg_attr(not(test), expect(dead_code))]
160 pub(crate) fn from_identifier(
161 identifier: &ClientIdentifier,
162 signature_scheme: SignatureScheme,
163 crypto: impl OpenMlsCrypto,
164 ) -> Result<Self> {
165 match identifier {
166 ClientIdentifier::Basic(client_id) => Self::basic(signature_scheme, client_id.clone(), crypto),
167 ClientIdentifier::X509(certs) => {
168 let cert = certs
169 .get(&signature_scheme)
170 .ok_or(Error::SignatureSchemeNotPresentInX509Identity(signature_scheme))?;
171 Self::x509(cert.clone())
172 }
173 }
174 }
175}
176
177impl From<Credential> for CredentialWithKey {
178 fn from(cb: Credential) -> Self {
179 Self {
180 credential: cb.mls_credential,
181 signature_key: cb.signature_key_pair.public().into(),
182 }
183 }
184}
185
186impl Eq for Credential {}
187impl PartialEq for Credential {
188 fn eq(&self, other: &Self) -> bool {
189 self.mls_credential == other.mls_credential && self.earliest_validity == other.earliest_validity && {
190 let sk = &self.signature_key_pair;
191 let ok = &other.signature_key_pair;
192 sk.signature_scheme() == ok.signature_scheme() && sk.public() == ok.public()
193 }
195 }
196}
197
198#[cfg(test)]
201mod tests {
202 use std::collections::HashMap;
203
204 use mls_crypto_provider::PkiKeypair;
205
206 use super::{x509::CertificateBundle, *};
207 use crate::{
208 ClientIdentifier, CredentialType, E2eiConversationState,
209 mls::{conversation::Conversation as _, credential::x509::CertificatePrivateKey},
210 test_utils::{
211 x509::{CertificateParams, X509TestChain},
212 *,
213 },
214 };
215
216 #[apply(all_cred_cipher)]
217 async fn basic_clients_can_send_messages(case: TestContext) {
218 if !case.is_basic() {
219 return;
220 }
221 let [alice, bob] = case.sessions_basic().await;
222 let conversation = case.create_conversation([&alice, &bob]).await;
223 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
224 }
225
226 #[apply(all_cred_cipher)]
227 async fn certificate_clients_can_send_messages(case: TestContext) {
228 if !case.is_x509() {
229 return;
230 }
231 let [alice, bob] = case.sessions_x509().await;
232 let conversation = case.create_conversation([&alice, &bob]).await;
233 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
234 }
235
236 #[apply(all_cred_cipher)]
237 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
238 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
240 let (alice, bob, alice_credential_type) = match case.credential_type {
242 CredentialType::Basic => (x509_session, basic_session, CredentialType::X509),
243 CredentialType::X509 => (basic_session, x509_session, CredentialType::Basic),
244 };
245
246 let conversation = case
247 .create_heterogeneous_conversation(alice_credential_type, case.credential_type, [&alice, &bob])
248 .await;
249 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
250 }
251
252 #[apply(all_cred_cipher)]
253 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
254 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
255
256 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
257
258 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
259 certs.certificate_chain = vec![];
260 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
261
262 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
263 .await
264 .unwrap_err();
265 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
266 }
267
268 #[apply(all_cred_cipher)]
269 async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestContext) {
270 use crate::MlsErrorKind;
271
272 if !case.is_x509() {
273 return;
274 }
275 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
276
277 let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
278
279 let new_cert = alice_cert
280 .pki_keypair
281 .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
282 .unwrap();
283 let mut alice_cert = alice_cert.clone();
284 alice_cert.certificate = new_cert;
285 let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
286 let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
287
288 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
289 .await
290 .unwrap();
291 let [bob] = case.sessions_x509().await;
292 let bob_key_package = bob.rand_key_package(&case).await;
293 let conversation = case.create_conversation([&alice]).await;
294 let err = conversation
295 .guard()
296 .await
297 .add_members([bob_key_package].into())
298 .await
299 .unwrap_err();
300 assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
301 }
302
303 #[apply(all_cred_cipher)]
304 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
305 if !case.is_x509() {
306 return;
307 }
308 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
309 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
310
311 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
312 let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
313
314 let eve_key = CertificatePrivateKey {
315 value: new_pki_kp.signing_key_bytes(),
316 signature_scheme: case.ciphersuite().signature_algorithm(),
317 };
318 let cb = CertificateBundle {
319 certificate_chain: certs.certificate_chain,
320 private_key: eve_key,
321 };
322 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
323
324 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
325 .await
326 .unwrap_err();
327 assert!(innermost_source_matches!(
328 err,
329 crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
330 ));
331 }
332
333 #[apply(all_cred_cipher)]
334 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
335 if !case.is_x509() {
336 return;
337 }
338 Box::pin(async move {
339 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
340
341 let expiration_time = core::time::Duration::from_secs(14);
342 let start = web_time::Instant::now();
343
344 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
345 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
346 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
347 .await
348 .unwrap();
349 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
350 .await
351 .unwrap();
352
353 let conversation = case.create_conversation([&alice, &bob]).await;
354 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
356
357 assert_eq!(
358 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
359 E2eiConversationState::Verified
360 );
361
362 let elapsed = start.elapsed();
363 if expiration_time > elapsed {
365 smol::Timer::after(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
366 }
367
368 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
369 assert_eq!(
370 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
371 E2eiConversationState::NotVerified
372 );
373 })
374 .await;
375 }
376
377 #[apply(all_cred_cipher)]
378 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
379 if !case.is_x509() {
380 return;
381 }
382 Box::pin(async {
383 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
384 let conversation = case.create_conversation([&alice, &bob]).await;
385
386 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
388 assert_eq!(
389 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
390 E2eiConversationState::Verified
391 );
392 assert_eq!(
393 conversation
394 .guard_of(&bob)
395 .await
396 .e2ei_conversation_state()
397 .await
398 .unwrap(),
399 E2eiConversationState::Verified
400 );
401
402 let conversation = conversation
404 .invite_with_credential_type_notify(CredentialType::Basic, [&charlie])
405 .await;
406
407 assert_eq!(
408 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
409 E2eiConversationState::NotVerified
410 );
411 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
412 assert_eq!(
413 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
414 E2eiConversationState::NotVerified
415 );
416 })
417 .await;
418 }
419
420 #[apply(all_cred_cipher)]
421 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
422 use crate::MlsErrorKind;
423
424 if !case.is_x509() {
425 return;
426 }
427 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
428
429 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
430 let local_ca = x509_test_chain.find_local_intermediate_ca();
431 let alice_cert = {
432 let name = "alice";
433 let common_name = format!("{name} Smith");
434 let handle = format!("{}_wire", name.to_lowercase());
435 let client_id: String = crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
436 .try_into()
437 .unwrap();
438 local_ca.create_and_sign_end_identity(CertificateParams {
439 common_name: Some(common_name.clone()),
440 handle: Some(handle.clone()),
441 client_id: Some(client_id.clone()),
442 validity_start: Some(tomorrow),
443 ..Default::default()
444 })
445 };
446 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
447 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
448 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
449 .await
450 .unwrap_err();
451
452 assert!(innermost_source_matches!(
453 err,
454 MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
455 ))
456 }
457
458 pub(crate) fn now_std() -> std::time::Duration {
464 let now = web_time::SystemTime::now();
465 now.duration_since(web_time::UNIX_EPOCH).unwrap()
466 }
467}