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