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