1use openmls_traits::OpenMlsCryptoProvider;
2use std::collections::HashMap;
3
4use wire_e2e_identity::prelude::{E2eiAcmeAuthorization, RustyE2eIdentity};
5use zeroize::Zeroize;
6
7use error::*;
8use mls_crypto_provider::MlsCryptoProvider;
9
10use crate::e2e_identity::init_certificates::NewCrlDistributionPoint;
11use crate::{
12 e2e_identity::{crypto::E2eiSignatureKeypair, id::QualifiedE2eiClientId},
13 mls::credential::x509::CertificatePrivateKey,
14 prelude::{id::ClientId, identifier::ClientIdentifier, CertificateBundle, MlsCiphersuite},
15 CryptoError, CryptoResult,
16};
17
18pub(crate) mod conversation_state;
19mod crypto;
20pub(crate) mod device_status;
21pub mod enabled;
22pub mod error;
23pub(crate) mod id;
24pub(crate) mod identity;
25pub(crate) mod init_certificates;
26#[cfg(not(target_family = "wasm"))]
27pub(crate) mod refresh_token;
28pub(crate) mod rotate;
29pub(crate) mod stash;
30pub mod types;
31
32use crate::context::CentralContext;
33pub use init_certificates::E2eiDumpedPkiEnv;
34
35type Json = Vec<u8>;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct CrlRegistration {
40 pub dirty: bool,
42 pub expiration: Option<u64>,
44}
45
46impl CentralContext {
47 pub async fn e2ei_new_enrollment(
56 &self,
57 client_id: ClientId,
58 display_name: String,
59 handle: String,
60 team: Option<String>,
61 expiry_sec: u32,
62 ciphersuite: MlsCiphersuite,
63 ) -> CryptoResult<E2eiEnrollment> {
64 let signature_keypair = None; E2eiEnrollment::try_new(
66 client_id,
67 display_name,
68 handle,
69 team,
70 expiry_sec,
71 &self.mls_provider().await?,
72 ciphersuite,
73 signature_keypair,
74 #[cfg(not(target_family = "wasm"))]
75 None, )
77 }
78
79 pub async fn e2ei_mls_init_only(
82 &self,
83 enrollment: &mut E2eiEnrollment,
84 certificate_chain: String,
85 nb_init_key_packages: Option<usize>,
86 ) -> CryptoResult<NewCrlDistributionPoint> {
87 let sk = enrollment.get_sign_key_for_mls()?;
88 let cs = enrollment.ciphersuite;
89 let certificate_chain = enrollment
90 .certificate_response(
91 certificate_chain,
92 self.mls_provider()
93 .await?
94 .authentication_service()
95 .borrow()
96 .await
97 .as_ref()
98 .ok_or(CryptoError::ConsumerError)?,
99 )
100 .await?;
101
102 let crl_new_distribution_points = self.extract_dp_on_init(&certificate_chain[..]).await?;
103
104 let private_key = CertificatePrivateKey {
105 value: sk,
106 signature_scheme: cs.signature_algorithm(),
107 };
108
109 let cert_bundle = CertificateBundle {
110 certificate_chain,
111 private_key,
112 };
113 let identifier = ClientIdentifier::X509(HashMap::from([(cs.signature_algorithm(), cert_bundle)]));
114 self.mls_init(identifier, vec![cs], nb_init_key_packages).await?;
115 Ok(crl_new_distribution_points)
116 }
117}
118
119#[derive(Debug, serde::Serialize, serde::Deserialize)]
121pub struct E2eiEnrollment {
122 delegate: RustyE2eIdentity,
123 pub(crate) sign_sk: E2eiSignatureKeypair,
124 client_id: String,
125 display_name: String,
126 handle: String,
127 team: Option<String>,
128 expiry: core::time::Duration,
129 directory: Option<types::E2eiAcmeDirectory>,
130 account: Option<wire_e2e_identity::prelude::E2eiAcmeAccount>,
131 user_authz: Option<E2eiAcmeAuthorization>,
132 device_authz: Option<E2eiAcmeAuthorization>,
133 valid_order: Option<wire_e2e_identity::prelude::E2eiAcmeOrder>,
134 finalize: Option<wire_e2e_identity::prelude::E2eiAcmeFinalize>,
135 ciphersuite: MlsCiphersuite,
136 #[cfg(not(target_family = "wasm"))]
137 refresh_token: Option<refresh_token::RefreshToken>,
138}
139
140impl std::ops::Deref for E2eiEnrollment {
141 type Target = RustyE2eIdentity;
142
143 fn deref(&self) -> &Self::Target {
144 &self.delegate
145 }
146}
147
148impl E2eiEnrollment {
149 #[allow(clippy::too_many_arguments)]
158 pub fn try_new(
159 client_id: ClientId,
160 display_name: String,
161 handle: String,
162 team: Option<String>,
163 expiry_sec: u32,
164 backend: &MlsCryptoProvider,
165 ciphersuite: MlsCiphersuite,
166 sign_keypair: Option<E2eiSignatureKeypair>,
167 #[cfg(not(target_family = "wasm"))] refresh_token: Option<refresh_token::RefreshToken>,
168 ) -> CryptoResult<Self> {
169 let alg = ciphersuite.try_into()?;
170 let sign_sk = match sign_keypair {
171 Some(kp) => kp,
172 None => Self::new_sign_key(ciphersuite, backend)?,
173 };
174
175 let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?;
176 let client_id = String::try_from(client_id)?;
177 let expiry = core::time::Duration::from_secs(u64::from(expiry_sec));
178 let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone()).map_err(E2eIdentityError::from)?;
179 Ok(Self {
180 delegate,
181 sign_sk,
182 client_id,
183 display_name,
184 handle,
185 team,
186 expiry,
187 directory: None,
188 account: None,
189 user_authz: None,
190 device_authz: None,
191 valid_order: None,
192 finalize: None,
193 ciphersuite,
194 #[cfg(not(target_family = "wasm"))]
195 refresh_token,
196 })
197 }
198
199 pub fn directory_response(&mut self, directory: Json) -> E2eIdentityResult<types::E2eiAcmeDirectory> {
208 let directory = serde_json::from_slice(&directory[..])?;
209 let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
210 self.directory = Some(directory.clone());
211 Ok(directory)
212 }
213
214 pub fn new_account_request(&self, previous_nonce: String) -> E2eIdentityResult<Json> {
223 let directory = self.directory.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
224 "You must first call 'directoryResponse()'",
225 ))?;
226 let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
227 let account = serde_json::to_vec(&account)?;
228 Ok(account)
229 }
230
231 pub fn new_account_response(&mut self, account: Json) -> E2eIdentityResult<()> {
238 let account = serde_json::from_slice(&account[..])?;
239 let account = self.acme_new_account_response(account)?;
240 self.account = Some(account);
241 Ok(())
242 }
243
244 pub fn new_order_request(&self, previous_nonce: String) -> E2eIdentityResult<Json> {
251 let directory = self.directory.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
252 "You must first call 'directoryResponse()'",
253 ))?;
254 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
255 "You must first call 'newAccountResponse()'",
256 ))?;
257 let order = self.acme_new_order_request(
258 &self.display_name,
259 &self.client_id,
260 &self.handle,
261 self.expiry,
262 &directory.try_into()?,
263 account,
264 previous_nonce,
265 )?;
266 let order = serde_json::to_vec(&order)?;
267 Ok(order)
268 }
269
270 pub fn new_order_response(&self, order: Json) -> E2eIdentityResult<types::E2eiNewAcmeOrder> {
277 let order = serde_json::from_slice(&order[..])?;
278 self.acme_new_order_response(order)?.try_into()
279 }
280
281 pub fn new_authz_request(&self, url: String, previous_nonce: String) -> E2eIdentityResult<Json> {
291 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
292 "You must first call 'newAccountResponse()'",
293 ))?;
294 let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
295 let authz = serde_json::to_vec(&authz)?;
296 Ok(authz)
297 }
298
299 pub fn new_authz_response(&mut self, authz: Json) -> E2eIdentityResult<types::E2eiNewAcmeAuthz> {
306 let authz = serde_json::from_slice(&authz[..])?;
307 let authz = self.acme_new_authz_response(authz)?;
308 match &authz {
309 E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
310 E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
311 };
312 authz.try_into()
313 }
314
315 #[allow(clippy::too_many_arguments)]
329 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> E2eIdentityResult<String> {
330 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
331 let authz = self
332 .device_authz
333 .as_ref()
334 .ok_or(E2eIdentityError::OutOfOrderEnrollment(
335 "You must first call 'newAuthzResponse()'",
336 ))?;
337 let challenge = match authz {
338 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
339 E2eiAcmeAuthorization::User { .. } => return Err(E2eIdentityError::ImplementationError),
340 };
341 Ok(self.new_dpop_token(
342 &self.client_id,
343 self.display_name.as_str(),
344 challenge,
345 backend_nonce,
346 self.handle.as_str(),
347 self.team.clone(),
348 expiry,
349 )?)
350 }
351
352 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> E2eIdentityResult<Json> {
362 let authz = self
363 .device_authz
364 .as_ref()
365 .ok_or(E2eIdentityError::OutOfOrderEnrollment(
366 "You must first call 'newAuthzResponse()'",
367 ))?;
368 let challenge = match authz {
369 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
370 E2eiAcmeAuthorization::User { .. } => return Err(E2eIdentityError::ImplementationError),
371 };
372 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
373 "You must first call 'newAccountResponse()'",
374 ))?;
375 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
376 let challenge = serde_json::to_vec(&challenge)?;
377 Ok(challenge)
378 }
379
380 pub fn new_dpop_challenge_response(&self, challenge: Json) -> E2eIdentityResult<()> {
387 let challenge = serde_json::from_slice(&challenge[..])?;
388 Ok(self.acme_new_challenge_response(challenge)?)
389 }
390
391 pub fn new_oidc_challenge_request(
402 &mut self,
403 id_token: String,
404 #[cfg(not(target_family = "wasm"))] refresh_token: String,
405 previous_nonce: String,
406 ) -> E2eIdentityResult<Json> {
407 #[cfg(not(target_family = "wasm"))]
408 {
409 if refresh_token.is_empty() {
410 return Err(E2eIdentityError::InvalidRefreshToken);
411 }
412 }
413 let authz = self.user_authz.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
414 "You must first call 'newAuthzResponse()'",
415 ))?;
416 let challenge = match authz {
417 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
418 E2eiAcmeAuthorization::Device { .. } => return Err(E2eIdentityError::ImplementationError),
419 };
420 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
421 "You must first call 'newAccountResponse()'",
422 ))?;
423 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
424 let challenge = serde_json::to_vec(&challenge)?;
425 #[cfg(not(target_family = "wasm"))]
426 {
427 self.refresh_token.replace(refresh_token.into());
428 }
429 Ok(challenge)
430 }
431
432 pub async fn new_oidc_challenge_response(
439 &mut self,
440 #[cfg(not(target_family = "wasm"))] backend: &MlsCryptoProvider,
441 challenge: Json,
442 ) -> E2eIdentityResult<()> {
443 let challenge = serde_json::from_slice(&challenge[..])?;
444 self.acme_new_challenge_response(challenge)?;
445
446 #[cfg(not(target_family = "wasm"))]
447 {
448 let refresh_token = self.refresh_token.take().ok_or(E2eIdentityError::OutOfOrderEnrollment(
453 "You must first call 'new_oidc_challenge_request()'",
454 ))?;
455 refresh_token.replace(backend).await?;
456 }
457 Ok(())
458 }
459
460 pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> E2eIdentityResult<Json> {
469 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
470 "You must first call 'newAccountResponse()'",
471 ))?;
472 let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
473 let order = serde_json::to_vec(&order)?;
474 Ok(order)
475 }
476
477 pub fn check_order_response(&mut self, order: Json) -> E2eIdentityResult<String> {
487 let order = serde_json::from_slice(&order[..])?;
488 let valid_order = self.acme_check_order_response(order)?;
489 let finalize_url = valid_order.finalize_url.to_string();
490 self.valid_order = Some(valid_order);
491 Ok(finalize_url)
492 }
493
494 pub fn finalize_request(&mut self, previous_nonce: String) -> E2eIdentityResult<Json> {
504 let account = self.account.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
505 "You must first call 'newAccountResponse()'",
506 ))?;
507 let order = self.valid_order.as_ref().ok_or(E2eIdentityError::OutOfOrderEnrollment(
508 "You must first call 'checkOrderResponse()'",
509 ))?;
510 let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
511 let finalize = serde_json::to_vec(&finalize)?;
512 Ok(finalize)
513 }
514
515 pub fn finalize_response(&mut self, finalize: Json) -> E2eIdentityResult<String> {
525 let finalize = serde_json::from_slice(&finalize[..])?;
526 let finalize = self.acme_finalize_response(finalize)?;
527 let certificate_url = finalize.certificate_url.to_string();
528 self.finalize = Some(finalize);
529 Ok(certificate_url)
530 }
531
532 pub fn certificate_request(&mut self, previous_nonce: String) -> E2eIdentityResult<Json> {
541 let account = self.account.take().ok_or(E2eIdentityError::OutOfOrderEnrollment(
542 "You must first call 'newAccountResponse()'",
543 ))?;
544 let finalize = self.finalize.take().ok_or(E2eIdentityError::OutOfOrderEnrollment(
545 "You must first call 'finalizeResponse()'",
546 ))?;
547 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
548 let certificate = serde_json::to_vec(&certificate)?;
549 Ok(certificate)
550 }
551
552 async fn certificate_response(
553 &mut self,
554 certificate_chain: String,
555 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
556 ) -> E2eIdentityResult<Vec<Vec<u8>>> {
557 let order = self.valid_order.take().ok_or(E2eIdentityError::OutOfOrderEnrollment(
558 "You must first call 'checkOrderResponse()'",
559 ))?;
560 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
561
562 self.sign_sk.zeroize();
564 self.delegate.sign_kp.zeroize();
565 self.delegate.acme_kp.zeroize();
566
567 #[cfg(not(target_family = "wasm"))]
568 self.refresh_token.zeroize();
569
570 Ok(certificates)
571 }
572}
573
574#[cfg(test)]
575pub(crate) mod tests {
577 use itertools::Itertools;
578 use mls_crypto_provider::PkiKeypair;
579
580 #[cfg(not(target_family = "wasm"))]
581 use openmls_traits::OpenMlsCryptoProvider;
582 use serde_json::json;
583 use wasm_bindgen_test::*;
584
585 use crate::context::CentralContext;
586 #[cfg(not(target_family = "wasm"))]
587 use crate::e2e_identity::refresh_token::RefreshToken;
588 use crate::{
589 e2e_identity::{id::QualifiedE2eiClientId, tests::x509::X509TestChain},
590 prelude::*,
591 test_utils::{context::TEAM, *},
592 CryptoResult,
593 };
594
595 wasm_bindgen_test_configure!(run_in_browser);
596
597 pub(crate) const E2EI_DISPLAY_NAME: &str = "Alice Smith";
598 pub(crate) const E2EI_HANDLE: &str = "alice_wire";
599 pub(crate) const E2EI_CLIENT_ID: &str = "bd4c7053-1c5a-4020-9559-cd7bf7961954:4959bc6ab12f2846@world.com";
600 pub(crate) const E2EI_CLIENT_ID_URI: &str = "vUxwUxxaQCCVWc1795YZVA!4959bc6ab12f2846@world.com";
601 pub(crate) const E2EI_EXPIRY: u32 = 90 * 24 * 3600;
602
603 pub(crate) fn init_enrollment(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
604 Box::pin(async move {
605 let E2eiInitWrapper { context: cc, case } = wrapper;
606 let cs = case.ciphersuite();
607 cc.e2ei_new_enrollment(
608 E2EI_CLIENT_ID.into(),
609 E2EI_DISPLAY_NAME.to_string(),
610 E2EI_HANDLE.to_string(),
611 Some(TEAM.to_string()),
612 E2EI_EXPIRY,
613 cs,
614 )
615 .await
616 })
617 }
618
619 pub(crate) const NEW_HANDLE: &str = "new_alice_wire";
620 pub(crate) const NEW_DISPLAY_NAME: &str = "New Alice Smith";
621 pub(crate) fn init_activation_or_rotation(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
622 Box::pin(async move {
623 let E2eiInitWrapper { context: cc, case } = wrapper;
624 let cs = case.ciphersuite();
625 match case.credential_type {
626 MlsCredentialType::Basic => {
627 cc.e2ei_new_activation_enrollment(
628 NEW_DISPLAY_NAME.to_string(),
629 NEW_HANDLE.to_string(),
630 Some(TEAM.to_string()),
631 E2EI_EXPIRY,
632 cs,
633 )
634 .await
635 }
636 MlsCredentialType::X509 => {
637 cc.e2ei_new_rotate_enrollment(
638 Some(NEW_DISPLAY_NAME.to_string()),
639 Some(NEW_HANDLE.to_string()),
640 Some(TEAM.to_string()),
641 E2EI_EXPIRY,
642 cs,
643 )
644 .await
645 }
646 }
647 })
648 }
649
650 #[apply(all_cred_cipher)]
651 #[wasm_bindgen_test]
652 async fn e2e_identity_should_work(case: TestCase) {
653 run_test_wo_clients(case.clone(), move |mut cc| {
654 Box::pin(async move {
655 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
656
657 let is_renewal = false;
658
659 let (mut enrollment, cert) = e2ei_enrollment(
660 &mut cc,
661 &case,
662 &x509_test_chain,
663 Some(E2EI_CLIENT_ID_URI),
664 is_renewal,
665 init_enrollment,
666 noop_restore,
667 )
668 .await
669 .unwrap();
670
671 cc.context
672 .e2ei_mls_init_only(&mut enrollment, cert, Some(INITIAL_KEYING_MATERIAL_COUNT))
673 .await
674 .unwrap();
675
676 let id = conversation_id();
678 cc.context
679 .new_conversation(&id, MlsCredentialType::X509, case.cfg.clone())
680 .await
681 .unwrap();
682 cc.context.encrypt_message(&id, "Hello e2e identity !").await.unwrap();
683 assert_eq!(
684 cc.context.e2ei_conversation_state(&id).await.unwrap(),
685 E2eiConversationState::Verified
686 );
687 assert!(cc.context.e2ei_is_enabled(case.signature_scheme()).await.unwrap());
688 })
689 })
690 .await
691 }
692
693 pub(crate) type RestoreFnReturn<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = E2eiEnrollment> + 'a>>;
694
695 pub(crate) fn noop_restore(e: E2eiEnrollment, _cc: &CentralContext) -> RestoreFnReturn<'_> {
696 Box::pin(async move { e })
697 }
698
699 pub(crate) type InitFnReturn<'a> =
700 std::pin::Pin<Box<dyn std::future::Future<Output = CryptoResult<E2eiEnrollment>> + 'a>>;
701
702 pub(crate) struct E2eiInitWrapper<'a> {
704 pub(crate) context: &'a CentralContext,
705 pub(crate) case: &'a TestCase,
706 }
707
708 pub(crate) async fn e2ei_enrollment<'a>(
709 ctx: &'a mut ClientContext,
710 case: &TestCase,
711 x509_test_chain: &X509TestChain,
712 client_id: Option<&str>,
713 #[cfg(not(target_family = "wasm"))] is_renewal: bool,
714 #[cfg(target_family = "wasm")] _is_renewal: bool,
715 init: impl Fn(E2eiInitWrapper) -> InitFnReturn<'_>,
716 restore: impl Fn(E2eiEnrollment, &'a CentralContext) -> RestoreFnReturn<'a>,
718 ) -> CryptoResult<(E2eiEnrollment, String)> {
719 x509_test_chain.register_with_central(&ctx.context).await;
720 #[cfg(not(target_family = "wasm"))]
721 {
722 let backend = ctx.context.mls_provider().await?;
723 let keystore = backend.key_store();
724 if is_renewal {
725 let initial_refresh_token =
726 crate::e2e_identity::refresh_token::RefreshToken::from("initial-refresh-token".to_string());
727 let initial_refresh_token =
728 core_crypto_keystore::entities::E2eiRefreshToken::from(initial_refresh_token);
729 keystore.save(initial_refresh_token).await?;
730 }
731 }
732
733 let wrapper = E2eiInitWrapper {
734 context: &ctx.context,
735 case,
736 };
737 let mut enrollment = init(wrapper).await?;
738
739 #[cfg(not(target_family = "wasm"))]
740 {
741 let backend = ctx.context.mls_provider().await?;
742 let keystore = backend.key_store();
743 if is_renewal {
744 assert!(enrollment.refresh_token.is_some());
745 assert!(RefreshToken::find(keystore).await.is_ok());
746 } else {
747 assert!(matches!(
748 enrollment.get_refresh_token().unwrap_err(),
749 E2eIdentityError::OutOfOrderEnrollment(_)
750 ));
751 assert!(RefreshToken::find(keystore).await.is_err());
752 }
753 }
754
755 let (display_name, handle) = (enrollment.display_name.clone(), &enrollment.handle.clone());
756
757 let directory = json!({
758 "newNonce": "https://example.com/acme/new-nonce",
759 "newAccount": "https://example.com/acme/new-account",
760 "newOrder": "https://example.com/acme/new-order",
761 "revokeCert": "https://example.com/acme/revoke-cert"
762 });
763 let directory = serde_json::to_vec(&directory)?;
764 enrollment.directory_response(directory)?;
765
766 let mut enrollment = restore(enrollment, &ctx.context).await;
767
768 let previous_nonce = "YUVndEZQVTV6ZUNlUkJxRG10c0syQmNWeW1kanlPbjM";
769 let _account_req = enrollment.new_account_request(previous_nonce.to_string())?;
770
771 let account_resp = json!({
772 "status": "valid",
773 "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders"
774 });
775 let account_resp = serde_json::to_vec(&account_resp)?;
776 enrollment.new_account_response(account_resp)?;
777
778 let enrollment = restore(enrollment, &ctx.context).await;
779
780 let _order_req = enrollment.new_order_request(previous_nonce.to_string()).unwrap();
781 let client_id = match client_id {
782 None => ctx.get_e2ei_client_id().await.to_uri(),
783 Some(client_id) => format!("{}{client_id}", wire_e2e_identity::prelude::E2eiClientId::URI_SCHEME),
784 };
785 let device_identifier = format!("{{\"name\":\"{display_name}\",\"domain\":\"world.com\",\"client-id\":\"{client_id}\",\"handle\":\"wireapp://%40{handle}@world.com\"}}");
786 let user_identifier = format!(
787 "{{\"name\":\"{display_name}\",\"domain\":\"world.com\",\"handle\":\"wireapp://%40{handle}@world.com\"}}"
788 );
789 let order_resp = json!({
790 "status": "pending",
791 "expires": "2037-01-05T14:09:07.99Z",
792 "notBefore": "2016-01-01T00:00:00Z",
793 "notAfter": "2037-01-08T00:00:00Z",
794 "identifiers": [
795 {
796 "type": "wireapp-user",
797 "value": user_identifier
798 },
799 {
800 "type": "wireapp-device",
801 "value": device_identifier
802 }
803 ],
804 "authorizations": [
805 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
806 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
807 ],
808 "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
809 });
810 let order_resp = serde_json::to_vec(&order_resp)?;
811 let new_order = enrollment.new_order_response(order_resp)?;
812
813 let mut enrollment = restore(enrollment, &ctx.context).await;
814
815 let order_url = "https://example.com/acme/wire-acme/order/C7uOXEgg5KPMPtbdE3aVMzv7cJjwUVth";
816
817 let [user_authz_url, device_authz_url] = new_order.authorizations.as_slice() else {
818 unreachable!()
819 };
820
821 let _user_authz_req = enrollment.new_authz_request(user_authz_url.to_string(), previous_nonce.to_string())?;
822
823 let user_authz_resp = json!({
824 "status": "pending",
825 "expires": "2037-01-02T14:09:30Z",
826 "identifier": {
827 "type": "wireapp-user",
828 "value": user_identifier
829 },
830 "challenges": [
831 {
832 "type": "wire-oidc-01",
833 "url": "https://localhost:55170/acme/acme/challenge/ZelRfonEK02jDGlPCJYHrY8tJKNsH0mw/RNb3z6tvknq7vz2U5DoHsSOGiWQyVtAz",
834 "status": "pending",
835 "token": "Gvg5AyOaw0uIQOWKE8lCSIP9nIYwcQiY",
836 "target": "http://example.com/target"
837 }
838 ]
839 });
840 let user_authz_resp = serde_json::to_vec(&user_authz_resp)?;
841 enrollment.new_authz_response(user_authz_resp)?;
842
843 let _device_authz_req =
844 enrollment.new_authz_request(device_authz_url.to_string(), previous_nonce.to_string())?;
845
846 let device_authz_resp = json!({
847 "status": "pending",
848 "expires": "2037-01-02T14:09:30Z",
849 "identifier": {
850 "type": "wireapp-device",
851 "value": device_identifier
852 },
853 "challenges": [
854 {
855 "type": "wire-dpop-01",
856 "url": "https://localhost:55170/acme/acme/challenge/ZelRfonEK02jDGlPCJYHrY8tJKNsH0mw/0y6hLM0TTOVUkawDhQcw5RB7ONwuhooW",
857 "status": "pending",
858 "token": "Gvg5AyOaw0uIQOWKE8lCSIP9nIYwcQiY",
859 "target": "https://wire.com/clients/4959bc6ab12f2846/access-token"
860 }
861 ]
862 });
863 let device_authz_resp = serde_json::to_vec(&device_authz_resp)?;
864 enrollment.new_authz_response(device_authz_resp)?;
865
866 let enrollment = restore(enrollment, &ctx.context).await;
867
868 let backend_nonce = "U09ZR0tnWE5QS1ozS2d3bkF2eWJyR3ZVUHppSTJsMnU";
869 let _dpop_token = enrollment.create_dpop_token(3600, backend_nonce.to_string())?;
870
871 let access_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjI0NGEzMDE1N2ZhMDMxMmQ2NDU5MWFjODg0NDQ5MDZjZDk4NjZlNTQifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE2MjM4L2RleCIsInN1YiI6IkNsQnBiVHAzYVhKbFlYQndQVTVxYUd4TmVrbDRUMWRHYWs5RVVtbE9SRUYzV1dwck1GcEhSbWhhUkVFeVRucEZlRTVVUlhsT1ZHY3ZObU14T0RZMlpqVTJOell4Tm1Zek1VQjNhWEpsTG1OdmJSSUViR1JoY0EiLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNjgwNzczMjE4LCJpYXQiOjE2ODA2ODY4MTgsIm5vbmNlIjoiT0t4cVNmel9USm5YbGw1TlpRcUdmdyIsImF0X2hhc2giOiI5VnlmTFdKSm55VEJYVm1LaDRCVV93IiwiY19oYXNoIjoibS1xZXdLN3RQdFNPUzZXN3lXMHpqdyIsIm5hbWUiOiJpbTp3aXJlYXBwPWFsaWNlX3dpcmUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJBbGljZSBTbWl0aCJ9.AemU4vGBsz_7j-_FxCZ1cdMPejwgIgDS7BehajJyeqkAncQVK_FXn5K8ZhFqqpPbaBB7ZVF8mABq8pw_PPnYtM36O8kPfxv5y6lxghlV5vv0aiz49eGl3YCgPvOLKVH7Gop4J4KytyFylsFwzHbDuy0-zzv_Tm9KtHjedrLrf1j9bVTtHosjopzGN3eAnVb3ayXritzJuIoeq3bGkmXrykWcMWJlVNfQl5cwPoGM4OBM_9E8bZ0MTQHi4sG1Dip_zhEfvtRYtM_N0RBRyPyJgWbTb90axl9EKCzcwChUFNdrN_DDMTyyOw8UVRBhupvtS1fzGDMUn4pinJqPlKxIjA".to_string();
872 let _dpop_chall_req = enrollment.new_dpop_challenge_request(access_token, previous_nonce.to_string())?;
873 let dpop_chall_resp = json!({
874 "type": "wire-dpop-01",
875 "url": "https://example.com/acme/chall/prV_B7yEyA4",
876 "status": "valid",
877 "token": "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0",
878 "target": "http://example.com/target"
879 });
880 let dpop_chall_resp = serde_json::to_vec(&dpop_chall_resp)?;
881 enrollment.new_dpop_challenge_response(dpop_chall_resp)?;
882
883 let mut enrollment = restore(enrollment, &ctx.context).await;
884
885 let id_token = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzU5NjE3NTYsImV4cCI6MTY3NjA0ODE1NiwibmJmIjoxNjc1OTYxNzU2LCJpc3MiOiJodHRwOi8vaWRwLyIsInN1YiI6ImltcHA6d2lyZWFwcD1OREV5WkdZd05qYzJNekZrTkRCaU5UbGxZbVZtTWpReVpUSXpOVGM0TldRLzY1YzNhYzFhMTYzMWMxMzZAZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwOi8vaWRwLyIsIm5hbWUiOiJTbWl0aCwgQWxpY2UgTSAoUUEpIiwiaGFuZGxlIjoiaW1wcDp3aXJlYXBwPWFsaWNlLnNtaXRoLnFhQGV4YW1wbGUuY29tIiwia2V5YXV0aCI6IlNZNzR0Sm1BSUloZHpSdEp2cHgzODlmNkVLSGJYdXhRLi15V29ZVDlIQlYwb0ZMVElSRGw3cjhPclZGNFJCVjhOVlFObEw3cUxjbWcifQ.0iiq3p5Bmmp8ekoFqv4jQu_GrnPbEfxJ36SCuw-UvV6hCi6GlxOwU7gwwtguajhsd1sednGWZpN8QssKI5_CDQ".to_string();
886 #[cfg(not(target_family = "wasm"))]
887 let new_refresh_token = "new-refresh-token";
888 let _oidc_chall_req = enrollment.new_oidc_challenge_request(
889 id_token,
890 #[cfg(not(target_family = "wasm"))]
891 new_refresh_token.to_string(),
892 previous_nonce.to_string(),
893 )?;
894
895 #[cfg(not(target_family = "wasm"))]
896 assert!(enrollment.get_refresh_token().is_ok());
897
898 let oidc_chall_resp = json!({
899 "type": "wire-oidc-01",
900 "url": "https://localhost:55794/acme/acme/challenge/tR33VAzGrR93UnBV5mTV9nVdTZrG2Ln0/QXgyA324mTntfVAIJKw2cF23i4UFJltk",
901 "status": "valid",
902 "token": "2FpTOmNQvNfWDktNWt1oIJnjLE3MkyFb",
903 "target": "http://example.com/target"
904 });
905 let oidc_chall_resp = serde_json::to_vec(&oidc_chall_resp)?;
906
907 #[cfg(not(target_family = "wasm"))]
908 {
909 let backend = ctx.context.mls_provider().await?;
910 let keystore = backend.key_store();
911 enrollment
912 .new_oidc_challenge_response(&ctx.context.mls_provider().await.unwrap(), oidc_chall_resp)
913 .await?;
914 assert_eq!(RefreshToken::find(keystore).await?.as_str(), new_refresh_token);
916 assert!(enrollment.get_refresh_token().is_err());
918 }
919
920 #[cfg(target_family = "wasm")]
921 enrollment.new_oidc_challenge_response(oidc_chall_resp).await?;
922
923 let mut enrollment = restore(enrollment, &ctx.context).await;
924
925 let _get_order_req = enrollment.check_order_request(order_url.to_string(), previous_nonce.to_string())?;
926
927 let order_resp = json!({
928 "status": "ready",
929 "finalize": "https://localhost:55170/acme/acme/order/FaKNEM5iL79ROLGJdO1DXVzIq5rxPEob/finalize",
930 "identifiers": [
931 {
932 "type": "wireapp-user",
933 "value": user_identifier
934 },
935 {
936 "type": "wireapp-device",
937 "value": device_identifier
938 }
939 ],
940 "authorizations": [
941 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
942 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
943 ],
944 "expires": "2037-02-10T14:59:20Z",
945 "notBefore": "2013-02-09T14:59:20.442908Z",
946 "notAfter": "2037-02-09T15:59:20.442908Z"
947 });
948 let order_resp = serde_json::to_vec(&order_resp)?;
949 enrollment.check_order_response(order_resp)?;
950
951 let mut enrollment = restore(enrollment, &ctx.context).await;
952
953 let _finalize_req = enrollment.finalize_request(previous_nonce.to_string())?;
954 let finalize_resp = json!({
955 "certificate": "https://localhost:55170/acme/acme/certificate/rLhCIYygqzWhUmP1i5tmtZxFUvJPFxSL",
956 "status": "valid",
957 "finalize": "https://localhost:55170/acme/acme/order/FaKNEM5iL79ROLGJdO1DXVzIq5rxPEob/finalize",
958 "identifiers": [
959 {
960 "type": "wireapp-user",
961 "value": user_identifier
962 },
963 {
964 "type": "wireapp-device",
965 "value": device_identifier
966 }
967 ],
968 "authorizations": [
969 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
970 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
971 ],
972 "expires": "2037-02-10T14:59:20Z",
973 "notBefore": "2013-02-09T14:59:20.442908Z",
974 "notAfter": "2037-02-09T15:59:20.442908Z"
975 });
976 let finalize_resp = serde_json::to_vec(&finalize_resp)?;
977 enrollment.finalize_response(finalize_resp)?;
978
979 let mut enrollment = restore(enrollment, &ctx.context).await;
980
981 let _certificate_req = enrollment.certificate_request(previous_nonce.to_string())?;
982
983 let existing_keypair = PkiKeypair::new(case.signature_scheme(), enrollment.sign_sk.to_vec()).unwrap();
984
985 let client_id = QualifiedE2eiClientId::from_str_unchecked(enrollment.client_id.as_str());
986 let cert = CertificateBundle::new(
987 handle,
988 &display_name,
989 Some(&client_id),
990 Some(existing_keypair),
991 x509_test_chain.find_local_intermediate_ca(),
992 );
993
994 let cert_chain = cert
995 .certificate_chain
996 .into_iter()
997 .map(|c| pem::Pem::new("CERTIFICATE", c).to_string())
998 .join("");
999
1000 Ok((enrollment, cert_chain))
1001 }
1002}