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