wire_e2e_identity/
lib.rs

1//! Support for obtaining X.509 certificates that bind keys to Wire users.
2//!
3//! # Overview
4//!
5//! On a high level, the process of getting a certificate for a particular Wire client looks like
6//! the following:
7//!
8//! 1. the client requests an [ACME](https://www.rfc-editor.org/rfc/rfc8555.html) server to create a new certificate
9//! 2. the ACME server responds with two challenges, [DPoP](#dpop-challenge-wire-dpop-01) and
10//!    [OIDC](#oidc-challenge-wire-oidc-01)
11//! 3. the client completes the DPoP challenge
12//! 4. the client completes the OIDC challenge
13//! 5. the ACME server verifies that both challenges are succesfully completed
14//! 6. the client generates and submits a CSR (certificate signing request) to the ACME server
15//! 7. the ACME server issues a new certificate
16//!
17//! The client must complete both challenges in order to get a certficate -- completing one
18//! challenge is not enough.
19//!
20//! ```text
21//!     ┌───────────────────┐    ┌───────────────────┐     ┌───────────────────┐
22//!     │                   │◄───┤                   ├────►│                   │
23//!     │    ACME server    │    │    Wire client    │     │    Wire server    │
24//!     │                   ├───►│                   │◄────┤                   │
25//!     └───────────────────┘    └───────────┬───────┘     └───────────────────┘
26//!                                      ▲   │
27//!                                      │   │             ┌───────────────────┐
28//!                                      │   └────────────►│                   │
29//!                                      │                 │     OIDC IdP      │
30//!                                      └─────────────────┤                   │
31//!                                                        └───────────────────┘
32//! ```
33//!
34//! # Challenges
35//!
36//! The only ACME server implementation supporting the two custom challenge types necessary for the flow is
37//! [step-ca](https://github.com/smallstep/certificates).
38//!
39//! ## DPoP challenge: `wire-dpop-01`
40//!
41//! This challenge checks whether the data stored with the Wire server (client ID, user name) matches
42//! the data in the device ID that the client submitted when creating the corresponding order.
43//!
44//! The challenge requires a Wire server that supports the `/clients/{cid}/access-token` endpoint.
45//!
46//! The flow is roughly as follows:
47//!  - the client requests a fresh nonce from the Wire server via the `/clients/{cid}/nonce` endpoint
48//!  - the Wire server responds with a nonce
49//!  - the client constructs a [DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) token
50//!     - the token contains client ID, team and user name
51//!     - the token is signed by the clients ACME account key (see [Terminology](https://www.rfc-editor.org/rfc/rfc8555.html#section-3))
52//!  - the client sends the DPoP, together with the nonce, to the Wire server, via the `/clients/{cid}/access-token`
53//!    endpoint
54//!  - the Wire server responds with an access token
55//!     - the access token contains the DPoP provided by the client
56//!     - the access token is signed by the key whose public part is known to the ACME server
57//!  - the client sends the Wire access token to the ACME server
58//!  - the ACME server verifies the challenge by
59//!     - verifying the outer, Wire access token signature
60//!     - verifying the inner, DPoP token signature
61//!     - verifying that the DPoP claims match values specified by the challenge
62//!
63//! [Validation function](https://github.com/smallstep/certificates/blob/2746cd06fb8d68c6720a00e96257038b7e0bbb54/acme/challenge.go#L537)
64//!
65//! ## OIDC challenge: `wire-oidc-01`
66//!
67//! This challenge checks whether the data stored with the IdP (name, preferred username) matches
68//! the data in the user ID that the client submitted when creating the corresponding order.
69//!
70//! The challenge requires an OpenID Connect 1.0 conformant provider with
71//! support for the [`claims` parameter](https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter)
72//! in the authorization request.
73//!
74//! **Important**: the IdP (identity provider) has to support specifying the values of the claims
75//! requested and those values must be present in the returned ID token. In particular, this
76//! mechanism is used by the client to specify values of the `keyauth` and `acme_aud` claims.
77//! The client expects the IdP to copy those values to the ID token,
78//! e.g.
79//! ```text
80//!  {
81//!   ...
82//!   "name": "Alice Smith",
83//!   "acme_aud": "https://stepca:32791/acme/wire/challenge/vkdvGckMpDfPFDwOYO6LZhSBx2...
84//!   "keyauth": "MXwp2QZezi1xShr3wjNuqKDmmapvtnxv.StoKb1FuB59XWMLjhsdeU94T95R_AoMP3u2g9HscYog",
85//!   ...
86//!  }
87//! ```
88//!
89//! Strictly speaking, this usage of the `claims` parameter is not according to the OIDC spec as
90//! the provider has no way to check that the specified values match.
91//!
92//! The flow makes use of the [PKCE extension](https://www.rfc-editor.org/rfc/rfc7636) to the
93//! [OAuth 2.0 Authorization Code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
94//! and looks roughly as follows:
95//!  - the client generates a PKCE `(code challenge, code verifier)` pair
96//!  - the client makes an authorization request to the user's IdP
97//!     - the request makes use of the `claims` parameter to instruct the IdP to include `keyauth` and `acme_aud` claims
98//!       with the provided values in the ID token
99//!     - the request specifies the `redirect_uri` parameter which is going to be used by the IdP to redirect the user
100//!       back after a login
101//!     - the request includes the PKCE code challenge value, to be verified later by the IdP when the client requests
102//!       an access token
103//!  - the user is redirected to the IdP's login page, where authentication is performed
104//!  - after a successful authentication, the IdP redirects the user back to the OIDC (in this case Wire) client,
105//!    returning the authorization code
106//!  - the client uses the authorization code and the PKCE code verifier to make an access token request to the IdP
107//!  - the IdP verifies both authorization code and the PKCE code verifier and returns the access token with the ID
108//!    token embedded in it; at this point the IdP is no longer needed
109//!  - the client extracts the ID token from the access token and sends it to the ACME server
110//!  - the ACME server verifies the challenge by
111//!     - verifying the ID token signature
112//!     - verifying that the `keyauth` value matches the key authorization value for this challenge
113//!     - verifying that the `acme_aud` value matches the URL of this challenge
114//!     - verifying that the ID token claims match values specified by the challenge (name, Wire handle)
115//!
116//! [Validation function](https://github.com/smallstep/certificates/blob/2746cd06fb8d68c6720a00e96257038b7e0bbb54/acme/challenge.go#L407)
117//!
118//! ## References
119//!
120//! - [RFC7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
121//! - [RFC8725: JSON Web Token Best Current Practices](https://www.ietf.org/rfc/rfc8725)
122//! - [RFC7636: Proof Key for Code Exchange by OAuth Public Clients](https://www.rfc-editor.org/rfc/rfc7636)
123//! - [RFC8555: Automatic Certificate Management Environment (ACME)](https://www.rfc-editor.org/rfc/rfc8555)
124//! - [RFC9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://www.rfc-editor.org/rfc/rfc9449)
125//! - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
126use error::*;
127use jwt_simple::prelude::{ES256KeyPair, ES384KeyPair, ES512KeyPair, Ed25519KeyPair, Jwk};
128use prelude::*;
129use rusty_jwt_tools::{
130    jwk::TryIntoJwk,
131    jwk_thumbprint::JwkThumbprint,
132    prelude::{ClientId, Dpop, Handle, Htm, Pem, RustyJwtTools},
133};
134use zeroize::Zeroize;
135
136use crate::{
137    acme::prelude::{AcmeChallenge, AcmeIdentifier},
138    prelude::x509::revocation::PkiEnvironment,
139};
140
141pub mod acme;
142mod error;
143mod types;
144
145pub mod prelude {
146    #[cfg(feature = "builder")]
147    pub use rusty_jwt_tools::prelude::generate_jwk;
148    pub use rusty_jwt_tools::prelude::{
149        ClientId as E2eiClientId, Handle, HashAlgorithm, JwsAlgorithm, RustyJwtError, parse_json_jwk,
150    };
151
152    pub use super::{
153        RustyE2eIdentity,
154        error::{E2eIdentityError, E2eIdentityResult},
155        types::{
156            E2eiAcmeAccount, E2eiAcmeAuthorization, E2eiAcmeChallenge, E2eiAcmeFinalize, E2eiAcmeOrder,
157            E2eiNewAcmeOrder,
158        },
159    };
160    pub use crate::acme::prelude::{
161        AcmeDirectory, RustyAcme, RustyAcmeError, WireIdentity, WireIdentityReader, compute_raw_key_thumbprint, x509,
162        x509::IdentityStatus,
163    };
164}
165
166pub type Json = serde_json::Value;
167
168#[derive(Debug, serde::Serialize, serde::Deserialize)]
169pub struct RustyE2eIdentity {
170    pub sign_alg: JwsAlgorithm,
171    pub sign_kp: Pem,
172    pub hash_alg: HashAlgorithm,
173    pub acme_kp: Pem,
174    pub acme_jwk: Jwk,
175}
176
177/// Enrollment flow.
178impl RustyE2eIdentity {
179    /// Builds an instance holding private key material. This instance has to be used in the whole
180    /// enrollment process then dropped to clear secret key material.
181    ///
182    /// # Parameters
183    /// * `sign_alg` - Signature algorithm (only Ed25519 for now)
184    /// * `raw_sign_key` - Raw signature key as bytes
185    pub fn try_new(sign_alg: JwsAlgorithm, mut raw_sign_key: Vec<u8>) -> E2eIdentityResult<Self> {
186        let sign_kp = match sign_alg {
187            JwsAlgorithm::Ed25519 => Ed25519KeyPair::from_bytes(&raw_sign_key[..])?.to_pem(),
188            JwsAlgorithm::P256 => ES256KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?,
189            JwsAlgorithm::P384 => ES384KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?,
190            JwsAlgorithm::P521 => ES512KeyPair::from_bytes(&raw_sign_key[..])?.to_pem()?,
191        };
192        let (acme_kp, acme_jwk) = match sign_alg {
193            JwsAlgorithm::Ed25519 => {
194                let kp = Ed25519KeyPair::generate();
195                (kp.to_pem().into(), kp.public_key().try_into_jwk()?)
196            }
197            JwsAlgorithm::P256 => {
198                let kp = ES256KeyPair::generate();
199                (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?)
200            }
201            JwsAlgorithm::P384 => {
202                let kp = ES384KeyPair::generate();
203                (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?)
204            }
205            JwsAlgorithm::P521 => {
206                let kp = ES512KeyPair::generate();
207                (kp.to_pem()?.into(), kp.public_key().try_into_jwk()?)
208            }
209        };
210        // drop the private immediately since it already has been copied
211        raw_sign_key.zeroize();
212        Ok(Self {
213            sign_alg,
214            sign_kp: sign_kp.into(),
215            hash_alg: HashAlgorithm::from(sign_alg),
216            acme_kp,
217            acme_jwk,
218        })
219    }
220
221    /// Parses the response from `GET /acme/{provisioner-name}/directory`.
222    /// Use this [AcmeDirectory] in the next step to fetch the first nonce from the acme server. Use
223    /// [AcmeDirectory::new_nonce].
224    ///
225    /// See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
226    ///
227    /// # Parameters
228    /// * `directory` - http response body
229    pub fn acme_directory_response(&self, directory: Json) -> E2eIdentityResult<AcmeDirectory> {
230        let directory = RustyAcme::acme_directory_response(directory)?;
231        Ok(directory)
232    }
233
234    /// For creating a new acme account. This returns a signed JWS-alike request body to send to
235    /// `POST /acme/{provisioner-name}/new-account`.
236    ///
237    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
238    ///
239    /// # Parameters
240    /// * `directory` - you got from [Self::acme_directory_response]
241    /// * `previous_nonce` - you got from calling `HEAD {directory.new_nonce}`
242    pub fn acme_new_account_request(
243        &self,
244        directory: &AcmeDirectory,
245        previous_nonce: String,
246    ) -> E2eIdentityResult<Json> {
247        let acct_req = RustyAcme::new_account_request(directory, self.sign_alg, &self.acme_kp, previous_nonce)?;
248        Ok(serde_json::to_value(acct_req)?)
249    }
250
251    /// Parses the response from `POST /acme/{provisioner-name}/new-account`.
252    ///
253    /// See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
254    ///
255    /// # Parameters
256    /// * `account` - http response body
257    pub fn acme_new_account_response(&self, account: Json) -> E2eIdentityResult<E2eiAcmeAccount> {
258        RustyAcme::new_account_response(account)?.try_into()
259    }
260
261    /// Creates a new acme order for the handle (userId + display name) and the clientId.
262    ///
263    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
264    ///
265    /// # Parameters
266    /// * `display_name` - human readable name displayed in the application e.g. `Smith, Alice M (QA)`
267    /// * `domain` - DNS name of owning backend e.g. `example.com`
268    /// * `client_id` - client identifier with user b64Url encoded & clientId hex encoded e.g.
269    ///   `NDUyMGUyMmY2YjA3NGU3NjkyZjE1NjJjZTAwMmQ2NTQ/6add501bacd1d90e@example.com`
270    /// * `handle` - user handle e.g. `alice.smith.qa@example.com`
271    /// * `expiry` - x509 generated certificate expiry
272    /// * `directory` - you got from [Self::acme_directory_response]
273    /// * `account` - you got from [Self::acme_new_account_response]
274    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/new-account`
275    #[allow(clippy::too_many_arguments)]
276    pub fn acme_new_order_request(
277        &self,
278        display_name: &str,
279        client_id: &str,
280        handle: &str,
281        expiry: core::time::Duration,
282        directory: &AcmeDirectory,
283        account: &E2eiAcmeAccount,
284        previous_nonce: String,
285    ) -> E2eIdentityResult<Json> {
286        let account = account.clone().try_into()?;
287        let client_id = ClientId::try_from_qualified(client_id)?;
288        let order_req = RustyAcme::new_order_request(
289            display_name,
290            client_id,
291            &handle.into(),
292            expiry,
293            directory,
294            &account,
295            self.sign_alg,
296            &self.acme_kp,
297            previous_nonce,
298        )?;
299        Ok(serde_json::to_value(order_req)?)
300    }
301
302    /// Parses the response from `POST /acme/{provisioner-name}/new-order`.
303    ///
304    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
305    ///
306    /// # Parameters
307    /// * `new_order` - http response body
308    pub fn acme_new_order_response(&self, new_order: Json) -> E2eIdentityResult<E2eiNewAcmeOrder> {
309        let new_order = RustyAcme::new_order_response(new_order)?;
310        let json_new_order = serde_json::to_vec(&new_order)?.into();
311        Ok(E2eiNewAcmeOrder {
312            delegate: json_new_order,
313            authorizations: new_order.authorizations,
314        })
315    }
316
317    /// Creates a new authorization request.
318    ///
319    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
320    ///
321    /// # Parameters
322    /// * `url` - one of the URL in new order's authorizations (from [Self::acme_new_order_response])
323    /// * `account` - you got from [Self::acme_new_account_response]
324    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/new-order` (or from the
325    ///   previous to this method if you are creating the second authorization)
326    pub fn acme_new_authz_request(
327        &self,
328        url: &url::Url,
329        account: &E2eiAcmeAccount,
330        previous_nonce: String,
331    ) -> E2eIdentityResult<Json> {
332        let account = account.clone().try_into()?;
333        let authz_req = RustyAcme::new_authz_request(url, &account, self.sign_alg, &self.acme_kp, previous_nonce)?;
334        Ok(serde_json::to_value(authz_req)?)
335    }
336
337    /// Parses the response from `POST /acme/{provisioner-name}/authz/{authz-id}`
338    ///
339    /// You then have to map the challenge from this authorization object. The `client_id_challenge`
340    /// will be the one with the `client_id_host` (you supplied to [Self::acme_new_order_request]) identifier,
341    /// the other will be your `handle_challenge`.
342    ///
343    /// See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
344    ///
345    /// # Parameters
346    /// * `new_authz` - http response body
347    pub fn acme_new_authz_response(&self, new_authz: Json) -> E2eIdentityResult<E2eiAcmeAuthorization> {
348        let authz = serde_json::from_value(new_authz)?;
349        let authz = RustyAcme::new_authz_response(authz)?;
350
351        let [challenge] = authz.challenges;
352        Ok(match authz.identifier {
353            AcmeIdentifier::WireappUser(_) => {
354                let thumbprint = JwkThumbprint::generate(&self.acme_jwk, self.hash_alg)?.kid;
355                let oidc_chall_token = &challenge.token;
356                let keyauth = format!("{oidc_chall_token}.{thumbprint}");
357                E2eiAcmeAuthorization::User {
358                    identifier: authz.identifier.to_json()?,
359                    challenge: challenge.try_into()?,
360                    keyauth,
361                }
362            }
363            AcmeIdentifier::WireappDevice(_) => E2eiAcmeAuthorization::Device {
364                identifier: authz.identifier.to_json()?,
365                challenge: challenge.try_into()?,
366            },
367        })
368    }
369
370    /// Generates a new client Dpop JWT token. It demonstrates proof of possession of the nonces
371    /// (from wire-server & acme server) and will be verified by the acme server when verifying the
372    /// challenge (in order to deliver a certificate).
373    ///
374    /// Then send it to
375    /// [`POST /clients/{id}/access-token`](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
376    /// on wire-server.
377    ///
378    /// # Parameters
379    /// * `access_token_url` - backend endpoint where this token will be sent. Should be [this one](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
380    /// * `client_id` - client identifier with user b64Url encoded & clientId hex encoded e.g.
381    ///   `NDUyMGUyMmY2YjA3NGU3NjkyZjE1NjJjZTAwMmQ2NTQ:6add501bacd1d90e@example.com`
382    /// * `dpop_challenge` - you found after [Self::acme_new_authz_response]
383    /// * `backend_nonce` - you get by calling `GET /clients/token/nonce` on wire-server.
384    /// * `handle` - user handle e.g. `alice.smith.qa@example.com` See endpoint [definition](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/get_clients__client__nonce)
385    /// * `expiry` - token expiry
386    #[allow(clippy::too_many_arguments)]
387    pub fn new_dpop_token(
388        &self,
389        client_id: &str,
390        display_name: &str,
391        dpop_challenge: &E2eiAcmeChallenge,
392        backend_nonce: String,
393        handle: &str,
394        team: Option<String>,
395        expiry: core::time::Duration,
396    ) -> E2eIdentityResult<String> {
397        let dpop_chall: AcmeChallenge = dpop_challenge.clone().try_into()?;
398        let audience = dpop_chall.url;
399        let client_id = ClientId::try_from_qualified(client_id)?;
400        let handle = Handle::from(handle).try_to_qualified(&client_id.domain)?;
401        let dpop = Dpop {
402            htm: Htm::Post,
403            htu: dpop_challenge.target.clone().into(),
404            challenge: dpop_chall.token.into(),
405            handle,
406            team: team.into(),
407            display_name: display_name.to_string(),
408            extra_claims: None,
409        };
410        Ok(RustyJwtTools::generate_dpop_token(
411            dpop,
412            &client_id,
413            backend_nonce.into(),
414            audience,
415            expiry,
416            self.sign_alg,
417            &self.acme_kp,
418        )?)
419    }
420
421    /// Creates a new challenge request.
422    ///
423    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
424    ///
425    /// # Parameters
426    /// * `access_token` - returned by wire-server from [this endpoint](https://staging-nginz-https.zinfra.io/api/swagger-ui/#/default/post_clients__cid__access_token)
427    /// * `dpop_challenge` - you found after [Self::acme_new_authz_response]
428    /// * `account` - you got from [Self::acme_new_account_response]
429    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
430    pub fn acme_dpop_challenge_request(
431        &self,
432        access_token: String,
433        dpop_challenge: &E2eiAcmeChallenge,
434        account: &E2eiAcmeAccount,
435        previous_nonce: String,
436    ) -> E2eIdentityResult<Json> {
437        let account = account.clone().try_into()?;
438        let dpop_challenge: AcmeChallenge = dpop_challenge.clone().try_into()?;
439        let new_challenge_req = RustyAcme::dpop_chall_request(
440            access_token,
441            dpop_challenge,
442            &account,
443            self.sign_alg,
444            &self.acme_kp,
445            previous_nonce,
446        )?;
447        Ok(serde_json::to_value(new_challenge_req)?)
448    }
449
450    /// Creates a new challenge request.
451    ///
452    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
453    ///
454    /// # Parameters
455    /// * `id_token` - returned by Identity Provider
456    /// * `oidc_challenge` - you found after [Self::acme_new_authz_response]
457    /// * `account` - you got from [Self::acme_new_account_response]
458    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/authz/{authz-id}`
459    pub fn acme_oidc_challenge_request(
460        &self,
461        id_token: String,
462        oidc_challenge: &E2eiAcmeChallenge,
463        account: &E2eiAcmeAccount,
464        previous_nonce: String,
465    ) -> E2eIdentityResult<Json> {
466        let account = account.clone().try_into()?;
467        let oidc_chall: AcmeChallenge = oidc_challenge.clone().try_into()?;
468        let new_challenge_req = RustyAcme::oidc_chall_request(
469            id_token,
470            oidc_chall,
471            &account,
472            self.sign_alg,
473            &self.acme_kp,
474            previous_nonce,
475        )?;
476        Ok(serde_json::to_value(new_challenge_req)?)
477    }
478
479    /// Parses the response from `POST /acme/{provisioner-name}/challenge/{challenge-id}`.
480    ///
481    /// See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
482    ///
483    /// # Parameters
484    /// * `challenge` - http response body
485    pub fn acme_new_challenge_response(&self, challenge: Json) -> E2eIdentityResult<()> {
486        let challenge = serde_json::from_value(challenge)?;
487        RustyAcme::new_chall_response(challenge)?;
488        Ok(())
489    }
490
491    /// Verifies that the previous challenge has been completed.
492    ///
493    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
494    ///
495    /// # Parameters
496    /// * `order_url` - "location" header from http response you got from [Self::acme_new_order_response]
497    /// * `account` - you got from [Self::acme_new_account_response]
498    /// * `previous_nonce` - "replay-nonce" response header from `POST
499    ///   /acme/{provisioner-name}/challenge/{challenge-id}`
500    pub fn acme_check_order_request(
501        &self,
502        order_url: url::Url,
503        account: &E2eiAcmeAccount,
504        previous_nonce: String,
505    ) -> E2eIdentityResult<Json> {
506        let account = account.clone().try_into()?;
507        let check_order_req =
508            RustyAcme::check_order_request(order_url, &account, self.sign_alg, &self.acme_kp, previous_nonce)?;
509        Ok(serde_json::to_value(check_order_req)?)
510    }
511
512    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}`.
513    ///
514    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
515    ///
516    /// # Parameters
517    /// * `order` - http response body
518    pub fn acme_check_order_response(&self, order: Json) -> E2eIdentityResult<E2eiAcmeOrder> {
519        RustyAcme::check_order_response(order)?.try_into()
520    }
521
522    /// Final step before fetching the certificate.
523    ///
524    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
525    ///
526    /// # Parameters
527    /// * `domains` - domains you want to generate a certificate for e.g. `["wire.com"]`
528    /// * `order` - you got from [Self::acme_check_order_response]
529    /// * `account` - you got from [Self::acme_new_account_response]
530    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/order/{order-id}`
531    pub fn acme_finalize_request(
532        &self,
533        order: &E2eiAcmeOrder,
534        account: &E2eiAcmeAccount,
535        previous_nonce: String,
536    ) -> E2eIdentityResult<Json> {
537        let order = order.clone().try_into()?;
538        let account = account.clone().try_into()?;
539        let finalize_req = RustyAcme::finalize_req(
540            &order,
541            &account,
542            self.sign_alg,
543            &self.acme_kp,
544            &self.sign_kp,
545            previous_nonce,
546        )?;
547        Ok(serde_json::to_value(finalize_req)?)
548    }
549
550    /// Parses the response from `POST /acme/{provisioner-name}/order/{order-id}/finalize`.
551    ///
552    /// See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
553    ///
554    /// # Parameters
555    /// * `finalize` - http response body
556    pub fn acme_finalize_response(&self, finalize: Json) -> E2eIdentityResult<E2eiAcmeFinalize> {
557        RustyAcme::finalize_response(finalize)?.try_into()
558    }
559
560    /// Creates a request for finally fetching the x509 certificate.
561    ///
562    /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2).
563    ///
564    /// # Parameters
565    /// * `domains` - domains you want to generate a certificate for e.g. `["wire.com"]`
566    /// * `order` - you got from [Self::acme_check_order_response]
567    /// * `account` - you got from [Self::acme_new_account_response]
568    /// * `previous_nonce` - "replay-nonce" response header from `POST /acme/{provisioner-name}/order/{order-id}`
569    pub fn acme_x509_certificate_request(
570        &self,
571        finalize: E2eiAcmeFinalize,
572        account: E2eiAcmeAccount,
573        previous_nonce: String,
574    ) -> E2eIdentityResult<Json> {
575        let finalize = finalize.try_into()?;
576        let account = account.try_into()?;
577        let certificate_req =
578            RustyAcme::certificate_req(finalize, account, self.sign_alg, &self.acme_kp, previous_nonce)?;
579        Ok(serde_json::to_value(certificate_req)?)
580    }
581
582    /// Parses the response from `POST /acme/{provisioner-name}/certificate/{certificate-id}`.
583    ///
584    /// See [RFC 8555 Section 7.4.2](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2)
585    ///
586    /// # Parameters
587    /// * `response` - http string response body
588    pub fn acme_x509_certificate_response(
589        &self,
590        response: String,
591        order: E2eiAcmeOrder,
592        env: Option<&PkiEnvironment>,
593    ) -> E2eIdentityResult<Vec<Vec<u8>>> {
594        let order = order.try_into()?;
595        Ok(RustyAcme::certificate_response(response, order, self.hash_alg, env)?)
596    }
597}