core_crypto/e2e_identity/
mod.rs

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)]
39/// Supporting struct for CRL registration result
40pub struct CrlRegistration {
41    /// Whether this CRL modifies the old CRL (i.e. has a different revocated cert list)
42    pub dirty: bool,
43    /// Optional expiration timestamp
44    pub expiration: Option<u64>,
45}
46
47impl CentralContext {
48    /// Creates an enrollment instance with private key material you can use in order to fetch
49    /// a new x509 certificate from the acme server.
50    ///
51    /// # Parameters
52    /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com`
53    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
54    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
55    /// * `expiry_sec` - generated x509 certificate expiry in seconds
56    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; // fresh install without a Basic client. Supplying None will generate a new keypair
66        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, // fresh install so no refresh token registered yet
80        )
81    }
82
83    /// Parses the ACME server response from the endpoint fetching x509 certificates and uses it
84    /// to initialize the MLS client with a certificate
85    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/// Wire end to end identity solution for fetching a x509 certificate which identifies a client.
130#[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    /// Builds an instance holding private key material. This instance has to be used in the whole
160    /// enrollment process then dropped to clear secret key material.
161    ///
162    /// # Parameters
163    /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com`
164    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
165    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
166    /// * `expiry_sec` - generated x509 certificate expiry in seconds
167    #[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    /// Parses the response from `GET /acme/{provisioner-name}/directory`.
210    /// Use this [types::E2eiAcmeDirectory] in the next step to fetch the first nonce from the acme server. Use
211    /// [types::E2eiAcmeDirectory.new_nonce].
212    ///
213    /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
214    ///
215    /// # Parameters
216    /// * `directory` - http response body
217    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    /// For creating a new acme account. This returns a signed JWS-alike request body to send to
225    /// `POST /acme/{provisioner-name}/new-account`.
226    ///
227    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
228    ///
229    /// # Parameters
230    /// * `directory` - you got from [Self::directory_response]
231    /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}`
232    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    /// Parses the response from `POST /acme/{provisioner-name}/new-account`.
243    ///
244    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
245    ///
246    /// # Parameters
247    /// * `account` - http response body
248    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    /// Creates a new acme order for the handle (userId + display name) and the clientId.
256    ///
257    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
258    ///
259    /// # Parameters
260    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-account`
261    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    /// Parses the response from `POST /acme/{provisioner-name}/new-order`.
283    ///
284    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
285    ///
286    /// # Parameters
287    /// * `new_order` - http response body
288    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    /// Creates a new authorization request.
294    ///
295    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
296    ///
297    /// # Parameters
298    /// * `url` - one of the URL in new order's authorizations (from [Self::new_order_response])
299    /// * `account` - you got from [Self::new_account_response]
300    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-order`
301    ///   (or from the previous to this method if you are creating the second authorization)
302    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    /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}`
312    ///
313    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
314    ///
315    /// # Parameters
316    /// * `new_authz` - http response body
317    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    /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces
328    /// (from wire-server & acme server) and will be verified by the acme server when verifying the
329    /// challenge (in order to deliver a certificate).
330    ///
331    /// Then send it to
332    /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
333    /// on wire-server.
334    ///
335    /// # Parameters
336    /// * `expiry_secs` - of the client Dpop JWT. This should be equal to the grace period set in Team Management
337    /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server.
338    ///   See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce)
339    /// * `expiry` - token expiry
340    #[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    /// Creates a new challenge request.
363    ///
364    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
365    ///
366    /// # Parameters
367    /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
368    /// * `dpop_challenge` - you found after [Self::new_authz_response]
369    /// * `account` - you got from [Self::new_account_response]
370    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
371    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    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the DPoP challenge
389    ///
390    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
391    ///
392    /// # Parameters
393    /// * `challenge` - http response body
394    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    /// Creates a new challenge request.
400    ///
401    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
402    ///
403    /// # Parameters
404    /// * `id_token` - you get back from Identity Provider
405    /// * `refresh_token` - you get back from Identity Provider to renew the access token
406    /// * `oidc_challenge` - you found after [Self::new_authz_response]
407    /// * `account` - you got from [Self::new_account_response]
408    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
409    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    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the OIDC challenge
442    ///
443    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
444    ///
445    /// # Parameters
446    /// * `challenge` - http response body
447    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            // Now that the OIDC challenge is valid, we can store the refresh token for future uses. Note
458            // that we could have persisted it at the end of the enrollment but what if the next enrollment
459            // steps fail ? Is it a reason good enough not to persist the token and ask the user to
460            // authenticate again: probably not.
461            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    /// Verifies that the previous challenge has been completed.
470    ///
471    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
472    ///
473    /// # Parameters
474    /// * `order_url` - `location` header from http response you got from [Self::new_order_response]
475    /// * `account` - you got from [Self::new_account_response]
476    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/challenge/{challenge-id}`
477    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    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`.
487    ///
488    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
489    ///
490    /// # Parameters
491    /// * `order` - http response body
492    ///
493    /// # Returns
494    /// The finalize url to use with [Self::finalize_request]
495    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    /// Final step before fetching the certificate.
504    ///
505    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
506    ///
507    /// # Parameters
508    /// * `domains` - you want to generate a certificate for e.g. `["wire.com"]`
509    /// * `order` - you got from [Self::check_order_response]
510    /// * `account` - you got from [Self::new_account_response]
511    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}`
512    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    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`.
525    ///
526    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
527    ///
528    /// # Parameters
529    /// * `finalize` - http response body
530    ///
531    /// # Returns
532    /// The certificate url to use with [Self::certificate_request]
533    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    /// Creates a request for finally fetching the x509 certificate.
542    ///
543    /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2).
544    ///
545    /// # Parameters
546    /// * `finalize` - you got from [Self::finalize_response]
547    /// * `account` - you got from [Self::new_account_response]
548    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}/finalize`
549    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        // zeroize the private material
573        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)]
585// This is pub(crate), to make constants below usable
586pub(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                // verify the created client can create a conversation
690                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    /// Helps the compiler with its lifetime inference rules while passing async closures
728    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        // used to verify persisting the instance actually does restore it entirely
742        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            // Now Refresh token is persisted in the keystore
954            assert_eq!(RefreshToken::find(keystore).await?.as_str(), new_refresh_token);
955            // No reason at this point to have the refresh token in memory
956            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}