acme_micro/order/auth.rs
1//
2use openssl::sha::sha256;
3use std::sync::Arc;
4use std::thread;
5use std::{convert::TryInto, time::Duration};
6
7use crate::acc::AccountInner;
8use crate::acc::AcmeKey;
9use crate::api::{ApiAuth, ApiChallenge, ApiEmptyObject, ApiEmptyString};
10use crate::error::*;
11use crate::jwt::*;
12use crate::util::{base64url, read_json};
13
14/// An authorization ([ownership proof]) for a domain name.
15///
16/// Each authorization for an order much be progressed to a valid state before the ACME API
17/// will issue a certificate.
18///
19/// Authorizations may or may not be required depending on previous orders against the same
20/// ACME account. The ACME API decides if the authorization is needed.
21///
22/// Currently there are two ways of providing the authorization.
23///
24/// * In a text file served using [HTTP] from a web server of the domain being authorized.
25/// * A `TXT` [DNS] record under the domain being authorized.
26///
27/// [ownership proof]: ../index.html#domain-ownership
28/// [HTTP]: #method.http_challenge
29/// [DNS]: #method.dns_challenge
30#[derive(Debug)]
31pub struct Auth {
32 inner: Arc<AccountInner>,
33 api_auth: ApiAuth,
34 auth_url: String,
35}
36
37impl Auth {
38 pub(crate) fn new(inner: &Arc<AccountInner>, api_auth: ApiAuth, auth_url: &str) -> Self {
39 Auth {
40 inner: inner.clone(),
41 api_auth,
42 auth_url: auth_url.into(),
43 }
44 }
45
46 /// Domain name for this authorization.
47 pub fn domain_name(&self) -> &str {
48 &self.api_auth.identifier.value
49 }
50
51 /// Whether we actually need to do the authorization. This might not be needed if we have
52 /// proven ownership of the domain recently in a previous order.
53 pub fn need_challenge(&self) -> bool {
54 !self.api_auth.is_status_valid()
55 }
56
57 /// Get the http challenge.
58 ///
59 /// The http challenge must be placed so it is accessible under:
60 ///
61 /// ```text
62 /// http://<domain-to-be-proven>/.well-known/acme-challenge/<token>
63 /// ```
64 ///
65 /// The challenge will be accessed over HTTP (not HTTPS), for obvious reasons.
66 ///
67 /// ```no_run
68 /// use acme_micro::order::Auth;
69 /// use acme_micro::Error;
70 /// use std::fs::File;
71 /// use std::io::Write;
72 /// use std::time::Duration;
73 ///
74 /// fn web_authorize(auth: &Auth) -> Result<(), Error> {
75 /// let challenge = auth.http_challenge().unwrap();
76 /// // Assuming our web server's root is under /var/www
77 /// let path = {
78 /// let token = challenge.http_token();
79 /// format!("/var/www/.well-known/acme-challenge/{}", token)
80 /// };
81 /// let mut file = File::create(&path)?;
82 /// file.write_all(challenge.http_proof()?.as_bytes())?;
83 /// challenge.validate(Duration::from_millis(5000))?;
84 /// Ok(())
85 /// }
86 /// ```
87 pub fn http_challenge(&self) -> Option<Challenge<Http>> {
88 self.api_auth
89 .http_challenge()
90 .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
91 }
92
93 /// Get the dns challenge.
94 ///
95 /// The dns challenge is a `TXT` record that must put created under:
96 ///
97 /// ```text
98 /// _acme-challenge.<domain-to-be-proven>. TXT <proof>
99 /// ```
100 ///
101 /// The <proof> contains the signed token proving this account update it.
102 ///
103 /// ```no_run
104 /// use acme_micro::order::Auth;
105 /// use acme_micro::Error;
106 /// use std::time::Duration;
107 ///
108 /// fn dns_authorize(auth: &Auth) -> Result<(), Error> {
109 /// let challenge = auth.dns_challenge().unwrap();
110 /// let record = format!("_acme-challenge.{}.", auth.domain_name());
111 /// // route_53_set_record(&record, "TXT", challenge.dns_proof());
112 /// challenge.validate(Duration::from_millis(5000))?;
113 /// Ok(())
114 /// }
115 /// ```
116 ///
117 /// The dns proof is not the same as the http proof.
118 pub fn dns_challenge(&self) -> Option<Challenge<Dns>> {
119 self.api_auth
120 .dns_challenge()
121 .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
122 }
123
124 /// Get the TLS ALPN challenge.
125 ///
126 /// The TLS ALPN challenge is a certificate that must be served when a
127 /// request is made for the ALPN protocol "tls-alpn-01". The certificate
128 /// must contain a single dNSName SAN containing the domain being
129 /// validated, as well as an ACME extension containing the SHA256 of the
130 /// key 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) -> 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) -> 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) -> 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(proof.as_bytes()))
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.into(),
215 _ph: std::marker::PhantomData,
216 }
217 }
218
219 /// Check whether this challlenge 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 fn validate(&self, delay: Duration) -> Result<()> {
230 let url_chall = &self.api_challenge.url;
231 let res = self.inner.transport.call(url_chall, &ApiEmptyObject)?;
232 let _: ApiChallenge = read_json(res)?;
233
234 let auth = wait_for_auth_status(&self.inner, &self.auth_url, delay)?;
235
236 if !auth.is_status_valid() {
237 let error = auth
238 .challenges
239 .iter()
240 .filter_map(|c| c.error.as_ref())
241 .next();
242 let reason = if let Some(error) = error {
243 format!(
244 "Failed: {}",
245 error.detail.clone().unwrap_or_else(|| error._type.clone())
246 )
247 } else {
248 "Validation failed and no error found".into()
249 };
250 bail!("Validation failed: {:?}", reason);
251 }
252
253 Ok(())
254 }
255
256 /// Access the underlying JSON object for debugging.
257 pub fn api_challenge(&self) -> &ApiChallenge {
258 &self.api_challenge
259 }
260}
261
262fn key_authorization(token: &str, key: &AcmeKey, extra_sha256: bool) -> Result<String> {
263 let jwk: Jwk = key.try_into()?;
264 let jwk_thumb: JwkThumb = (&jwk).into();
265 let jwk_json = serde_json::to_string(&jwk_thumb)?;
266 let digest = base64url(&sha256(jwk_json.as_bytes()));
267 let key_auth = format!("{}.{}", token, digest);
268 let res = if extra_sha256 {
269 base64url(&sha256(key_auth.as_bytes()))
270 } else {
271 key_auth
272 };
273 Ok(res)
274}
275
276fn wait_for_auth_status(
277 inner: &Arc<AccountInner>,
278 auth_url: &str,
279 delay: Duration,
280) -> Result<ApiAuth> {
281 let auth = loop {
282 let res = inner.transport.call(auth_url, &ApiEmptyString)?;
283 let auth: ApiAuth = read_json(res)?;
284 if !auth.is_status_pending() {
285 break auth;
286 }
287 thread::sleep(delay);
288 };
289 Ok(auth)
290}
291
292#[cfg(test)]
293mod test {
294 use crate::*;
295
296 #[test]
297 fn test_get_challenges() -> Result<()> {
298 let server = crate::test::with_directory_server();
299 let url = DirectoryUrl::Other(&server.dir_url);
300 let dir = Directory::from_url(url)?;
301 let acc = dir.register_account(vec!["mailto:foo@bar.com".to_string()])?;
302 let ord = acc.new_order("acmetest.example.com", &[])?;
303 let authz = ord.authorizations()?;
304 assert!(authz.len() == 1);
305 let auth = &authz[0];
306 {
307 let http = auth.http_challenge().unwrap();
308 assert!(http.need_validate());
309 }
310 {
311 let dns = auth.dns_challenge().unwrap();
312 assert!(dns.need_validate());
313 }
314 Ok(())
315 }
316}