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