acme_lib/order/
auth.rs

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