Skip to main content

wire_e2e_identity/acquisition/
mod.rs

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