Skip to main content

wire_e2e_identity/acquisition/
dpop_challenge.rs

1use obfuscate::Obfuscated;
2use rusty_jwt_tools::prelude::{Dpop, Handle, Htm, RustyJwtTools};
3use url::Url;
4
5use super::{Result, X509CredentialAcquisition, get_header, states};
6use crate::{
7    acme::{AcmeAccount, AcmeChallenge, AcmeChallengeType, AcmeOrder, RustyAcme, RustyAcmeError},
8    pki_env::hooks::HttpMethod,
9};
10
11impl X509CredentialAcquisition<states::Initialized> {
12    async fn get_challenge(
13        &self,
14        url: &url::Url,
15        acme_account: &AcmeAccount,
16        nonce: String,
17    ) -> Result<(String, AcmeChallenge)> {
18        let authz_request =
19            RustyAcme::new_authz_request(url, acme_account, self.config.sign_alg, &self.acme_kp, nonce.clone())?;
20        let (nonce, response) = self.acme_request(url, &authz_request).await?;
21        let authorization = RustyAcme::new_authz_response(response)?;
22        let [challenge] = authorization.challenges;
23        log::debug!(
24            "acquisition({:?}): got ACME challenge {:?}",
25            Obfuscated::from(&self.sign_kp),
26            challenge.typ
27        );
28        Ok((nonce, challenge))
29    }
30
31    async fn get_challenges(
32        &self,
33        acme_account: &AcmeAccount,
34        order: &AcmeOrder,
35        nonce: String,
36    ) -> Result<(String, AcmeChallenge, AcmeChallenge)> {
37        // ACME authorization objects specify challenges we must do in order to get a
38        // certificate. We expect exactly two authorization objects, one for the "wireapp-user"
39        // identifier and one for the "wireapp-device" identifier. Each authorization must
40        // specify exactly one challenge.
41        //
42        // See [RFC 8555 Section 7.5](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5).
43        let (nonce, challenge1) = self
44            .get_challenge(&order.authorizations[0], acme_account, nonce)
45            .await?;
46        let (nonce, challenge2) = self
47            .get_challenge(&order.authorizations[1], acme_account, nonce)
48            .await?;
49
50        // To make things easier for our caller, we return challenges in the fixed order
51        // (wire-dpop-01, wire-oidc-01). We cannot rely on ACME giving us challenges in a specific
52        // order.
53        use AcmeChallengeType::*;
54        match (challenge1.typ, challenge2.typ) {
55            (WireDpop01, WireOidc01) => Ok((nonce, challenge1, challenge2)),
56            (WireOidc01, WireDpop01) => Ok((nonce, challenge2, challenge1)),
57            _ => Err(RustyAcmeError::from(crate::acme::AcmeAuthzError::InvalidChallengeType).into()),
58        }
59    }
60
61    /// Complete the DPoP challenge.
62    pub async fn complete_dpop_challenge(self) -> Result<X509CredentialAcquisition<states::DpopChallengeCompleted>> {
63        let hooks = self.pki_env.hooks();
64
65        // Get the ACME server directory via `GET /acme/{provisioner-name}/directory`.
66        //
67        // See [RFC 8555 Section 7.1.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1)
68        let url: Url = self
69            .config
70            .acme_directory_url
71            .parse()
72            .expect("valid ACME directory URL");
73
74        let resp = hooks
75            .http_request(HttpMethod::Get, url.to_string(), vec![], vec![])
76            .await?;
77        log::debug!(
78            "acquisition({:?}): got ACME server directory: {:?}",
79            Obfuscated::from(&self.sign_kp),
80            str::from_utf8(&resp.body),
81        );
82        let body = resp.json()?;
83        let directory = RustyAcme::acme_directory_response(body)?;
84
85        let url = directory.new_nonce.to_string();
86        let resp = hooks.http_request(HttpMethod::Get, url, vec![], vec![]).await?;
87        let nonce = get_header(&resp, "replay-nonce")?;
88        log::debug!(
89            "acquisition({:?}): got the initial nonce",
90            Obfuscated::from(&self.sign_kp),
91        );
92
93        // Create a new ACME account.
94        //
95        // See [RFC 8555 Section 7.3](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3).
96        let account_request = RustyAcme::new_account_request(&directory, self.config.sign_alg, &self.acme_kp, nonce)?;
97        let (nonce, response) = self.acme_request(&directory.new_account, &account_request).await?;
98        let acme_account = RustyAcme::new_account_response(response)?;
99        log::debug!(
100            "acquisition({:?}): created a new ACME account",
101            Obfuscated::from(&self.sign_kp),
102        );
103
104        // Create a new ACME order.
105        //
106        // See [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4).
107        let order_request = RustyAcme::new_order_request(
108            &self.config.display_name,
109            self.config.client_id.clone(),
110            &self.config.handle.clone().into(),
111            self.config.validity_period,
112            &directory,
113            &acme_account,
114            self.config.sign_alg,
115            &self.acme_kp,
116            nonce,
117        )?;
118        let (nonce, response) = self.acme_request(&directory.new_order, &order_request).await?;
119        let order = RustyAcme::new_order_response(response)?;
120        log::debug!(
121            "acquisition({:?}): created a new ACME order",
122            Obfuscated::from(&self.sign_kp),
123        );
124
125        let (nonce, dpop_challenge, oidc_challenge) = self.get_challenges(&acme_account, &order, nonce).await?;
126
127        // Generate a new client DPoP JWT token. It demonstrates proof of possession of nonces from
128        // the Wire server and the ACME server), and will be verified by the ACME server when
129        // verifying the challenge.
130        let backend_nonce = hooks.get_backend_nonce().await?;
131        log::debug!(
132            "acquisition({:?}): got the Wire server nonce",
133            Obfuscated::from(&self.sign_kp),
134        );
135
136        let audience = dpop_challenge.url.clone();
137        let client_id = &self.config.client_id;
138        let handle = Handle::from(self.config.handle.clone()).try_to_qualified(&client_id.domain)?;
139        let dpop = Dpop {
140            htm: Htm::Post,
141            htu: dpop_challenge.target.clone().into(),
142            challenge: dpop_challenge.token.clone().into(),
143            handle,
144            team: self.config.team.clone().into(),
145            display_name: self.config.display_name.clone(),
146            extra_claims: None,
147        };
148        let token = RustyJwtTools::generate_dpop_token(
149            dpop,
150            client_id,
151            backend_nonce.into(),
152            audience,
153            std::time::Duration::from_mins(5),
154            self.config.sign_alg,
155            &self.acme_kp,
156        )?;
157
158        // Send the DPoP token to Wire server and get back an access token.
159        let access_token = hooks.fetch_backend_access_token(token).await?;
160        log::debug!(
161            "acquisition({:?}): got the Wire server access token",
162            Obfuscated::from(&self.sign_kp),
163        );
164
165        // Complete the DPoP challenge.
166        //
167        // See [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1).
168        let dpop_challenge_request = RustyAcme::dpop_chall_request(
169            access_token,
170            dpop_challenge.clone(),
171            &acme_account,
172            self.config.sign_alg,
173            &self.acme_kp,
174            nonce,
175        )?;
176        let (nonce, response) = self.acme_request(&dpop_challenge.url, &dpop_challenge_request).await?;
177        let _ = RustyAcme::new_chall_response(response)?;
178        log::info!(
179            "acquisition({:?}): DPoP challenge completed",
180            Obfuscated::from(&self.sign_kp),
181        );
182
183        Ok(X509CredentialAcquisition::<states::DpopChallengeCompleted> {
184            pki_env: self.pki_env,
185            config: self.config,
186            sign_kp: self.sign_kp,
187            acme_kp: self.acme_kp,
188            acme_jwk: self.acme_jwk,
189            data: states::DpopChallengeCompleted {
190                nonce,
191                acme_account,
192                order,
193                oidc_challenge,
194            },
195        })
196    }
197}