1#![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 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 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
177fn 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 Ok(secs) => SystemTime::now() + Duration::from_secs(secs),
194 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
241pub trait HttpClient: Send + Sync + 'static {
243 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
261pub struct BytesResponse {
263 pub parts: http::response::Parts,
265 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#[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#[async_trait]
365pub trait BytesBody: Send {
366 #[allow(clippy::wrong_self_convention)] 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}