wire_e2e_identity/acquisition/
dpop_challenge.rs

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