instant_acme/
lib.rs

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