Skip to main content

wire_e2e_identity/acquisition/
mod.rs

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