1#![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 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 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
176fn 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 Ok(secs) => SystemTime::now() + Duration::from_secs(secs),
193 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
240pub trait HttpClient: Send + Sync + 'static {
242 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
260pub struct BytesResponse {
262 pub parts: http::response::Parts,
264 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#[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#[async_trait]
364pub trait BytesBody: Send {
365 #[allow(clippy::wrong_self_convention)] 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}