instant_acme/
lib.rs

1//! Async pure-Rust ACME (RFC 8555) client.
2
3#![warn(unreachable_pub)]
4#![warn(missing_docs)]
5#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
6
7use std::convert::Infallible;
8use std::error::Error as StdError;
9use std::fmt;
10use std::future::Future;
11use std::pin::Pin;
12use std::str::FromStr;
13use std::task::{Context, Poll};
14use std::time::{Duration, SystemTime};
15
16use async_trait::async_trait;
17use bytes::{Buf, Bytes};
18use http::header::{CONTENT_TYPE, RETRY_AFTER, USER_AGENT};
19use http::{Method, Request, Response, StatusCode};
20use http_body::{Frame, SizeHint};
21use http_body_util::BodyExt;
22use httpdate::HttpDate;
23#[cfg(feature = "hyper-rustls")]
24use hyper::body::Incoming;
25#[cfg(feature = "hyper-rustls")]
26use hyper_rustls::HttpsConnectorBuilder;
27#[cfg(feature = "hyper-rustls")]
28use hyper_rustls::builderstates::WantsSchemes;
29#[cfg(feature = "hyper-rustls")]
30use hyper_util::client::legacy::Client as HyperClient;
31#[cfg(feature = "hyper-rustls")]
32use hyper_util::client::legacy::connect::{Connect, HttpConnector};
33#[cfg(feature = "hyper-rustls")]
34use hyper_util::rt::TokioExecutor;
35use serde::Serialize;
36
37mod account;
38pub use account::Key;
39pub use account::{Account, AccountBuilder, ExternalAccountKey};
40mod order;
41pub use order::{
42    AuthorizationHandle, Authorizations, ChallengeHandle, Identifiers, KeyAuthorization, Order,
43    RetryPolicy,
44};
45mod types;
46#[cfg(feature = "time")]
47pub use types::RenewalInfo;
48pub use types::{
49    AccountCredentials, Authorization, AuthorizationState, AuthorizationStatus,
50    AuthorizedIdentifier, CertificateIdentifier, Challenge, ChallengeType, Error, Identifier,
51    LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem, ProfileMeta,
52    RevocationReason, RevocationRequest, ZeroSsl,
53};
54use types::{Directory, JoseJson, Signer};
55
56struct Client {
57    http: Box<dyn HttpClient>,
58    directory: Directory,
59    directory_url: Option<String>,
60}
61
62impl Client {
63    async fn new(directory_url: String, http: Box<dyn HttpClient>) -> Result<Self, Error> {
64        let request = Request::builder()
65            .uri(&directory_url)
66            .header(USER_AGENT, CRATE_USER_AGENT)
67            .body(BodyWrapper::default())
68            .expect("infallible error should not occur");
69        let rsp = http.request(request).await?;
70        let body = rsp.body().await.map_err(Error::Other)?;
71        Ok(Client {
72            http,
73            directory: serde_json::from_slice(&body)?,
74            directory_url: Some(directory_url),
75        })
76    }
77
78    async fn post(
79        &self,
80        payload: Option<&impl Serialize>,
81        mut nonce: Option<String>,
82        signer: &impl Signer,
83        url: &str,
84    ) -> Result<BytesResponse, Error> {
85        let mut retries = 3;
86        loop {
87            let mut response = self
88                .post_attempt(payload, nonce.clone(), signer, url)
89                .await?;
90            if response.parts.status != StatusCode::BAD_REQUEST {
91                return Ok(response);
92            }
93            let body = response.body.into_bytes().await.map_err(Error::Other)?;
94            let problem = serde_json::from_slice::<Problem>(&body)?;
95            if let Some("urn:ietf:params:acme:error:badNonce") = problem.r#type.as_deref() {
96                retries -= 1;
97                if retries != 0 {
98                    // Retrieve the new nonce. If it isn't there (it
99                    // should be, the spec requires it) then we will
100                    // manually refresh a new one in `post_attempt`
101                    // due to `nonce` being `None` but getting it from
102                    // the response saves us making that request.
103                    nonce = nonce_from_response(&response);
104                    continue;
105                }
106            }
107
108            return Ok(BytesResponse {
109                parts: response.parts,
110                body: Box::new(body),
111            });
112        }
113    }
114
115    async fn post_attempt(
116        &self,
117        payload: Option<&impl Serialize>,
118        nonce: Option<String>,
119        signer: &impl Signer,
120        url: &str,
121    ) -> Result<BytesResponse, Error> {
122        let nonce = self.nonce(nonce).await?;
123        let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
124        let request = Request::builder()
125            .method(Method::POST)
126            .uri(url)
127            .header(USER_AGENT, CRATE_USER_AGENT)
128            .header(CONTENT_TYPE, JOSE_JSON)
129            .body(BodyWrapper::from(serde_json::to_vec(&body)?))?;
130
131        self.http.request(request).await
132    }
133
134    async fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
135        if let Some(nonce) = nonce {
136            return Ok(nonce);
137        }
138
139        let request = Request::builder()
140            .method(Method::HEAD)
141            .uri(&self.directory.new_nonce)
142            .header(USER_AGENT, CRATE_USER_AGENT)
143            .body(BodyWrapper::default())
144            .expect("infallible error should not occur");
145
146        let rsp = self.http.request(request).await?;
147        // https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
148        // "The server's response MUST include a Replay-Nonce header field containing a fresh
149        // nonce and SHOULD have status code 200 (OK)."
150        if rsp.parts.status != StatusCode::OK {
151            return Err("error response from newNonce resource".into());
152        }
153
154        match nonce_from_response(&rsp) {
155            Some(nonce) => Ok(nonce),
156            None => Err("no nonce found in newNonce response".into()),
157        }
158    }
159}
160
161impl fmt::Debug for Client {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("Client")
164            .field("client", &"..")
165            .field("directory", &self.directory)
166            .finish()
167    }
168}
169
170fn nonce_from_response(rsp: &BytesResponse) -> Option<String> {
171    rsp.parts
172        .headers
173        .get(REPLAY_NONCE)
174        .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
175}
176
177/// Parse the `Retry-After` header from the response
178///
179/// <https://httpwg.org/specs/rfc9110.html#field.retry-after>
180///
181/// # Syntax
182///
183/// Retry-After = HTTP-date / delay-seconds
184/// delay-seconds  = 1*DIGIT
185fn retry_after(rsp: &BytesResponse) -> Option<SystemTime> {
186    let value = rsp.parts.headers.get(RETRY_AFTER)?.to_str().ok()?.trim();
187    if value.is_empty() {
188        return None;
189    }
190
191    Some(match u64::from_str(value) {
192        // `delay-seconds` is a number of seconds to wait
193        Ok(secs) => SystemTime::now() + Duration::from_secs(secs),
194        // `HTTP-date` looks like `Fri, 31 Dec 1999 23:59:59 GMT`
195        Err(_) => SystemTime::from(HttpDate::from_str(value).ok()?),
196    })
197}
198
199#[cfg(feature = "hyper-rustls")]
200struct DefaultClient(HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, BodyWrapper<Bytes>>);
201
202#[cfg(feature = "hyper-rustls")]
203impl DefaultClient {
204    fn try_new() -> Result<Self, Error> {
205        Ok(Self::new(
206            hyper_rustls::HttpsConnectorBuilder::new()
207                .try_with_platform_verifier()
208                .map_err(|e| Error::Other(Box::new(e)))?,
209        ))
210    }
211
212    fn with_roots(roots: rustls::RootCertStore) -> Result<Self, Error> {
213        Ok(Self::new(
214            hyper_rustls::HttpsConnectorBuilder::new().with_tls_config(
215                rustls::ClientConfig::builder()
216                    .with_root_certificates(roots)
217                    .with_no_client_auth(),
218            ),
219        ))
220    }
221
222    fn new(builder: HttpsConnectorBuilder<WantsSchemes>) -> Self {
223        Self(
224            HyperClient::builder(TokioExecutor::new())
225                .build(builder.https_only().enable_http1().enable_http2().build()),
226        )
227    }
228}
229
230#[cfg(feature = "hyper-rustls")]
231impl HttpClient for DefaultClient {
232    fn request(
233        &self,
234        req: Request<BodyWrapper<Bytes>>,
235    ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
236        let fut = self.0.request(req);
237        Box::pin(async move { BytesResponse::try_from(fut.await) })
238    }
239}
240
241/// A HTTP client abstraction
242pub trait HttpClient: Send + Sync + 'static {
243    /// Send the given request and return the response
244    fn request(
245        &self,
246        req: Request<BodyWrapper<Bytes>>,
247    ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>>;
248}
249
250#[cfg(feature = "hyper-rustls")]
251impl<C: Connect + Clone + Send + Sync + 'static> HttpClient for HyperClient<C, BodyWrapper<Bytes>> {
252    fn request(
253        &self,
254        req: Request<BodyWrapper<Bytes>>,
255    ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
256        let fut = self.request(req);
257        Box::pin(async move { BytesResponse::try_from(fut.await) })
258    }
259}
260
261/// Response with object safe body type
262pub struct BytesResponse {
263    /// Response status and header
264    pub parts: http::response::Parts,
265    /// Response body
266    pub body: Box<dyn BytesBody>,
267}
268
269impl BytesResponse {
270    #[cfg(feature = "hyper-rustls")]
271    fn try_from(
272        result: Result<Response<Incoming>, hyper_util::client::legacy::Error>,
273    ) -> Result<Self, Error> {
274        match result {
275            Ok(rsp) => Ok(BytesResponse::from(rsp)),
276            Err(e) => Err(Error::Other(Box::new(e))),
277        }
278    }
279
280    pub(crate) async fn body(mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
281        self.body.into_bytes().await
282    }
283}
284
285impl<B> From<Response<B>> for BytesResponse
286where
287    B: http_body::Body + Send + Unpin + 'static,
288    B::Data: Send,
289    B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
290{
291    fn from(rsp: Response<B>) -> Self {
292        let (parts, body) = rsp.into_parts();
293        Self {
294            parts,
295            body: Box::new(BodyWrapper { inner: Some(body) }),
296        }
297    }
298}
299
300/// A simple HTTP body wrapper type
301#[derive(Default)]
302pub struct BodyWrapper<B> {
303    inner: Option<B>,
304}
305
306#[async_trait]
307impl<B> BytesBody for BodyWrapper<B>
308where
309    B: http_body::Body + Send + Unpin + 'static,
310    B::Data: Send,
311    B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
312{
313    async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
314        let Some(body) = self.inner.take() else {
315            return Ok(Bytes::new());
316        };
317
318        match body.collect().await {
319            Ok(body) => Ok(body.to_bytes()),
320            Err(e) => Err(e.into()),
321        }
322    }
323}
324
325impl http_body::Body for BodyWrapper<Bytes> {
326    type Data = Bytes;
327    type Error = Infallible;
328
329    fn poll_frame(
330        mut self: Pin<&mut Self>,
331        _cx: &mut Context<'_>,
332    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
333        Poll::Ready(self.inner.take().map(|d| Ok(Frame::data(d))))
334    }
335
336    fn is_end_stream(&self) -> bool {
337        self.inner.is_none()
338    }
339
340    fn size_hint(&self) -> SizeHint {
341        match self.inner.as_ref() {
342            Some(data) => SizeHint::with_exact(u64::try_from(data.remaining()).unwrap()),
343            None => SizeHint::with_exact(0),
344        }
345    }
346}
347
348impl From<Vec<u8>> for BodyWrapper<Bytes> {
349    fn from(data: Vec<u8>) -> Self {
350        BodyWrapper {
351            inner: Some(Bytes::from(data)),
352        }
353    }
354}
355
356#[async_trait]
357impl BytesBody for Bytes {
358    async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
359        Ok(self.to_owned())
360    }
361}
362
363/// Object safe body trait
364#[async_trait]
365pub trait BytesBody: Send {
366    /// Convert the body into [`Bytes`]
367    ///
368    /// This consumes the body. The behavior for calling this method multiple times is undefined.
369    #[allow(clippy::wrong_self_convention)] // async_trait doesn't support taking `self`
370    async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>>;
371}
372
373mod crypto {
374    #[cfg(feature = "aws-lc-rs")]
375    pub(crate) use aws_lc_rs as ring_like;
376    #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
377    pub(crate) use ring as ring_like;
378
379    pub(crate) use ring_like::digest::{Digest, SHA256, digest};
380    pub(crate) use ring_like::hmac;
381    pub(crate) use ring_like::rand::SystemRandom;
382    pub(crate) use ring_like::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair};
383    pub(crate) use ring_like::signature::{KeyPair, Signature};
384
385    use super::Error;
386
387    #[cfg(feature = "aws-lc-rs")]
388    pub(crate) fn p256_key_pair_from_pkcs8(
389        pkcs8: &[u8],
390        _: &SystemRandom,
391    ) -> Result<EcdsaKeyPair, Error> {
392        EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8)
393            .map_err(|_| Error::KeyRejected)
394    }
395
396    #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
397    pub(crate) fn p256_key_pair_from_pkcs8(
398        pkcs8: &[u8],
399        rng: &SystemRandom,
400    ) -> Result<EcdsaKeyPair, Error> {
401        EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8, rng)
402            .map_err(|_| Error::KeyRejected)
403    }
404}
405
406const CRATE_USER_AGENT: &str = concat!("instant-acme/", env!("CARGO_PKG_VERSION"));
407const JOSE_JSON: &str = "application/jose+json";
408const REPLAY_NONCE: &str = "Replay-Nonce";
409
410#[cfg(all(test, feature = "hyper-rustls"))]
411mod tests {
412    use super::*;
413
414    #[tokio::test]
415    async fn deserialize_old_credentials() -> Result<(), Error> {
416        const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","urls":{"newNonce":"new-nonce","newAccount":"new-acct","newOrder":"new-order", "revokeCert": "revoke-cert"}}"#;
417        Account::builder()?
418            .from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
419            .await?;
420        Ok(())
421    }
422
423    #[tokio::test]
424    async fn deserialize_new_credentials() -> Result<(), Error> {
425        const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
426        Account::builder()?
427            .from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
428            .await?;
429        Ok(())
430    }
431}