core_crypto/mls/credential/
mod.rs1use openmls::prelude::{Credential, CredentialWithKey};
2use openmls_basic_credential::SignatureKeyPair;
3use std::cmp::Ordering;
4use std::hash::{Hash, Hasher};
5
6pub(crate) mod crl;
7mod error;
8pub(crate) mod ext;
9pub(crate) mod typ;
10pub(crate) mod x509;
11
12pub(crate) use error::{Error, Result};
13
14#[derive(Debug, serde::Serialize, serde::Deserialize)]
15pub struct CredentialBundle {
16 pub(crate) credential: Credential,
17 pub(crate) signature_key: SignatureKeyPair,
18 pub(crate) created_at: u64,
19}
20
21impl CredentialBundle {
22 pub fn credential(&self) -> &Credential {
23 &self.credential
24 }
25
26 pub(crate) fn signature_key(&self) -> &SignatureKeyPair {
27 &self.signature_key
28 }
29
30 pub fn to_mls_credential_with_key(&self) -> CredentialWithKey {
31 CredentialWithKey {
32 credential: self.credential.clone(),
33 signature_key: self.signature_key.to_public_vec().into(),
34 }
35 }
36}
37
38impl From<CredentialBundle> for CredentialWithKey {
39 fn from(cb: CredentialBundle) -> Self {
40 Self {
41 credential: cb.credential,
42 signature_key: cb.signature_key.public().into(),
43 }
44 }
45}
46
47impl Clone for CredentialBundle {
48 fn clone(&self) -> Self {
49 Self {
50 credential: self.credential.clone(),
51 signature_key: SignatureKeyPair::from_raw(
52 self.signature_key.signature_scheme(),
53 self.signature_key.private().to_vec(),
54 self.signature_key.to_public_vec(),
55 ),
56 created_at: self.created_at,
57 }
58 }
59}
60
61impl Eq for CredentialBundle {}
62impl PartialEq for CredentialBundle {
63 fn eq(&self, other: &Self) -> bool {
64 self.credential.eq(&other.credential)
65 && self.created_at.eq(&other.created_at)
66 && self
67 .signature_key
68 .signature_scheme()
69 .eq(&other.signature_key.signature_scheme())
70 && self.signature_key.public().eq(other.signature_key.public())
71 }
72}
73
74impl Hash for CredentialBundle {
75 fn hash<H: Hasher>(&self, state: &mut H) {
76 self.created_at.hash(state);
77 self.signature_key.signature_scheme().hash(state);
78 self.signature_key.public().hash(state);
79 self.credential().identity().hash(state);
80 match self.credential().mls_credential() {
81 openmls::prelude::MlsCredentialType::X509(cert) => {
82 cert.certificates.hash(state);
83 }
84 openmls::prelude::MlsCredentialType::Basic(_) => {}
85 };
86 }
87}
88
89impl Ord for CredentialBundle {
90 fn cmp(&self, other: &Self) -> Ordering {
91 self.created_at.cmp(&other.created_at)
92 }
93}
94
95impl PartialOrd for CredentialBundle {
96 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
97 Some(self.cmp(other))
98 }
99}
100
101#[cfg(test)]
104mod tests {
105 use mls_crypto_provider::PkiKeypair;
106 use std::collections::HashMap;
107 use wasm_bindgen_test::*;
108
109 use super::x509::CertificateBundle;
110 use super::*;
111 use crate::mls::conversation::Conversation as _;
112 use crate::{
113 mls::credential::x509::CertificatePrivateKey,
114 prelude::{ClientIdentifier, E2eiConversationState, MlsCredentialType},
115 test_utils::{
116 x509::{CertificateParams, X509TestChain},
117 *,
118 },
119 };
120
121 wasm_bindgen_test_configure!(run_in_browser);
122
123 #[apply(all_cred_cipher)]
124 #[wasm_bindgen_test]
125 async fn basic_clients_can_send_messages(case: TestContext) {
126 if !case.is_basic() {
127 return;
128 }
129 let [alice, bob] = case.sessions_basic().await;
130 let conversation = case.create_conversation([&alice, &bob]).await;
131 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
132 }
133
134 #[apply(all_cred_cipher)]
135 #[wasm_bindgen_test]
136 async fn certificate_clients_can_send_messages(case: TestContext) {
137 if !case.is_x509() {
138 return;
139 }
140 let [alice, bob] = case.sessions_x509().await;
141 let conversation = case.create_conversation([&alice, &bob]).await;
142 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
143 }
144
145 #[apply(all_cred_cipher)]
146 #[wasm_bindgen_test]
147 async fn heterogeneous_clients_can_send_messages(case: TestContext) {
148 let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await;
150 let (alice, bob, alice_credential_type) = match case.credential_type {
152 MlsCredentialType::Basic => (x509_session, basic_session, MlsCredentialType::X509),
153 MlsCredentialType::X509 => (basic_session, x509_session, MlsCredentialType::Basic),
154 };
155
156 let conversation = case
157 .create_heterogeneous_conversation(alice_credential_type, case.credential_type, [&alice, &bob])
158 .await;
159 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
160 }
161
162 #[apply(all_cred_cipher)]
163 #[wasm_bindgen_test]
164 async fn should_fail_when_certificate_chain_is_empty(case: TestContext) {
165 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
166
167 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
168
169 let mut certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
170 certs.certificate_chain = vec![];
171 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), certs)]));
172
173 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
174 .await
175 .unwrap_err();
176 assert!(innermost_source_matches!(err, Error::InvalidIdentity));
177 }
178
179 #[apply(all_cred_cipher)]
180 #[wasm_bindgen_test]
181 async fn should_fail_when_certificate_chain_has_a_single_self_signed(case: TestContext) {
182 use crate::MlsErrorKind;
183
184 if !case.is_x509() {
185 return;
186 }
187 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
188
189 let (_alice_identifier, alice_cert) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
190
191 let new_cert = alice_cert
192 .pki_keypair
193 .re_sign(&alice_cert.certificate, &alice_cert.certificate, None)
194 .unwrap();
195 let mut alice_cert = alice_cert.clone();
196 alice_cert.certificate = new_cert;
197 let cb = CertificateBundle::from_self_signed_certificate(&alice_cert);
198 let alice_identifier = ClientIdentifier::X509([(case.signature_scheme(), cb)].into());
199
200 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
201 .await
202 .unwrap();
203 let [bob] = case.sessions_x509().await;
204 let bob_key_package = bob.rand_key_package(&case).await;
205 let conversation = case.create_conversation([&alice]).await;
206 let err = conversation
207 .guard()
208 .await
209 .add_members([bob_key_package].into())
210 .await
211 .unwrap_err();
212 assert!(innermost_source_matches!(err, MlsErrorKind::MlsAddMembersError(_)));
213 }
214
215 #[apply(all_cred_cipher)]
216 #[wasm_bindgen_test]
217 async fn should_fail_when_signature_key_doesnt_match_certificate_public_key(case: TestContext) {
218 if !case.is_x509() {
219 return;
220 }
221 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
222 let x509_intermediate = x509_test_chain.find_local_intermediate_ca();
223
224 let certs = CertificateBundle::rand(&"alice".into(), x509_intermediate);
225 let new_pki_kp = PkiKeypair::rand_unchecked(case.signature_scheme());
226
227 let eve_key = CertificatePrivateKey {
228 value: new_pki_kp.signing_key_bytes(),
229 signature_scheme: case.ciphersuite().signature_algorithm(),
230 };
231 let cb = CertificateBundle {
232 certificate_chain: certs.certificate_chain,
233 private_key: eve_key,
234 };
235 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
236
237 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
238 .await
239 .unwrap_err();
240 assert!(innermost_source_matches!(
241 err,
242 crate::MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::MismatchKeypair),
243 ));
244 }
245
246 #[apply(all_cred_cipher)]
247 #[wasm_bindgen_test]
248 async fn should_not_fail_but_degrade_when_certificate_expired(case: TestContext) {
249 if !case.is_x509() {
250 return;
251 }
252 Box::pin(async move {
253 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
254
255 let expiration_time = core::time::Duration::from_secs(14);
256 let start = web_time::Instant::now();
257
258 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
259 let (bob_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("bob", Some(expiration_time));
260 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
261 .await
262 .unwrap();
263 let bob = SessionContext::new_with_identifier(&case, bob_identifier, Some(&x509_test_chain))
264 .await
265 .unwrap();
266
267 let conversation = case.create_conversation([&alice, &bob]).await;
268 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
270
271 assert_eq!(
272 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
273 E2eiConversationState::Verified
274 );
275
276 let elapsed = start.elapsed();
277 if expiration_time > elapsed {
279 async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(2)).await;
280 }
281
282 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
283 assert_eq!(
284 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
285 E2eiConversationState::NotVerified
286 );
287 })
288 .await;
289 }
290
291 #[apply(all_cred_cipher)]
292 #[wasm_bindgen_test]
293 async fn should_not_fail_but_degrade_when_basic_joins(case: TestContext) {
294 if !case.is_x509() {
295 return;
296 }
297 Box::pin(async {
298 let ([alice, bob], [charlie]) = case.sessions_mixed_credential_types().await;
299 let conversation = case.create_conversation([&alice, &bob]).await;
300
301 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
303 assert_eq!(
304 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
305 E2eiConversationState::Verified
306 );
307 assert_eq!(
308 conversation
309 .guard_of(&bob)
310 .await
311 .e2ei_conversation_state()
312 .await
313 .unwrap(),
314 E2eiConversationState::Verified
315 );
316
317 let conversation = conversation
319 .invite_with_credential_type_notify(MlsCredentialType::Basic, [&charlie])
320 .await;
321
322 assert_eq!(
323 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
324 E2eiConversationState::NotVerified
325 );
326 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
327 assert_eq!(
328 conversation.guard().await.e2ei_conversation_state().await.unwrap(),
329 E2eiConversationState::NotVerified
330 );
331 })
332 .await;
333 }
334
335 #[apply(all_cred_cipher)]
336 #[wasm_bindgen_test]
337 async fn should_fail_when_certificate_not_valid_yet(case: TestContext) {
338 use crate::MlsErrorKind;
339
340 if !case.is_x509() {
341 return;
342 }
343 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
344
345 let tomorrow = now_std() + core::time::Duration::from_secs(3600 * 24);
346 let local_ca = x509_test_chain.find_local_intermediate_ca();
347 let alice_cert = {
348 let name = "alice";
349 let common_name = format!("{name} Smith");
350 let handle = format!("{}_wire", name.to_lowercase());
351 let client_id: String = crate::e2e_identity::id::QualifiedE2eiClientId::generate_with_domain("wire.com")
352 .try_into()
353 .unwrap();
354 local_ca.create_and_sign_end_identity(CertificateParams {
355 common_name: Some(common_name.clone()),
356 handle: Some(handle.clone()),
357 client_id: Some(client_id.clone()),
358 validity_start: Some(tomorrow),
359 ..Default::default()
360 })
361 };
362 let cb = CertificateBundle::from_certificate_and_issuer(&alice_cert, local_ca);
363 let alice_identifier = ClientIdentifier::X509(HashMap::from([(case.signature_scheme(), cb)]));
364 let err = SessionContext::new_with_identifier(&case, alice_identifier, Some(&x509_test_chain))
365 .await
366 .unwrap_err();
367
368 assert!(innermost_source_matches!(
369 err,
370 MlsErrorKind::MlsCryptoError(openmls::prelude::CryptoError::ExpiredCertificate),
371 ))
372 }
373
374 pub(crate) fn now_std() -> std::time::Duration {
380 let now = web_time::SystemTime::now();
381 now.duration_since(web_time::UNIX_EPOCH).unwrap()
382 }
383}