Skip to main content

wire_e2e_identity/acquisition/
mod.rs

1use std::sync::Arc;
2
3use jwt_simple::prelude::Jwk;
4use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, JwsAlgorithm, Pem};
5use url::Url;
6
7use crate::{
8    acme::AcmeJws,
9    pki_env::{
10        PkiEnvironment,
11        hooks::{HttpHeader, HttpMethod, HttpResponse},
12    },
13};
14
15mod checks;
16mod dpop_challenge;
17mod error;
18mod initial;
19mod oidc_challenge;
20mod serialization;
21
22pub mod identity;
23pub mod thumbprint;
24
25pub use error::Error as AcquisitionError;
26pub use serialization::ClientIdDef;
27
28#[derive(Debug, serde::Serialize, serde::Deserialize)]
29pub struct X509CredentialConfiguration {
30    pub acme_directory_url: String,
31    pub sign_alg: JwsAlgorithm,
32    pub hash_alg: HashAlgorithm,
33    pub display_name: String,
34    #[serde(with = "ClientIdDef")]
35    pub client_id: ClientId,
36    pub handle: String,
37    pub domain: String,
38    pub team: Option<String>,
39    pub validity_period: std::time::Duration,
40}
41
42pub mod states {
43    use crate::acme::{AcmeAccount, AcmeChallenge, AcmeOrder};
44
45    #[derive(Debug, serde::Serialize, serde::Deserialize)]
46    pub struct Initialized;
47
48    #[derive(Debug, serde::Serialize, serde::Deserialize)]
49    pub struct DpopChallengeCompleted {
50        pub nonce: String,
51        pub acme_account: AcmeAccount,
52        pub order: AcmeOrder,
53        pub oidc_challenge: AcmeChallenge,
54    }
55}
56
57#[derive(core_crypto_macros::Debug)]
58/// The type representing the X509 acquisition process.
59///
60/// Performs the two ACME challenges necessary to obtain a certificate,
61/// wire-dpop-01 and wire-oidc-01, in that order.
62///
63/// State transitions:
64///      (*)
65///       |
66///       | ::try_new()
67///       |
68///       v
69///  Initialized
70///       |
71///       | .complete_dpop_challenge()
72///       |
73///       v
74///  DpopChallengeCompleted
75///       |
76///       | .complete_oidc_challenge()
77///       |
78///       v
79///  (no final state, acquisition is consumed)
80///
81/// After the second (OIDC) challenge, the signing keypair and the certificate
82/// chain is returned to the caller. Regardless of success, the acquisition
83/// instance is consumed and cannot be used anymore.
84///
85/// Sample usage:
86///
87/// ```rust,ignore
88/// let acq = X509CredentialAcquisition::try_new(pki_env, config)?;
89/// let (sign_kp, certs) = acq
90///     .complete_dpop_challenge().await?
91///     .complete_oidc_challenge().await?;
92/// ```
93#[derive(serde::Serialize)]
94pub struct X509CredentialAcquisition<T: std::fmt::Debug = states::Initialized> {
95    /// A reference to the PKI environment that stores trust anchors.
96    #[serde(skip)]
97    pki_env: Arc<PkiEnvironment>,
98    /// The configuration used for acquisition.
99    config: X509CredentialConfiguration,
100    /// The signing keypair, public part of which will be certified
101    /// by the ACME server via inclusion in the certificate.
102    /// This keypair is essentially the credential.
103    #[sensitive]
104    sign_kp: Pem,
105    /// The keypair used to sign requests (JWS messages) sent to
106    /// the ACME server. Bound to the ACME client account.
107    #[sensitive]
108    acme_kp: Pem,
109    /// Public part of the `acme_kp` keypair, in JSON Web Key form.
110    acme_jwk: Jwk,
111    /// State-specific data.
112    data: T,
113}
114
115pub type Result<T> = std::result::Result<T, error::Error>;
116
117fn get_header(resp: &HttpResponse, header: &'static str) -> Result<String> {
118    resp.first_header(header)
119        .ok_or_else(|| error::Error::MissingHeader(header))
120}
121
122impl<T: std::fmt::Debug> X509CredentialAcquisition<T> {
123    /// The signing algorithm used for certificate acquisition.
124    pub fn sign_alg(&self) -> JwsAlgorithm {
125        self.config.sign_alg
126    }
127
128    /// Send an HTTP request to the ACME server and return the result in the form of a
129    /// pair (nonce, deserialized JSON response). The nonce is returned so it can be
130    /// used by the caller to construct the body of the next ACME request.
131    async fn acme_request(&self, url: &Url, body: &AcmeJws) -> Result<(String, serde_json::Value)> {
132        let headers = vec![HttpHeader {
133            name: "content-type".into(),
134            value: "application/jose+json".into(),
135        }];
136        let body = serde_json::to_string(&body)?.into();
137        let response = self
138            .pki_env
139            .hooks()
140            .http_request(HttpMethod::Post, url.to_string(), headers, body)
141            .await?;
142
143        let nonce = get_header(&response, "replay-nonce")?;
144        Ok((nonce, response.json()?))
145    }
146}