Skip to main content

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}