acme_lite/order/auth.rs
1use std::{sync::Arc, time::Duration};
2
3use base64::prelude::*;
4use sha2::{Digest as _, Sha256};
5
6use crate::{
7 acc::{AccountInner, AcmeKey},
8 api::{ApiAuth, ApiChallenge, ApiEmptyObject, ApiEmptyString},
9 jws::{Jwk, JwkThumb},
10};
11
12/// An authorization ([ownership proof]) for a domain name.
13///
14/// Each authorization for an order much be progressed to a valid state before the ACME API
15/// will issue a certificate.
16///
17/// Authorizations may or may not be required depending on previous orders against the same
18/// ACME account. The ACME API decides if the authorization is needed.
19///
20/// Currently there are two ways of providing the authorization.
21///
22/// * In a text file served using [HTTP] from a web server of the domain being authorized.
23/// * A `TXT` [DNS] record under the domain being authorized.
24///
25/// [ownership proof]: ../index.html#domain-ownership
26/// [HTTP]: #method.http_challenge
27/// [DNS]: #method.dns_challenge
28#[derive(Debug)]
29pub struct Auth {
30 inner: Arc<AccountInner>,
31 api_auth: ApiAuth,
32 auth_url: String,
33}
34
35impl Auth {
36 pub(crate) fn new(inner: &Arc<AccountInner>, api_auth: ApiAuth, auth_url: &str) -> Self {
37 Auth {
38 inner: inner.clone(),
39 api_auth,
40 auth_url: auth_url.to_owned(),
41 }
42 }
43
44 /// Domain name for this authorization.
45 pub fn domain_name(&self) -> &str {
46 &self.api_auth.identifier.value
47 }
48
49 /// Whether we actually need to do the authorization. This might not be needed if we have
50 /// proven ownership of the domain recently in a previous order.
51 pub fn need_challenge(&self) -> bool {
52 !self.api_auth.is_status_valid()
53 }
54
55 /// Get the http challenge.
56 ///
57 /// The http challenge must be placed so it is accessible under:
58 ///
59 /// ```text
60 /// http://<domain-to-be-proven>/.well-known/acme-challenge/<token>
61 /// ```
62 ///
63 /// The challenge will be accessed over HTTP (not HTTPS), for obvious reasons.
64 ///
65 /// ```no_run
66 /// use std::fs::File;
67 /// use std::io::Write;
68 /// use std::time::Duration;
69 ///
70 /// use acme_lite::order::Auth;
71 ///
72 /// async fn web_authorize(auth: &Auth) -> eyre::Result<()> {
73 /// let challenge = auth.http_challenge().unwrap();
74 ///
75 /// // Assuming our web server's root is under /var/www
76 /// let path = {
77 /// let token = challenge.http_token();
78 /// format!("/var/www/.well-known/acme-challenge/{}", token)
79 /// };
80 ///
81 /// let mut file = File::create(&path)?;
82 /// file.write_all(challenge.http_proof()?.as_bytes())?;
83 /// challenge.validate(Duration::from_millis(5000)).await?;
84 ///
85 /// Ok(())
86 /// }
87 /// ```
88 pub fn http_challenge(&self) -> Option<Challenge<Http>> {
89 self.api_auth
90 .http_challenge()
91 .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
92 }
93
94 /// Get the dns challenge.
95 ///
96 /// The dns challenge is a `TXT` record that must put created under:
97 ///
98 /// ```text
99 /// _acme-challenge.<domain-to-be-proven>. TXT <proof>
100 /// ```
101 ///
102 /// The <proof> contains the signed token proving this account update it.
103 ///
104 /// ```no_run
105 /// use std::time::Duration;
106 ///
107 /// use acme_lite::order::Auth;
108 ///
109 /// async fn dns_authorize(auth: &Auth) -> eyre::Result<()> {
110 /// let challenge = auth.dns_challenge().unwrap();
111 /// let record = format!("_acme-challenge.{}.", auth.domain_name());
112 /// // route_53_set_record(&record, "TXT", challenge.dns_proof());
113 /// challenge.validate(Duration::from_millis(5000)).await?;
114 /// Ok(())
115 /// }
116 /// ```
117 ///
118 /// The dns proof is not the same as the http proof.
119 pub fn dns_challenge(&self) -> Option<Challenge<Dns>> {
120 self.api_auth
121 .dns_challenge()
122 .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
123 }
124
125 /// Returns the TLS ALPN challenge.
126 ///
127 /// The TLS ALPN challenge is a certificate that must be served when a request is made for the
128 /// ALPN protocol "tls-alpn-01". The certificate must contain a single DNSName SAN containing
129 /// the domain being validated, as well as an ACME extension containing the SHA256 of the key
130 /// authorization.
131 pub fn tls_alpn_challenge(&self) -> Option<Challenge<TlsAlpn>> {
132 self.api_auth
133 .tls_alpn_challenge()
134 .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
135 }
136
137 /// Access the underlying JSON object for debugging. We don't
138 /// refresh the authorization when the corresponding challenge is validated,
139 /// so there will be no changes to see here.
140 pub fn api_auth(&self) -> &ApiAuth {
141 &self.api_auth
142 }
143}
144
145/// Marker type for http challenges.
146#[doc(hidden)]
147pub struct Http;
148
149/// Marker type for dns challenges.
150#[doc(hidden)]
151pub struct Dns;
152
153/// Marker type for tls alpn challenges.
154#[doc(hidden)]
155pub struct TlsAlpn;
156
157/// A DNS, HTTP, or TLS-ALPN challenge as obtained from the [`Auth`].
158///
159/// [`Auth`]: struct.Auth.html
160pub struct Challenge<A> {
161 inner: Arc<AccountInner>,
162 api_challenge: ApiChallenge,
163 auth_url: String,
164 _ph: std::marker::PhantomData<A>,
165}
166
167impl Challenge<Http> {
168 /// The `token` is a unique identifier of the challenge. It is the file name in the
169 /// http challenge like so:
170 ///
171 /// ```text
172 /// http://<domain-to-be-proven>/.well-known/acme-challenge/<token>
173 /// ```
174 pub fn http_token(&self) -> &str {
175 &self.api_challenge.token
176 }
177
178 /// The `proof` is some text content that is placed in the file named by `token`.
179 pub fn http_proof(&self) -> eyre::Result<String> {
180 let acme_key = self.inner.transport.acme_key();
181 let proof = key_authorization(&self.api_challenge.token, acme_key, false)?;
182 Ok(proof)
183 }
184}
185
186impl Challenge<Dns> {
187 /// The `proof` is the `TXT` record placed under:
188 ///
189 /// ```text
190 /// _acme-challenge.<domain-to-be-proven>. TXT <proof>
191 /// ```
192 pub fn dns_proof(&self) -> eyre::Result<String> {
193 let acme_key = self.inner.transport.acme_key();
194 let proof = key_authorization(&self.api_challenge.token, acme_key, true)?;
195 Ok(proof)
196 }
197}
198
199impl Challenge<TlsAlpn> {
200 /// The `proof` is the contents of the ACME extension to be placed in the
201 /// certificate used for validation.
202 pub fn tls_alpn_proof(&self) -> eyre::Result<[u8; 32]> {
203 let acme_key = self.inner.transport.acme_key();
204 let proof = key_authorization(&self.api_challenge.token, acme_key, false)?;
205 Ok(Sha256::digest(proof).try_into().unwrap())
206 }
207}
208
209impl<A> Challenge<A> {
210 fn new(inner: &Arc<AccountInner>, api_challenge: ApiChallenge, auth_url: &str) -> Self {
211 Challenge {
212 inner: inner.clone(),
213 api_challenge,
214 auth_url: auth_url.to_owned(),
215 _ph: std::marker::PhantomData,
216 }
217 }
218
219 /// Check whether this challenge really need validation. It might already been
220 /// done in a previous order for the same account.
221 pub fn need_validate(&self) -> bool {
222 self.api_challenge.is_status_pending()
223 }
224
225 /// Tell the ACME API to attempt validating the proof of this challenge.
226 ///
227 /// The user must first update the DNS record or HTTP web server depending
228 /// on the type challenge being validated.
229 pub async fn validate(&self, delay: Duration) -> eyre::Result<()> {
230 let res = self
231 .inner
232 .transport
233 .call(&self.api_challenge.url, &ApiEmptyObject)
234 .await?;
235
236 let _api_challenge = res.json::<ApiChallenge>().await?;
237
238 let auth = wait_for_auth_status(&self.inner, &self.auth_url, delay).await?;
239
240 if !auth.is_status_valid() {
241 let error = auth
242 .challenges
243 .iter()
244 .filter_map(|c| c.error.as_ref())
245 .next();
246
247 let reason = match error {
248 Some(error) => error.detail.clone().unwrap_or_else(|| error._type.clone()),
249 None => "Validation failed and no error found".to_owned(),
250 };
251
252 return Err(eyre::eyre!("Validation failed: {reason}"));
253 }
254
255 Ok(())
256 }
257
258 /// Access the underlying JSON object for debugging.
259 pub fn api_challenge(&self) -> &ApiChallenge {
260 &self.api_challenge
261 }
262}
263
264fn key_authorization(token: &str, key: &AcmeKey, extra_sha256: bool) -> eyre::Result<String> {
265 let jwk = Jwk::try_from(key)?;
266 let jwk_thumb = JwkThumb::from(&jwk);
267 let jwk_json = serde_json::to_string(&jwk_thumb)?;
268
269 let digest = BASE64_URL_SAFE_NO_PAD.encode(Sha256::digest(jwk_json));
270 let key_auth = format!("{token}.{digest}");
271
272 let res = if extra_sha256 {
273 BASE64_URL_SAFE_NO_PAD.encode(Sha256::digest(key_auth))
274 } else {
275 key_auth
276 };
277
278 Ok(res)
279}
280
281async fn wait_for_auth_status(
282 inner: &Arc<AccountInner>,
283 auth_url: &str,
284 delay: Duration,
285) -> eyre::Result<ApiAuth> {
286 let auth = loop {
287 let res = inner.transport.call(auth_url, &ApiEmptyString).await?;
288 let auth = res.json::<ApiAuth>().await?;
289
290 if !auth.is_status_pending() {
291 break auth;
292 }
293
294 tokio::time::sleep(delay).await;
295 };
296
297 Ok(auth)
298}
299
300#[cfg(test)]
301mod tests {
302 use crate::*;
303
304 #[tokio::test]
305 async fn test_get_challenges() {
306 let server = crate::test::with_directory_server();
307 let url = DirectoryUrl::Other(&server.dir_url);
308 let dir = Directory::from_url(url).await.unwrap();
309 let acc = dir
310 .register_account(Some(vec!["mailto:foo@bar.com".to_owned()]))
311 .await
312 .unwrap();
313 let ord = acc.new_order("acmetest.example.com", &[]).await.unwrap();
314 let authz = ord.authorizations().await.unwrap();
315 assert!(authz.len() == 1);
316 let auth = &authz[0];
317
318 let http = auth.http_challenge().unwrap();
319 assert!(http.need_validate());
320
321 let dns = auth.dns_challenge().unwrap();
322 assert!(dns.need_validate());
323 }
324}