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}