1use openmls_traits::OpenMlsCryptoProvider;
2use std::collections::HashMap;
3
4use wire_e2e_identity::prelude::{E2eiAcmeAuthorization, RustyE2eIdentity};
5use zeroize::Zeroize;
6
7use mls_crypto_provider::MlsCryptoProvider;
8
9use crate::{
10 RecursiveError,
11 context::CentralContext,
12 e2e_identity::{
13 crypto::E2eiSignatureKeypair, id::QualifiedE2eiClientId, init_certificates::NewCrlDistributionPoint,
14 },
15 mls::credential::x509::CertificatePrivateKey,
16 prelude::{CertificateBundle, MlsCiphersuite, id::ClientId, identifier::ClientIdentifier},
17};
18
19pub(crate) mod conversation_state;
20mod crypto;
21pub(crate) mod device_status;
22pub mod enabled;
23mod error;
24pub(crate) mod id;
25pub(crate) mod identity;
26pub(crate) mod init_certificates;
27#[cfg(not(target_family = "wasm"))]
28pub(crate) mod refresh_token;
29pub(crate) mod rotate;
30pub(crate) mod stash;
31pub mod types;
32
33pub use error::{Error, Result};
34pub use init_certificates::E2eiDumpedPkiEnv;
35
36type Json = Vec<u8>;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct CrlRegistration {
41 pub dirty: bool,
43 pub expiration: Option<u64>,
45}
46
47impl CentralContext {
48 pub async fn e2ei_new_enrollment(
57 &self,
58 client_id: ClientId,
59 display_name: String,
60 handle: String,
61 team: Option<String>,
62 expiry_sec: u32,
63 ciphersuite: MlsCiphersuite,
64 ) -> Result<E2eiEnrollment> {
65 let signature_keypair = None; E2eiEnrollment::try_new(
67 client_id,
68 display_name,
69 handle,
70 team,
71 expiry_sec,
72 &self
73 .mls_provider()
74 .await
75 .map_err(RecursiveError::root("getting mls provider"))?,
76 ciphersuite,
77 signature_keypair,
78 #[cfg(not(target_family = "wasm"))]
79 None, )
81 }
82
83 pub async fn e2ei_mls_init_only(
86 &self,
87 enrollment: &mut E2eiEnrollment,
88 certificate_chain: String,
89 nb_init_key_packages: Option<usize>,
90 ) -> Result<NewCrlDistributionPoint> {
91 let sk = enrollment.get_sign_key_for_mls()?;
92 let cs = enrollment.ciphersuite;
93 let certificate_chain = enrollment
94 .certificate_response(
95 certificate_chain,
96 self.mls_provider()
97 .await
98 .map_err(RecursiveError::root("getting mls provider"))?
99 .authentication_service()
100 .borrow()
101 .await
102 .as_ref()
103 .ok_or(Error::PkiEnvironmentUnset)?,
104 )
105 .await?;
106
107 let crl_new_distribution_points = self
108 .extract_dp_on_init(&certificate_chain[..])
109 .await
110 .map_err(RecursiveError::mls_credential("extracting dp on init"))?;
111
112 let private_key = CertificatePrivateKey {
113 value: sk,
114 signature_scheme: cs.signature_algorithm(),
115 };
116
117 let cert_bundle = CertificateBundle {
118 certificate_chain,
119 private_key,
120 };
121 let identifier = ClientIdentifier::X509(HashMap::from([(cs.signature_algorithm(), cert_bundle)]));
122 self.mls_init(identifier, vec![cs], nb_init_key_packages)
123 .await
124 .map_err(RecursiveError::mls("initializing mls"))?;
125 Ok(crl_new_distribution_points)
126 }
127}
128
129#[derive(Debug, serde::Serialize, serde::Deserialize)]
131pub struct E2eiEnrollment {
132 delegate: RustyE2eIdentity,
133 pub(crate) sign_sk: E2eiSignatureKeypair,
134 client_id: String,
135 display_name: String,
136 handle: String,
137 team: Option<String>,
138 expiry: core::time::Duration,
139 directory: Option<types::E2eiAcmeDirectory>,
140 account: Option<wire_e2e_identity::prelude::E2eiAcmeAccount>,
141 user_authz: Option<E2eiAcmeAuthorization>,
142 device_authz: Option<E2eiAcmeAuthorization>,
143 valid_order: Option<wire_e2e_identity::prelude::E2eiAcmeOrder>,
144 finalize: Option<wire_e2e_identity::prelude::E2eiAcmeFinalize>,
145 ciphersuite: MlsCiphersuite,
146 #[cfg(not(target_family = "wasm"))]
147 refresh_token: Option<refresh_token::RefreshToken>,
148}
149
150impl std::ops::Deref for E2eiEnrollment {
151 type Target = RustyE2eIdentity;
152
153 fn deref(&self) -> &Self::Target {
154 &self.delegate
155 }
156}
157
158impl E2eiEnrollment {
159 #[allow(clippy::too_many_arguments)]
168 pub fn try_new(
169 client_id: ClientId,
170 display_name: String,
171 handle: String,
172 team: Option<String>,
173 expiry_sec: u32,
174 backend: &MlsCryptoProvider,
175 ciphersuite: MlsCiphersuite,
176 sign_keypair: Option<E2eiSignatureKeypair>,
177 #[cfg(not(target_family = "wasm"))] refresh_token: Option<refresh_token::RefreshToken>,
178 ) -> Result<Self> {
179 let alg = ciphersuite.try_into()?;
180 let sign_sk = match sign_keypair {
181 Some(kp) => kp,
182 None => Self::new_sign_key(ciphersuite, backend)?,
183 };
184
185 let client_id = QualifiedE2eiClientId::try_from(client_id.as_slice())?;
186 let client_id = String::try_from(client_id)?;
187 let expiry = core::time::Duration::from_secs(u64::from(expiry_sec));
188 let delegate = RustyE2eIdentity::try_new(alg, sign_sk.clone()).map_err(Error::from)?;
189 Ok(Self {
190 delegate,
191 sign_sk,
192 client_id,
193 display_name,
194 handle,
195 team,
196 expiry,
197 directory: None,
198 account: None,
199 user_authz: None,
200 device_authz: None,
201 valid_order: None,
202 finalize: None,
203 ciphersuite,
204 #[cfg(not(target_family = "wasm"))]
205 refresh_token,
206 })
207 }
208
209 pub fn directory_response(&mut self, directory: Json) -> Result<types::E2eiAcmeDirectory> {
218 let directory = serde_json::from_slice(&directory[..])?;
219 let directory: types::E2eiAcmeDirectory = self.acme_directory_response(directory)?.into();
220 self.directory = Some(directory.clone());
221 Ok(directory)
222 }
223
224 pub fn new_account_request(&self, previous_nonce: String) -> Result<Json> {
233 let directory = self
234 .directory
235 .as_ref()
236 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
237 let account = self.acme_new_account_request(&directory.try_into()?, previous_nonce)?;
238 let account = serde_json::to_vec(&account)?;
239 Ok(account)
240 }
241
242 pub fn new_account_response(&mut self, account: Json) -> Result<()> {
249 let account = serde_json::from_slice(&account[..])?;
250 let account = self.acme_new_account_response(account)?;
251 self.account = Some(account);
252 Ok(())
253 }
254
255 pub fn new_order_request(&self, previous_nonce: String) -> Result<Json> {
262 let directory = self
263 .directory
264 .as_ref()
265 .ok_or(Error::OutOfOrderEnrollment("You must first call 'directoryResponse()'"))?;
266 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
267 "You must first call 'newAccountResponse()'",
268 ))?;
269 let order = self.acme_new_order_request(
270 &self.display_name,
271 &self.client_id,
272 &self.handle,
273 self.expiry,
274 &directory.try_into()?,
275 account,
276 previous_nonce,
277 )?;
278 let order = serde_json::to_vec(&order)?;
279 Ok(order)
280 }
281
282 pub fn new_order_response(&self, order: Json) -> Result<types::E2eiNewAcmeOrder> {
289 let order = serde_json::from_slice(&order[..])?;
290 self.acme_new_order_response(order)?.try_into()
291 }
292
293 pub fn new_authz_request(&self, url: String, previous_nonce: String) -> Result<Json> {
303 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
304 "You must first call 'newAccountResponse()'",
305 ))?;
306 let authz = self.acme_new_authz_request(&url.parse()?, account, previous_nonce)?;
307 let authz = serde_json::to_vec(&authz)?;
308 Ok(authz)
309 }
310
311 pub fn new_authz_response(&mut self, authz: Json) -> Result<types::E2eiNewAcmeAuthz> {
318 let authz = serde_json::from_slice(&authz[..])?;
319 let authz = self.acme_new_authz_response(authz)?;
320 match &authz {
321 E2eiAcmeAuthorization::User { .. } => self.user_authz = Some(authz.clone()),
322 E2eiAcmeAuthorization::Device { .. } => self.device_authz = Some(authz.clone()),
323 };
324 authz.try_into()
325 }
326
327 #[allow(clippy::too_many_arguments)]
341 pub fn create_dpop_token(&self, expiry_secs: u32, backend_nonce: String) -> Result<String> {
342 let expiry = core::time::Duration::from_secs(expiry_secs as u64);
343 let authz = self
344 .device_authz
345 .as_ref()
346 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
347 let challenge = match authz {
348 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
349 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
350 };
351 Ok(self.new_dpop_token(
352 &self.client_id,
353 self.display_name.as_str(),
354 challenge,
355 backend_nonce,
356 self.handle.as_str(),
357 self.team.clone(),
358 expiry,
359 )?)
360 }
361
362 pub fn new_dpop_challenge_request(&self, access_token: String, previous_nonce: String) -> Result<Json> {
372 let authz = self
373 .device_authz
374 .as_ref()
375 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
376 let challenge = match authz {
377 E2eiAcmeAuthorization::Device { challenge, .. } => challenge,
378 E2eiAcmeAuthorization::User { .. } => return Err(Error::ImplementationError),
379 };
380 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
381 "You must first call 'newAccountResponse()'",
382 ))?;
383 let challenge = self.acme_dpop_challenge_request(access_token, challenge, account, previous_nonce)?;
384 let challenge = serde_json::to_vec(&challenge)?;
385 Ok(challenge)
386 }
387
388 pub fn new_dpop_challenge_response(&self, challenge: Json) -> Result<()> {
395 let challenge = serde_json::from_slice(&challenge[..])?;
396 Ok(self.acme_new_challenge_response(challenge)?)
397 }
398
399 pub fn new_oidc_challenge_request(
410 &mut self,
411 id_token: String,
412 #[cfg(not(target_family = "wasm"))] refresh_token: String,
413 previous_nonce: String,
414 ) -> Result<Json> {
415 #[cfg(not(target_family = "wasm"))]
416 {
417 if refresh_token.is_empty() {
418 return Err(Error::InvalidRefreshToken);
419 }
420 }
421 let authz = self
422 .user_authz
423 .as_ref()
424 .ok_or(Error::OutOfOrderEnrollment("You must first call 'newAuthzResponse()'"))?;
425 let challenge = match authz {
426 E2eiAcmeAuthorization::User { challenge, .. } => challenge,
427 E2eiAcmeAuthorization::Device { .. } => return Err(Error::ImplementationError),
428 };
429 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
430 "You must first call 'newAccountResponse()'",
431 ))?;
432 let challenge = self.acme_oidc_challenge_request(id_token, challenge, account, previous_nonce)?;
433 let challenge = serde_json::to_vec(&challenge)?;
434 #[cfg(not(target_family = "wasm"))]
435 {
436 self.refresh_token.replace(refresh_token.into());
437 }
438 Ok(challenge)
439 }
440
441 pub async fn new_oidc_challenge_response(
448 &mut self,
449 #[cfg(not(target_family = "wasm"))] backend: &MlsCryptoProvider,
450 challenge: Json,
451 ) -> Result<()> {
452 let challenge = serde_json::from_slice(&challenge[..])?;
453 self.acme_new_challenge_response(challenge)?;
454
455 #[cfg(not(target_family = "wasm"))]
456 {
457 let refresh_token = self.refresh_token.take().ok_or(Error::OutOfOrderEnrollment(
462 "You must first call 'new_oidc_challenge_request()'",
463 ))?;
464 refresh_token.replace(backend).await?;
465 }
466 Ok(())
467 }
468
469 pub fn check_order_request(&self, order_url: String, previous_nonce: String) -> Result<Json> {
478 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
479 "You must first call 'newAccountResponse()'",
480 ))?;
481 let order = self.acme_check_order_request(order_url.parse()?, account, previous_nonce)?;
482 let order = serde_json::to_vec(&order)?;
483 Ok(order)
484 }
485
486 pub fn check_order_response(&mut self, order: Json) -> Result<String> {
496 let order = serde_json::from_slice(&order[..])?;
497 let valid_order = self.acme_check_order_response(order)?;
498 let finalize_url = valid_order.finalize_url.to_string();
499 self.valid_order = Some(valid_order);
500 Ok(finalize_url)
501 }
502
503 pub fn finalize_request(&mut self, previous_nonce: String) -> Result<Json> {
513 let account = self.account.as_ref().ok_or(Error::OutOfOrderEnrollment(
514 "You must first call 'newAccountResponse()'",
515 ))?;
516 let order = self.valid_order.as_ref().ok_or(Error::OutOfOrderEnrollment(
517 "You must first call 'checkOrderResponse()'",
518 ))?;
519 let finalize = self.acme_finalize_request(order, account, previous_nonce)?;
520 let finalize = serde_json::to_vec(&finalize)?;
521 Ok(finalize)
522 }
523
524 pub fn finalize_response(&mut self, finalize: Json) -> Result<String> {
534 let finalize = serde_json::from_slice(&finalize[..])?;
535 let finalize = self.acme_finalize_response(finalize)?;
536 let certificate_url = finalize.certificate_url.to_string();
537 self.finalize = Some(finalize);
538 Ok(certificate_url)
539 }
540
541 pub fn certificate_request(&mut self, previous_nonce: String) -> Result<Json> {
550 let account = self.account.take().ok_or(Error::OutOfOrderEnrollment(
551 "You must first call 'newAccountResponse()'",
552 ))?;
553 let finalize = self
554 .finalize
555 .take()
556 .ok_or(Error::OutOfOrderEnrollment("You must first call 'finalizeResponse()'"))?;
557 let certificate = self.acme_x509_certificate_request(finalize, account, previous_nonce)?;
558 let certificate = serde_json::to_vec(&certificate)?;
559 Ok(certificate)
560 }
561
562 async fn certificate_response(
563 &mut self,
564 certificate_chain: String,
565 env: &wire_e2e_identity::prelude::x509::revocation::PkiEnvironment,
566 ) -> Result<Vec<Vec<u8>>> {
567 let order = self.valid_order.take().ok_or(Error::OutOfOrderEnrollment(
568 "You must first call 'checkOrderResponse()'",
569 ))?;
570 let certificates = self.acme_x509_certificate_response(certificate_chain, order, Some(env))?;
571
572 self.sign_sk.zeroize();
574 self.delegate.sign_kp.zeroize();
575 self.delegate.acme_kp.zeroize();
576
577 #[cfg(not(target_family = "wasm"))]
578 self.refresh_token.zeroize();
579
580 Ok(certificates)
581 }
582}
583
584#[cfg(test)]
585pub(crate) mod tests {
587 use itertools::Itertools;
588 use mls_crypto_provider::PkiKeypair;
589
590 #[cfg(not(target_family = "wasm"))]
591 use openmls_traits::OpenMlsCryptoProvider;
592 use serde_json::json;
593 use wasm_bindgen_test::*;
594
595 use crate::mls::conversation::Conversation as _;
596 #[cfg(not(target_family = "wasm"))]
597 use crate::{
598 RecursiveError,
599 e2e_identity::{Error, refresh_token::RefreshToken},
600 };
601 use crate::{
602 context::CentralContext,
603 e2e_identity::{Result, id::QualifiedE2eiClientId, tests::x509::X509TestChain},
604 prelude::*,
605 test_utils::{context::TEAM, *},
606 };
607
608 wasm_bindgen_test_configure!(run_in_browser);
609
610 pub(crate) const E2EI_DISPLAY_NAME: &str = "Alice Smith";
611 pub(crate) const E2EI_HANDLE: &str = "alice_wire";
612 pub(crate) const E2EI_CLIENT_ID: &str = "bd4c7053-1c5a-4020-9559-cd7bf7961954:4959bc6ab12f2846@world.com";
613 pub(crate) const E2EI_CLIENT_ID_URI: &str = "vUxwUxxaQCCVWc1795YZVA!4959bc6ab12f2846@world.com";
614 pub(crate) const E2EI_EXPIRY: u32 = 90 * 24 * 3600;
615
616 pub(crate) fn init_enrollment(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
617 Box::pin(async move {
618 let E2eiInitWrapper { context: cc, case } = wrapper;
619 let cs = case.ciphersuite();
620 cc.e2ei_new_enrollment(
621 E2EI_CLIENT_ID.into(),
622 E2EI_DISPLAY_NAME.to_string(),
623 E2EI_HANDLE.to_string(),
624 Some(TEAM.to_string()),
625 E2EI_EXPIRY,
626 cs,
627 )
628 .await
629 })
630 }
631
632 pub(crate) const NEW_HANDLE: &str = "new_alice_wire";
633 pub(crate) const NEW_DISPLAY_NAME: &str = "New Alice Smith";
634 pub(crate) fn init_activation_or_rotation(wrapper: E2eiInitWrapper) -> InitFnReturn<'_> {
635 Box::pin(async move {
636 let E2eiInitWrapper { context: cc, case } = wrapper;
637 let cs = case.ciphersuite();
638 match case.credential_type {
639 MlsCredentialType::Basic => {
640 cc.e2ei_new_activation_enrollment(
641 NEW_DISPLAY_NAME.to_string(),
642 NEW_HANDLE.to_string(),
643 Some(TEAM.to_string()),
644 E2EI_EXPIRY,
645 cs,
646 )
647 .await
648 }
649 MlsCredentialType::X509 => {
650 cc.e2ei_new_rotate_enrollment(
651 Some(NEW_DISPLAY_NAME.to_string()),
652 Some(NEW_HANDLE.to_string()),
653 Some(TEAM.to_string()),
654 E2EI_EXPIRY,
655 cs,
656 )
657 .await
658 }
659 }
660 })
661 }
662
663 #[apply(all_cred_cipher)]
664 #[wasm_bindgen_test]
665 async fn e2e_identity_should_work(case: TestCase) {
666 run_test_wo_clients(case.clone(), move |mut cc| {
667 Box::pin(async move {
668 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
669
670 let is_renewal = false;
671
672 let (mut enrollment, cert) = e2ei_enrollment(
673 &mut cc,
674 &case,
675 &x509_test_chain,
676 Some(E2EI_CLIENT_ID_URI),
677 is_renewal,
678 init_enrollment,
679 noop_restore,
680 )
681 .await
682 .unwrap();
683
684 cc.context
685 .e2ei_mls_init_only(&mut enrollment, cert, Some(INITIAL_KEYING_MATERIAL_COUNT))
686 .await
687 .unwrap();
688
689 let id = conversation_id();
691 cc.context
692 .new_conversation(&id, MlsCredentialType::X509, case.cfg.clone())
693 .await
694 .unwrap();
695 cc.context
696 .conversation(&id)
697 .await
698 .unwrap()
699 .encrypt_message("Hello e2e identity !")
700 .await
701 .unwrap();
702 assert_eq!(
703 cc.context
704 .conversation(&id)
705 .await
706 .unwrap()
707 .e2ei_conversation_state()
708 .await
709 .unwrap(),
710 E2eiConversationState::Verified
711 );
712 assert!(cc.context.e2ei_is_enabled(case.signature_scheme()).await.unwrap());
713 })
714 })
715 .await
716 }
717
718 pub(crate) type RestoreFnReturn<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = E2eiEnrollment> + 'a>>;
719
720 pub(crate) fn noop_restore(e: E2eiEnrollment, _cc: &CentralContext) -> RestoreFnReturn<'_> {
721 Box::pin(async move { e })
722 }
723
724 pub(crate) type InitFnReturn<'a> =
725 std::pin::Pin<Box<dyn std::future::Future<Output = Result<E2eiEnrollment>> + 'a>>;
726
727 pub(crate) struct E2eiInitWrapper<'a> {
729 pub(crate) context: &'a CentralContext,
730 pub(crate) case: &'a TestCase,
731 }
732
733 pub(crate) async fn e2ei_enrollment<'a>(
734 ctx: &'a mut ClientContext,
735 case: &TestCase,
736 x509_test_chain: &X509TestChain,
737 client_id: Option<&str>,
738 #[cfg(not(target_family = "wasm"))] is_renewal: bool,
739 #[cfg(target_family = "wasm")] _is_renewal: bool,
740 init: impl Fn(E2eiInitWrapper) -> InitFnReturn<'_>,
741 restore: impl Fn(E2eiEnrollment, &'a CentralContext) -> RestoreFnReturn<'a>,
743 ) -> Result<(E2eiEnrollment, String)> {
744 x509_test_chain.register_with_central(&ctx.context).await;
745 #[cfg(not(target_family = "wasm"))]
746 {
747 let backend = ctx
748 .context
749 .mls_provider()
750 .await
751 .map_err(RecursiveError::root("getting mls provider"))?;
752 let keystore = backend.key_store();
753 if is_renewal {
754 let initial_refresh_token =
755 crate::e2e_identity::refresh_token::RefreshToken::from("initial-refresh-token".to_string());
756 let initial_refresh_token =
757 core_crypto_keystore::entities::E2eiRefreshToken::from(initial_refresh_token);
758 keystore.save(initial_refresh_token).await?;
759 }
760 }
761
762 let wrapper = E2eiInitWrapper {
763 context: &ctx.context,
764 case,
765 };
766 let mut enrollment = init(wrapper).await?;
767
768 #[cfg(not(target_family = "wasm"))]
769 {
770 let backend = ctx
771 .context
772 .mls_provider()
773 .await
774 .map_err(RecursiveError::root("getting mls provider"))?;
775 let keystore = backend.key_store();
776 if is_renewal {
777 assert!(enrollment.refresh_token.is_some());
778 assert!(RefreshToken::find(keystore).await.is_ok());
779 } else {
780 assert!(matches!(
781 enrollment.get_refresh_token().unwrap_err(),
782 Error::OutOfOrderEnrollment(_)
783 ));
784 assert!(RefreshToken::find(keystore).await.is_err());
785 }
786 }
787
788 let (display_name, handle) = (enrollment.display_name.clone(), &enrollment.handle.clone());
789
790 let directory = json!({
791 "newNonce": "https://example.com/acme/new-nonce",
792 "newAccount": "https://example.com/acme/new-account",
793 "newOrder": "https://example.com/acme/new-order",
794 "revokeCert": "https://example.com/acme/revoke-cert"
795 });
796 let directory = serde_json::to_vec(&directory)?;
797 enrollment.directory_response(directory)?;
798
799 let mut enrollment = restore(enrollment, &ctx.context).await;
800
801 let previous_nonce = "YUVndEZQVTV6ZUNlUkJxRG10c0syQmNWeW1kanlPbjM";
802 let _account_req = enrollment.new_account_request(previous_nonce.to_string())?;
803
804 let account_resp = json!({
805 "status": "valid",
806 "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders"
807 });
808 let account_resp = serde_json::to_vec(&account_resp)?;
809 enrollment.new_account_response(account_resp)?;
810
811 let enrollment = restore(enrollment, &ctx.context).await;
812
813 let _order_req = enrollment.new_order_request(previous_nonce.to_string()).unwrap();
814 let client_id = match client_id {
815 None => ctx.get_e2ei_client_id().await.to_uri(),
816 Some(client_id) => format!("{}{client_id}", wire_e2e_identity::prelude::E2eiClientId::URI_SCHEME),
817 };
818 let device_identifier = format!(
819 "{{\"name\":\"{display_name}\",\"domain\":\"world.com\",\"client-id\":\"{client_id}\",\"handle\":\"wireapp://%40{handle}@world.com\"}}"
820 );
821 let user_identifier = format!(
822 "{{\"name\":\"{display_name}\",\"domain\":\"world.com\",\"handle\":\"wireapp://%40{handle}@world.com\"}}"
823 );
824 let order_resp = json!({
825 "status": "pending",
826 "expires": "2037-01-05T14:09:07.99Z",
827 "notBefore": "2016-01-01T00:00:00Z",
828 "notAfter": "2037-01-08T00:00:00Z",
829 "identifiers": [
830 {
831 "type": "wireapp-user",
832 "value": user_identifier
833 },
834 {
835 "type": "wireapp-device",
836 "value": device_identifier
837 }
838 ],
839 "authorizations": [
840 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
841 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
842 ],
843 "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
844 });
845 let order_resp = serde_json::to_vec(&order_resp)?;
846 let new_order = enrollment.new_order_response(order_resp)?;
847
848 let mut enrollment = restore(enrollment, &ctx.context).await;
849
850 let order_url = "https://example.com/acme/wire-acme/order/C7uOXEgg5KPMPtbdE3aVMzv7cJjwUVth";
851
852 let [user_authz_url, device_authz_url] = new_order.authorizations.as_slice() else {
853 unreachable!()
854 };
855
856 let _user_authz_req = enrollment.new_authz_request(user_authz_url.to_string(), previous_nonce.to_string())?;
857
858 let user_authz_resp = json!({
859 "status": "pending",
860 "expires": "2037-01-02T14:09:30Z",
861 "identifier": {
862 "type": "wireapp-user",
863 "value": user_identifier
864 },
865 "challenges": [
866 {
867 "type": "wire-oidc-01",
868 "url": "https://localhost:55170/acme/acme/challenge/ZelRfonEK02jDGlPCJYHrY8tJKNsH0mw/RNb3z6tvknq7vz2U5DoHsSOGiWQyVtAz",
869 "status": "pending",
870 "token": "Gvg5AyOaw0uIQOWKE8lCSIP9nIYwcQiY",
871 "target": "http://example.com/target"
872 }
873 ]
874 });
875 let user_authz_resp = serde_json::to_vec(&user_authz_resp)?;
876 enrollment.new_authz_response(user_authz_resp)?;
877
878 let _device_authz_req =
879 enrollment.new_authz_request(device_authz_url.to_string(), previous_nonce.to_string())?;
880
881 let device_authz_resp = json!({
882 "status": "pending",
883 "expires": "2037-01-02T14:09:30Z",
884 "identifier": {
885 "type": "wireapp-device",
886 "value": device_identifier
887 },
888 "challenges": [
889 {
890 "type": "wire-dpop-01",
891 "url": "https://localhost:55170/acme/acme/challenge/ZelRfonEK02jDGlPCJYHrY8tJKNsH0mw/0y6hLM0TTOVUkawDhQcw5RB7ONwuhooW",
892 "status": "pending",
893 "token": "Gvg5AyOaw0uIQOWKE8lCSIP9nIYwcQiY",
894 "target": "https://wire.com/clients/4959bc6ab12f2846/access-token"
895 }
896 ]
897 });
898 let device_authz_resp = serde_json::to_vec(&device_authz_resp)?;
899 enrollment.new_authz_response(device_authz_resp)?;
900
901 let enrollment = restore(enrollment, &ctx.context).await;
902
903 let backend_nonce = "U09ZR0tnWE5QS1ozS2d3bkF2eWJyR3ZVUHppSTJsMnU";
904 let _dpop_token = enrollment.create_dpop_token(3600, backend_nonce.to_string())?;
905
906 let access_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjI0NGEzMDE1N2ZhMDMxMmQ2NDU5MWFjODg0NDQ5MDZjZDk4NjZlNTQifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE2MjM4L2RleCIsInN1YiI6IkNsQnBiVHAzYVhKbFlYQndQVTVxYUd4TmVrbDRUMWRHYWs5RVVtbE9SRUYzV1dwck1GcEhSbWhhUkVFeVRucEZlRTVVUlhsT1ZHY3ZObU14T0RZMlpqVTJOell4Tm1Zek1VQjNhWEpsTG1OdmJSSUViR1JoY0EiLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNjgwNzczMjE4LCJpYXQiOjE2ODA2ODY4MTgsIm5vbmNlIjoiT0t4cVNmel9USm5YbGw1TlpRcUdmdyIsImF0X2hhc2giOiI5VnlmTFdKSm55VEJYVm1LaDRCVV93IiwiY19oYXNoIjoibS1xZXdLN3RQdFNPUzZXN3lXMHpqdyIsIm5hbWUiOiJpbTp3aXJlYXBwPWFsaWNlX3dpcmUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJBbGljZSBTbWl0aCJ9.AemU4vGBsz_7j-_FxCZ1cdMPejwgIgDS7BehajJyeqkAncQVK_FXn5K8ZhFqqpPbaBB7ZVF8mABq8pw_PPnYtM36O8kPfxv5y6lxghlV5vv0aiz49eGl3YCgPvOLKVH7Gop4J4KytyFylsFwzHbDuy0-zzv_Tm9KtHjedrLrf1j9bVTtHosjopzGN3eAnVb3ayXritzJuIoeq3bGkmXrykWcMWJlVNfQl5cwPoGM4OBM_9E8bZ0MTQHi4sG1Dip_zhEfvtRYtM_N0RBRyPyJgWbTb90axl9EKCzcwChUFNdrN_DDMTyyOw8UVRBhupvtS1fzGDMUn4pinJqPlKxIjA".to_string();
907 let _dpop_chall_req = enrollment.new_dpop_challenge_request(access_token, previous_nonce.to_string())?;
908 let dpop_chall_resp = json!({
909 "type": "wire-dpop-01",
910 "url": "https://example.com/acme/chall/prV_B7yEyA4",
911 "status": "valid",
912 "token": "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0",
913 "target": "http://example.com/target"
914 });
915 let dpop_chall_resp = serde_json::to_vec(&dpop_chall_resp)?;
916 enrollment.new_dpop_challenge_response(dpop_chall_resp)?;
917
918 let mut enrollment = restore(enrollment, &ctx.context).await;
919
920 let id_token = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzU5NjE3NTYsImV4cCI6MTY3NjA0ODE1NiwibmJmIjoxNjc1OTYxNzU2LCJpc3MiOiJodHRwOi8vaWRwLyIsInN1YiI6ImltcHA6d2lyZWFwcD1OREV5WkdZd05qYzJNekZrTkRCaU5UbGxZbVZtTWpReVpUSXpOVGM0TldRLzY1YzNhYzFhMTYzMWMxMzZAZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwOi8vaWRwLyIsIm5hbWUiOiJTbWl0aCwgQWxpY2UgTSAoUUEpIiwiaGFuZGxlIjoiaW1wcDp3aXJlYXBwPWFsaWNlLnNtaXRoLnFhQGV4YW1wbGUuY29tIiwia2V5YXV0aCI6IlNZNzR0Sm1BSUloZHpSdEp2cHgzODlmNkVLSGJYdXhRLi15V29ZVDlIQlYwb0ZMVElSRGw3cjhPclZGNFJCVjhOVlFObEw3cUxjbWcifQ.0iiq3p5Bmmp8ekoFqv4jQu_GrnPbEfxJ36SCuw-UvV6hCi6GlxOwU7gwwtguajhsd1sednGWZpN8QssKI5_CDQ".to_string();
921 #[cfg(not(target_family = "wasm"))]
922 let new_refresh_token = "new-refresh-token";
923 let _oidc_chall_req = enrollment.new_oidc_challenge_request(
924 id_token,
925 #[cfg(not(target_family = "wasm"))]
926 new_refresh_token.to_string(),
927 previous_nonce.to_string(),
928 )?;
929
930 #[cfg(not(target_family = "wasm"))]
931 assert!(enrollment.get_refresh_token().is_ok());
932
933 let oidc_chall_resp = json!({
934 "type": "wire-oidc-01",
935 "url": "https://localhost:55794/acme/acme/challenge/tR33VAzGrR93UnBV5mTV9nVdTZrG2Ln0/QXgyA324mTntfVAIJKw2cF23i4UFJltk",
936 "status": "valid",
937 "token": "2FpTOmNQvNfWDktNWt1oIJnjLE3MkyFb",
938 "target": "http://example.com/target"
939 });
940 let oidc_chall_resp = serde_json::to_vec(&oidc_chall_resp)?;
941
942 #[cfg(not(target_family = "wasm"))]
943 {
944 let backend = ctx
945 .context
946 .mls_provider()
947 .await
948 .map_err(RecursiveError::root("getting mls provider"))?;
949 let keystore = backend.key_store();
950 enrollment
951 .new_oidc_challenge_response(&ctx.context.mls_provider().await.unwrap(), oidc_chall_resp)
952 .await?;
953 assert_eq!(RefreshToken::find(keystore).await?.as_str(), new_refresh_token);
955 assert!(enrollment.get_refresh_token().is_err());
957 }
958
959 #[cfg(target_family = "wasm")]
960 enrollment.new_oidc_challenge_response(oidc_chall_resp).await?;
961
962 let mut enrollment = restore(enrollment, &ctx.context).await;
963
964 let _get_order_req = enrollment.check_order_request(order_url.to_string(), previous_nonce.to_string())?;
965
966 let order_resp = json!({
967 "status": "ready",
968 "finalize": "https://localhost:55170/acme/acme/order/FaKNEM5iL79ROLGJdO1DXVzIq5rxPEob/finalize",
969 "identifiers": [
970 {
971 "type": "wireapp-user",
972 "value": user_identifier
973 },
974 {
975 "type": "wireapp-device",
976 "value": device_identifier
977 }
978 ],
979 "authorizations": [
980 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
981 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
982 ],
983 "expires": "2037-02-10T14:59:20Z",
984 "notBefore": "2013-02-09T14:59:20.442908Z",
985 "notAfter": "2037-02-09T15:59:20.442908Z"
986 });
987 let order_resp = serde_json::to_vec(&order_resp)?;
988 enrollment.check_order_response(order_resp)?;
989
990 let mut enrollment = restore(enrollment, &ctx.context).await;
991
992 let _finalize_req = enrollment.finalize_request(previous_nonce.to_string())?;
993 let finalize_resp = json!({
994 "certificate": "https://localhost:55170/acme/acme/certificate/rLhCIYygqzWhUmP1i5tmtZxFUvJPFxSL",
995 "status": "valid",
996 "finalize": "https://localhost:55170/acme/acme/order/FaKNEM5iL79ROLGJdO1DXVzIq5rxPEob/finalize",
997 "identifiers": [
998 {
999 "type": "wireapp-user",
1000 "value": user_identifier
1001 },
1002 {
1003 "type": "wireapp-device",
1004 "value": device_identifier
1005 }
1006 ],
1007 "authorizations": [
1008 "https://example.com/acme/authz/6SDQFoXfk1UT75qRfzurqxWCMEatapiL",
1009 "https://example.com/acme/authz/d2sJyM0MaV6wTX4ClP8eUQ8TF4ZKk7jz"
1010 ],
1011 "expires": "2037-02-10T14:59:20Z",
1012 "notBefore": "2013-02-09T14:59:20.442908Z",
1013 "notAfter": "2037-02-09T15:59:20.442908Z"
1014 });
1015 let finalize_resp = serde_json::to_vec(&finalize_resp)?;
1016 enrollment.finalize_response(finalize_resp)?;
1017
1018 let mut enrollment = restore(enrollment, &ctx.context).await;
1019
1020 let _certificate_req = enrollment.certificate_request(previous_nonce.to_string())?;
1021
1022 let existing_keypair = PkiKeypair::new(case.signature_scheme(), enrollment.sign_sk.to_vec()).unwrap();
1023
1024 let client_id = QualifiedE2eiClientId::from_str_unchecked(enrollment.client_id.as_str());
1025 let cert = CertificateBundle::new(
1026 handle,
1027 &display_name,
1028 Some(&client_id),
1029 Some(existing_keypair),
1030 x509_test_chain.find_local_intermediate_ca(),
1031 );
1032
1033 let cert_chain = cert
1034 .certificate_chain
1035 .into_iter()
1036 .map(|c| pem::Pem::new("CERTIFICATE", c).to_string())
1037 .join("");
1038
1039 Ok((enrollment, cert_chain))
1040 }
1041}