1#![warn(unreachable_pub)]
4#![warn(missing_docs)]
5
6use std::error::Error as StdError;
7use std::fmt;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
14use bytes::Bytes;
15use http::header::{CONTENT_TYPE, LOCATION};
16use http::{Method, Request, Response, StatusCode};
17use http_body_util::{BodyExt, Full};
18#[cfg(feature = "hyper-rustls")]
19use hyper_util::client::legacy::connect::Connect;
20#[cfg(feature = "hyper-rustls")]
21use hyper_util::client::legacy::Client as HyperClient;
22#[cfg(feature = "hyper-rustls")]
23use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
24use serde::de::DeserializeOwned;
25use serde::Serialize;
26
27mod types;
28pub use types::{
29 AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error,
30 Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem,
31 RevocationReason, RevocationRequest, ZeroSsl,
32};
33use types::{
34 DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload,
35 Signer, SigningAlgorithm,
36};
37
38pub struct Order {
46 account: Arc<AccountInner>,
47 nonce: Option<String>,
48 url: String,
49 state: OrderState,
50}
51
52impl Order {
53 pub async fn authorizations(&mut self) -> Result<Vec<Authorization>, Error> {
69 let mut authorizations = Vec::with_capacity(self.state.authorizations.len());
70 for url in &self.state.authorizations {
71 authorizations.push(self.account.get(&mut self.nonce, url).await?);
72 }
73 Ok(authorizations)
74 }
75
76 pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
81 KeyAuthorization::new(challenge, &self.account.key)
82 }
83
84 pub async fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> {
90 let rsp = self
91 .account
92 .post(
93 Some(&FinalizeRequest::new(csr_der)),
94 self.nonce.take(),
95 &self.state.finalize,
96 )
97 .await?;
98
99 self.nonce = nonce_from_response(&rsp);
100 self.state = Problem::check::<OrderState>(rsp).await?;
101 Ok(())
102 }
103
104 pub async fn certificate(&mut self) -> Result<Option<String>, Error> {
112 if matches!(self.state.status, OrderStatus::Processing) {
113 let rsp = self
114 .account
115 .post(None::<&Empty>, self.nonce.take(), &self.url)
116 .await?;
117 self.nonce = nonce_from_response(&rsp);
118 self.state = Problem::check::<OrderState>(rsp).await?;
119 }
120
121 if let Some(error) = &self.state.error {
122 return Err(Error::Api(error.clone()));
123 } else if self.state.status == OrderStatus::Processing {
124 return Ok(None);
125 } else if self.state.status != OrderStatus::Valid {
126 return Err(Error::Str("invalid order state"));
127 }
128
129 let cert_url = match &self.state.certificate {
130 Some(cert_url) => cert_url,
131 None => return Err(Error::Str("no certificate URL found")),
132 };
133
134 let rsp = self
135 .account
136 .post(None::<&Empty>, self.nonce.take(), cert_url)
137 .await?;
138
139 self.nonce = nonce_from_response(&rsp);
140 let body = Problem::from_response(rsp).await?;
141 Ok(Some(
142 String::from_utf8(body.to_vec())
143 .map_err(|_| "unable to decode certificate as UTF-8")?,
144 ))
145 }
146
147 pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
151 let rsp = self
152 .account
153 .post(Some(&Empty {}), self.nonce.take(), challenge_url)
154 .await?;
155
156 self.nonce = nonce_from_response(&rsp);
157 let _ = Problem::check::<Challenge>(rsp).await?;
158 Ok(())
159 }
160
161 pub async fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
163 self.account.get(&mut self.nonce, challenge_url).await
164 }
165
166 pub async fn refresh(&mut self) -> Result<&OrderState, Error> {
168 let rsp = self
169 .account
170 .post(None::<&Empty>, self.nonce.take(), &self.url)
171 .await?;
172
173 self.nonce = nonce_from_response(&rsp);
174 self.state = Problem::check::<OrderState>(rsp).await?;
175 Ok(&self.state)
176 }
177
178 pub fn into_parts(self) -> (String, OrderState) {
180 (self.url, self.state)
181 }
182
183 pub fn state(&mut self) -> &OrderState {
187 &self.state
188 }
189
190 pub fn url(&self) -> &str {
192 &self.url
193 }
194}
195
196#[derive(Clone)]
205pub struct Account {
206 inner: Arc<AccountInner>,
207}
208
209impl Account {
210 #[cfg(feature = "hyper-rustls")]
214 pub async fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
215 Ok(Self {
216 inner: Arc::new(
217 AccountInner::from_credentials(credentials, Box::new(DefaultClient::try_new()?))
218 .await?,
219 ),
220 })
221 }
222
223 pub async fn from_credentials_and_http(
227 credentials: AccountCredentials,
228 http: Box<dyn HttpClient>,
229 ) -> Result<Self, Error> {
230 Ok(Self {
231 inner: Arc::new(AccountInner::from_credentials(credentials, http).await?),
232 })
233 }
234
235 pub async fn from_parts(
240 id: String,
241 key_pkcs8_der: &[u8],
242 directory_url: &str,
243 http: Box<dyn HttpClient>,
244 ) -> Result<Self, Error> {
245 Ok(Self {
246 inner: Arc::new(AccountInner {
247 id,
248 key: Key::from_pkcs8_der(key_pkcs8_der)?,
249 client: Client::new(directory_url, http).await?,
250 }),
251 })
252 }
253
254 #[cfg(feature = "hyper-rustls")]
259 pub async fn create(
260 account: &NewAccount<'_>,
261 server_url: &str,
262 external_account: Option<&ExternalAccountKey>,
263 ) -> Result<(Account, AccountCredentials), Error> {
264 Self::create_inner(
265 account,
266 external_account,
267 Client::new(server_url, Box::new(DefaultClient::try_new()?)).await?,
268 server_url,
269 )
270 .await
271 }
272
273 pub async fn create_with_http(
278 account: &NewAccount<'_>,
279 server_url: &str,
280 external_account: Option<&ExternalAccountKey>,
281 http: Box<dyn HttpClient>,
282 ) -> Result<(Account, AccountCredentials), Error> {
283 Self::create_inner(
284 account,
285 external_account,
286 Client::new(server_url, http).await?,
287 server_url,
288 )
289 .await
290 }
291
292 async fn create_inner(
293 account: &NewAccount<'_>,
294 external_account: Option<&ExternalAccountKey>,
295 client: Client,
296 server_url: &str,
297 ) -> Result<(Account, AccountCredentials), Error> {
298 let (key, key_pkcs8) = Key::generate()?;
299 let payload = NewAccountPayload {
300 new_account: account,
301 external_account_binding: external_account
302 .map(|eak| {
303 JoseJson::new(
304 Some(&Jwk::new(&key.inner)),
305 eak.header(None, &client.urls.new_account),
306 eak,
307 )
308 })
309 .transpose()?,
310 };
311
312 let rsp = client
313 .post(Some(&payload), None, &key, &client.urls.new_account)
314 .await?;
315
316 let account_url = rsp
317 .parts
318 .headers
319 .get(LOCATION)
320 .and_then(|hv| hv.to_str().ok())
321 .map(|s| s.to_owned());
322
323 let _ = Problem::from_response(rsp).await?;
325 let id = account_url.ok_or("failed to get account URL")?;
326 let credentials = AccountCredentials {
327 id: id.clone(),
328 key_pkcs8: key_pkcs8.as_ref().to_vec(),
329 directory: Some(server_url.to_owned()),
330 urls: None,
333 };
334
335 let account = AccountInner {
336 client,
337 key,
338 id: id.clone(),
339 };
340
341 Ok((
342 Self {
343 inner: Arc::new(account),
344 },
345 credentials,
346 ))
347 }
348
349 pub async fn new_order(&self, order: &NewOrder<'_>) -> Result<Order, Error> {
353 let rsp = self
354 .inner
355 .post(Some(order), None, &self.inner.client.urls.new_order)
356 .await?;
357
358 let nonce = nonce_from_response(&rsp);
359 let order_url = rsp
360 .parts
361 .headers
362 .get(LOCATION)
363 .and_then(|hv| hv.to_str().ok())
364 .map(|s| s.to_owned());
365
366 Ok(Order {
367 account: self.inner.clone(),
368 nonce,
369 state: Problem::check::<OrderState>(rsp).await?,
373 url: order_url.ok_or("no order URL found")?,
374 })
375 }
376
377 pub async fn order(&self, url: String) -> Result<Order, Error> {
383 let rsp = self.inner.post(None::<&Empty>, None, &url).await?;
384 Ok(Order {
385 account: self.inner.clone(),
386 nonce: nonce_from_response(&rsp),
387 state: Problem::check::<OrderState>(rsp).await?,
391 url,
392 })
393 }
394
395 pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
397 let revoke_url = match self.inner.client.urls.revoke_cert.as_deref() {
398 Some(url) => url,
399 None => return Err("no revokeCert URL found".into()),
404 };
405
406 let rsp = self.inner.post(Some(payload), None, revoke_url).await?;
407 let _ = Problem::from_response(rsp).await?;
409 Ok(())
410 }
411
412 pub fn id(&self) -> &str {
414 &self.inner.id
415 }
416}
417
418struct AccountInner {
419 client: Client,
420 key: Key,
421 id: String,
422}
423
424impl AccountInner {
425 async fn from_credentials(
426 credentials: AccountCredentials,
427 http: Box<dyn HttpClient>,
428 ) -> Result<Self, Error> {
429 Ok(Self {
430 id: credentials.id,
431 key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
432 client: match (credentials.directory, credentials.urls) {
433 (Some(server_url), _) => Client::new(&server_url, http).await?,
434 (None, Some(urls)) => Client { http, urls },
435 (None, None) => return Err("no server URLs found".into()),
436 },
437 })
438 }
439
440 async fn get<T: DeserializeOwned>(
441 &self,
442 nonce: &mut Option<String>,
443 url: &str,
444 ) -> Result<T, Error> {
445 let rsp = self.post(None::<&Empty>, nonce.take(), url).await?;
446 *nonce = nonce_from_response(&rsp);
447 Problem::check(rsp).await
448 }
449
450 async fn post(
451 &self,
452 payload: Option<&impl Serialize>,
453 nonce: Option<String>,
454 url: &str,
455 ) -> Result<BytesResponse, Error> {
456 self.client.post(payload, nonce, self, url).await
457 }
458}
459
460impl Signer for AccountInner {
461 type Signature = <Key as Signer>::Signature;
462
463 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
464 debug_assert!(nonce.is_some());
465 Header {
466 alg: self.key.signing_algorithm,
467 key: KeyOrKeyId::KeyId(&self.id),
468 nonce,
469 url,
470 }
471 }
472
473 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
474 self.key.sign(payload)
475 }
476}
477
478struct Client {
479 http: Box<dyn HttpClient>,
480 urls: DirectoryUrls,
481}
482
483impl Client {
484 async fn new(server_url: &str, http: Box<dyn HttpClient>) -> Result<Self, Error> {
485 let req = Request::builder()
486 .uri(server_url)
487 .body(Full::default())
488 .expect("infallible error should not occur");
489 let rsp = http.request(req).await?;
490 let body = rsp.body().await.map_err(Error::Other)?;
491 Ok(Client {
492 http,
493 urls: serde_json::from_slice(&body)?,
494 })
495 }
496
497 async fn post(
498 &self,
499 payload: Option<&impl Serialize>,
500 mut nonce: Option<String>,
501 signer: &impl Signer,
502 url: &str,
503 ) -> Result<BytesResponse, Error> {
504 let mut retries = 3;
505 loop {
506 let mut response = self
507 .post_attempt(payload, nonce.clone(), signer, url)
508 .await?;
509 if response.parts.status != StatusCode::BAD_REQUEST {
510 return Ok(response);
511 }
512 let body = response.body.into_bytes().await.map_err(Error::Other)?;
513 let problem = serde_json::from_slice::<Problem>(&body)?;
514 if let Some("urn:ietf:params:acme:error:badNonce") = problem.r#type.as_deref() {
515 retries -= 1;
516 if retries != 0 {
517 nonce = nonce_from_response(&response);
523 continue;
524 }
525 }
526
527 return Ok(BytesResponse {
528 parts: response.parts,
529 body: Box::new(body),
530 });
531 }
532 }
533
534 async fn post_attempt(
535 &self,
536 payload: Option<&impl Serialize>,
537 nonce: Option<String>,
538 signer: &impl Signer,
539 url: &str,
540 ) -> Result<BytesResponse, Error> {
541 let nonce = self.nonce(nonce).await?;
542 let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
543 let request = Request::builder()
544 .method(Method::POST)
545 .uri(url)
546 .header(CONTENT_TYPE, JOSE_JSON)
547 .body(Full::from(serde_json::to_vec(&body)?))?;
548
549 self.http.request(request).await
550 }
551
552 async fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
553 if let Some(nonce) = nonce {
554 return Ok(nonce);
555 }
556
557 let request = Request::builder()
558 .method(Method::HEAD)
559 .uri(&self.urls.new_nonce)
560 .body(Full::default())
561 .expect("infallible error should not occur");
562
563 let rsp = self.http.request(request).await?;
564 if rsp.parts.status != StatusCode::OK {
568 return Err("error response from newNonce resource".into());
569 }
570
571 match nonce_from_response(&rsp) {
572 Some(nonce) => Ok(nonce),
573 None => Err("no nonce found in newNonce response".into()),
574 }
575 }
576}
577
578impl fmt::Debug for Client {
579 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580 f.debug_struct("Client")
581 .field("client", &"..")
582 .field("urls", &self.urls)
583 .finish()
584 }
585}
586
587struct Key {
588 rng: crypto::SystemRandom,
589 signing_algorithm: SigningAlgorithm,
590 inner: crypto::EcdsaKeyPair,
591 thumb: String,
592}
593
594impl Key {
595 fn generate() -> Result<(Self, crypto::pkcs8::Document), Error> {
596 let rng = crypto::SystemRandom::new();
597 let pkcs8 =
598 crypto::EcdsaKeyPair::generate_pkcs8(&crypto::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)?;
599 Self::new(pkcs8.as_ref(), rng).map(|key| (key, pkcs8))
600 }
601
602 fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
603 Self::new(pkcs8_der, crypto::SystemRandom::new())
604 }
605
606 fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result<Self, Error> {
607 let inner = crypto::p256_key_pair_from_pkcs8(pkcs8_der, &rng)?;
608 let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&inner)?);
609 Ok(Self {
610 rng,
611 signing_algorithm: SigningAlgorithm::Es256,
612 inner,
613 thumb,
614 })
615 }
616}
617
618impl Signer for Key {
619 type Signature = crypto::Signature;
620
621 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
622 debug_assert!(nonce.is_some());
623 Header {
624 alg: self.signing_algorithm,
625 key: KeyOrKeyId::from_key(&self.inner),
626 nonce,
627 url,
628 }
629 }
630
631 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
632 Ok(self.inner.sign(&self.rng, payload)?)
633 }
634}
635
636pub struct KeyAuthorization(String);
642
643impl KeyAuthorization {
644 fn new(challenge: &Challenge, key: &Key) -> Self {
645 Self(format!("{}.{}", challenge.token, &key.thumb))
646 }
647
648 pub fn as_str(&self) -> &str {
652 &self.0
653 }
654
655 pub fn digest(&self) -> impl AsRef<[u8]> {
661 crypto::digest(&crypto::SHA256, self.0.as_bytes())
662 }
663
664 pub fn dns_value(&self) -> String {
668 BASE64_URL_SAFE_NO_PAD.encode(self.digest())
669 }
670}
671
672impl fmt::Debug for KeyAuthorization {
673 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674 f.debug_tuple("KeyAuthorization").finish()
675 }
676}
677
678pub struct ExternalAccountKey {
682 id: String,
683 key: crypto::hmac::Key,
684}
685
686impl ExternalAccountKey {
687 pub fn new(id: String, key_value: &[u8]) -> Self {
689 Self {
690 id,
691 key: crypto::hmac::Key::new(crypto::hmac::HMAC_SHA256, key_value),
692 }
693 }
694}
695
696impl Signer for ExternalAccountKey {
697 type Signature = crypto::hmac::Tag;
698
699 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
700 debug_assert_eq!(nonce, None);
701 Header {
702 alg: SigningAlgorithm::Hs256,
703 key: KeyOrKeyId::KeyId(&self.id),
704 nonce,
705 url,
706 }
707 }
708
709 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
710 Ok(crypto::hmac::sign(&self.key, payload))
711 }
712}
713
714fn nonce_from_response(rsp: &BytesResponse) -> Option<String> {
715 rsp.parts
716 .headers
717 .get(REPLAY_NONCE)
718 .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
719}
720
721#[cfg(feature = "hyper-rustls")]
722struct DefaultClient(HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, Full<Bytes>>);
723
724#[cfg(feature = "hyper-rustls")]
725impl DefaultClient {
726 fn try_new() -> Result<Self, Error> {
727 Ok(Self(
728 HyperClient::builder(TokioExecutor::new()).build(
729 hyper_rustls::HttpsConnectorBuilder::new()
730 .with_native_roots()
731 .map_err(|e| Error::Other(Box::new(e)))?
732 .https_only()
733 .enable_http1()
734 .enable_http2()
735 .build(),
736 ),
737 ))
738 }
739}
740
741#[cfg(feature = "hyper-rustls")]
742impl HttpClient for DefaultClient {
743 fn request(
744 &self,
745 req: Request<Full<Bytes>>,
746 ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
747 let fut = self.0.request(req);
748 Box::pin(async move {
749 match fut.await {
750 Ok(rsp) => Ok(BytesResponse::from(rsp)),
751 Err(e) => Err(e.into()),
752 }
753 })
754 }
755}
756
757pub trait HttpClient: Send + Sync + 'static {
759 fn request(
761 &self,
762 req: Request<Full<Bytes>>,
763 ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>>;
764}
765
766#[cfg(feature = "hyper-rustls")]
767impl<C: Connect + Clone + Send + Sync + 'static> HttpClient for HyperClient<C, Full<Bytes>> {
768 fn request(
769 &self,
770 req: Request<Full<Bytes>>,
771 ) -> Pin<Box<dyn Future<Output = Result<BytesResponse, Error>> + Send>> {
772 let fut = self.request(req);
773 Box::pin(async move {
774 match fut.await {
775 Ok(rsp) => Ok(BytesResponse::from(rsp)),
776 Err(e) => Err(e.into()),
777 }
778 })
779 }
780}
781
782pub struct BytesResponse {
784 pub parts: http::response::Parts,
786 pub body: Box<dyn BytesBody>,
788}
789
790impl BytesResponse {
791 pub(crate) async fn body(mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
792 self.body.into_bytes().await
793 }
794}
795
796impl<B> From<Response<B>> for BytesResponse
797where
798 B: http_body::Body + Send + Unpin + 'static,
799 B::Data: Send,
800 B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
801{
802 fn from(rsp: Response<B>) -> Self {
803 let (parts, body) = rsp.into_parts();
804 Self {
805 parts,
806 body: Box::new(BodyWrapper { inner: Some(body) }),
807 }
808 }
809}
810
811struct BodyWrapper<B> {
812 inner: Option<B>,
813}
814
815#[async_trait]
816impl<B> BytesBody for BodyWrapper<B>
817where
818 B: http_body::Body + Send + Unpin + 'static,
819 B::Data: Send,
820 B::Error: Into<Box<dyn StdError + Send + Sync + 'static>>,
821{
822 async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
823 let Some(body) = self.inner.take() else {
824 return Ok(Bytes::new());
825 };
826
827 match body.collect().await {
828 Ok(body) => Ok(body.to_bytes()),
829 Err(e) => Err(e.into()),
830 }
831 }
832}
833
834#[async_trait]
835impl BytesBody for Bytes {
836 async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>> {
837 Ok(self.to_owned())
838 }
839}
840
841#[async_trait]
843pub trait BytesBody: Send {
844 #[allow(clippy::wrong_self_convention)] async fn into_bytes(&mut self) -> Result<Bytes, Box<dyn StdError + Send + Sync + 'static>>;
849}
850
851mod crypto {
852 #[cfg(all(feature = "aws-lc-rs", any(feature = "fips", not(feature = "ring"))))]
853 pub(crate) use aws_lc_rs as ring_like;
854 #[cfg(all(feature = "ring", not(feature = "fips")))]
855 pub(crate) use ring as ring_like;
856
857 pub(crate) use ring_like::digest::{digest, Digest, SHA256};
858 pub(crate) use ring_like::error::{KeyRejected, Unspecified};
859 pub(crate) use ring_like::rand::SystemRandom;
860 pub(crate) use ring_like::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
861 pub(crate) use ring_like::signature::{KeyPair, Signature};
862 pub(crate) use ring_like::{hmac, pkcs8};
863
864 #[cfg(all(feature = "aws-lc-rs", any(feature = "fips", not(feature = "ring"))))]
865 pub(crate) fn p256_key_pair_from_pkcs8(
866 pkcs8: &[u8],
867 _: &SystemRandom,
868 ) -> Result<EcdsaKeyPair, KeyRejected> {
869 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8)
870 }
871
872 #[cfg(all(feature = "ring", not(feature = "fips")))]
873 pub(crate) fn p256_key_pair_from_pkcs8(
874 pkcs8: &[u8],
875 rng: &SystemRandom,
876 ) -> Result<EcdsaKeyPair, KeyRejected> {
877 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8, rng)
878 }
879}
880
881const JOSE_JSON: &str = "application/jose+json";
882const REPLAY_NONCE: &str = "Replay-Nonce";
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[tokio::test]
889 async fn deserialize_old_credentials() -> Result<(), Error> {
890 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"}}"#;
891 Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
892 Ok(())
893 }
894
895 #[tokio::test]
896 async fn deserialize_new_credentials() -> Result<(), Error> {
897 const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
898 Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
899 Ok(())
900 }
901}