wire_e2e_identity/acquisition/
dpop_challenge.rs1use 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 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 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 pub async fn complete_dpop_challenge(self) -> Result<X509CredentialAcquisition<states::DpopChallengeCompleted>> {
63 let hooks = self.pki_env.hooks();
64
65 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 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 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 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 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 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}