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}