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 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)]
38/// Supporting struct for CRL registration result
39pub struct CrlRegistration {
40    /// Whether this CRL modifies the old CRL (i.e. has a different revocated cert list)
41    pub dirty: bool,
42    /// Optional expiration timestamp
43    pub expiration: Option<u64>,
44}
45
46impl CentralContext {
47    /// Creates an enrollment instance with private key material you can use in order to fetch
48    /// a new x509 certificate from the acme server.
49    ///
50    /// # Parameters
51    /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com`
52    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
53    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
54    /// * `expiry_sec` - generated x509 certificate expiry in seconds
55    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; // fresh install without a Basic client. Supplying None will generate a new keypair
65        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, // fresh install so no refresh token registered yet
76        )
77    }
78
79    /// Parses the ACME server response from the endpoint fetching x509 certificates and uses it
80    /// to initialize the MLS client with a certificate
81    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/// Wire end to end identity solution for fetching a x509 certificate which identifies a client.
120#[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    /// Builds an instance holding private key material. This instance has to be used in the whole
150    /// enrollment process then dropped to clear secret key material.
151    ///
152    /// # Parameters
153    /// * `client_id` - client identifier e.g. `b7ac11a4-8f01-4527-af88-1c30885a7931:6add501bacd1d90e@example.com`
154    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
155    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
156    /// * `expiry_sec` - generated x509 certificate expiry in seconds
157    #[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    /// Parses the response from `GET /acme/{provisioner-name}/directory`.
200    /// Use this [types::E2eiAcmeDirectory] in the next step to fetch the first nonce from the acme server. Use
201    /// [types::E2eiAcmeDirectory.new_nonce].
202    ///
203    /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
204    ///
205    /// # Parameters
206    /// * `directory` - http response body
207    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    /// For creating a new acme account. This returns a signed JWS-alike request body to send to
215    /// `POST /acme/{provisioner-name}/new-account`.
216    ///
217    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
218    ///
219    /// # Parameters
220    /// * `directory` - you got from [Self::directory_response]
221    /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}`
222    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    /// Parses the response from `POST /acme/{provisioner-name}/new-account`.
232    ///
233    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
234    ///
235    /// # Parameters
236    /// * `account` - http response body
237    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    /// Creates a new acme order for the handle (userId + display name) and the clientId.
245    ///
246    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
247    ///
248    /// # Parameters
249    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-account`
250    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    /// Parses the response from `POST /acme/{provisioner-name}/new-order`.
271    ///
272    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
273    ///
274    /// # Parameters
275    /// * `new_order` - http response body
276    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    /// Creates a new authorization request.
282    ///
283    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
284    ///
285    /// # Parameters
286    /// * `url` - one of the URL in new order's authorizations (from [Self::new_order_response])
287    /// * `account` - you got from [Self::new_account_response]
288    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/new-order`
289    ///   (or from the previous to this method if you are creating the second authorization)
290    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    /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}`
300    ///
301    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
302    ///
303    /// # Parameters
304    /// * `new_authz` - http response body
305    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    /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces
316    /// (from wire-server & acme server) and will be verified by the acme server when verifying the
317    /// challenge (in order to deliver a certificate).
318    ///
319    /// Then send it to
320    /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
321    /// on wire-server.
322    ///
323    /// # Parameters
324    /// * `expiry_secs` - of the client Dpop JWT. This should be equal to the grace period set in Team Management
325    /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server.
326    ///   See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce)
327    /// * `expiry` - token expiry
328    #[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    /// Creates a new challenge request.
353    ///
354    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
355    ///
356    /// # Parameters
357    /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
358    /// * `dpop_challenge` - you found after [Self::new_authz_response]
359    /// * `account` - you got from [Self::new_account_response]
360    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
361    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    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the DPoP challenge
381    ///
382    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
383    ///
384    /// # Parameters
385    /// * `challenge` - http response body
386    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    /// Creates a new challenge request.
392    ///
393    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
394    ///
395    /// # Parameters
396    /// * `id_token` - you get back from Identity Provider
397    /// * `refresh_token` - you get back from Identity Provider to renew the access token
398    /// * `oidc_challenge` - you found after [Self::new_authz_response]
399    /// * `account` - you got from [Self::new_account_response]
400    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
401    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    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}` for the OIDC challenge
433    ///
434    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
435    ///
436    /// # Parameters
437    /// * `challenge` - http response body
438    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            // Now that the OIDC challenge is valid, we can store the refresh token for future uses. Note
449            // that we could have persisted it at the end of the enrollment but what if the next enrollment
450            // steps fail ? Is it a reason good enough not to persist the token and ask the user to
451            // authenticate again: probably not.
452            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    /// Verifies that the previous challenge has been completed.
461    ///
462    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
463    ///
464    /// # Parameters
465    /// * `order_url` - `location` header from http response you got from [Self::new_order_response]
466    /// * `account` - you got from [Self::new_account_response]
467    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/challenge/{challenge-id}`
468    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    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`.
478    ///
479    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
480    ///
481    /// # Parameters
482    /// * `order` - http response body
483    ///
484    /// # Returns
485    /// The finalize url to use with [Self::finalize_request]
486    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    /// Final step before fetching the certificate.
495    ///
496    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
497    ///
498    /// # Parameters
499    /// * `domains` - you want to generate a certificate for e.g. `["wire.com"]`
500    /// * `order` - you got from [Self::check_order_response]
501    /// * `account` - you got from [Self::new_account_response]
502    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}`
503    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    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`.
516    ///
517    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
518    ///
519    /// # Parameters
520    /// * `finalize` - http response body
521    ///
522    /// # Returns
523    /// The certificate url to use with [Self::certificate_request]
524    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    /// Creates a request for finally fetching the x509 certificate.
533    ///
534    /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2).
535    ///
536    /// # Parameters
537    /// * `finalize` - you got from [Self::finalize_response]
538    /// * `account` - you got from [Self::new_account_response]
539    /// * `previous_nonce` - `replay-nonce` response header from `POST /acme/{provisioner-name}/order/{order-id}/finalize`
540    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        // zeroize the private material
563        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)]
575// This is pub(crate), to make constants below usable
576pub(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                // verify the created client can create a conversation
677                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    /// Helps the compiler with its lifetime inference rules while passing async closures
703    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        // used to verify persisting the instance actually does restore it entirely
717        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            // Now Refresh token is persisted in the keystore
915            assert_eq!(RefreshToken::find(keystore).await?.as_str(), new_refresh_token);
916            // No reason at this point to have the refresh token in memory
917            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}