Skip to main content

acme/
acme.rs

1//! Async pure-Rust ACME (RFC 8555) client.
2
3#![warn(unreachable_pub)]
4#![warn(missing_docs)]
5
6use std::{fmt, sync::Arc};
7
8use crypto::{Hasher, encoding::pkcs8, hmac::Hmac, p256::PrivateKey, sha2::Sha256};
9use reqwest::{
10    Method, Response, StatusCode,
11    header::{CONTENT_TYPE, LOCATION},
12};
13use serde::{Serialize, de::DeserializeOwned};
14
15mod types;
16pub use types::{
17    AccountCredentials,
18    Authorization,
19    AuthorizationStatus,
20    Challenge,
21    ChallengeType,
22    Error,
23    Identifier,
24    LetsEncrypt,
25    NewAccount,
26    NewOrder,
27    OrderState,
28    OrderStatus,
29    Problem,
30    RevocationReason,
31    ZeroSsl, // RevocationRequest
32};
33use types::{
34    DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload, Signer,
35    SigningAlgorithm,
36};
37
38/// An ACME order as described in RFC 8555 (section 7.1.3)
39///
40/// An order is created from an [`Account`] by calling [`Account::new_order()`]. The `Order`
41/// type represents the stable identity of an order, while the [`Order::state()`] method
42/// gives you access to the current state of the order according to the server.
43///
44/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
45pub struct Order {
46    account: Arc<AccountInner>,
47    nonce: Option<String>,
48    url: String,
49    state: OrderState,
50}
51
52impl Order {
53    /// Retrieve the authorizations for this order
54    ///
55    /// An order will contain one authorization to complete per identifier in the order.
56    /// After creating an order, you'll need to retrieve the authorizations so that
57    /// you can set up a challenge response for each authorization.
58    ///
59    /// For each authorization, you'll need to:
60    ///
61    /// * Select which [`ChallengeType`] you want to complete
62    /// * Create a [`KeyAuthorization`] for that [`Challenge`]
63    /// * Call [`Order::set_challenge_ready()`] for that challenge
64    ///
65    /// After the challenges have been set up, check the [`Order::state()`] to see
66    /// if the order is ready to be finalized (or becomes invalid). Once it is
67    /// ready, call `Order::finalize()` to get the certificate.
68    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    /// Create a [`KeyAuthorization`] for the given [`Challenge`]
77    ///
78    /// Signs the challenge's token with the account's private key and use the
79    /// value from [`KeyAuthorization::as_str()`] as the challenge response.
80    pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
81        KeyAuthorization::new(challenge, &self.account.key)
82    }
83
84    /// Request a certificate from the given Certificate Signing Request (CSR)
85    ///
86    /// Creating a CSR is outside of the scope of instant-acme. Make sure you pass in a
87    /// DER representation of the CSR in `csr_der`. Call `certificate()` to retrieve the
88    /// certificate chain once the order is in the appropriate state.
89    pub async fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> {
90        let rsp = self
91            .account
92            .post(Some(&FinalizeRequest::new(csr_der)), self.nonce.take(), &self.state.finalize)
93            .await?;
94
95        self.nonce = nonce_from_response(&rsp);
96        self.state = Problem::check::<OrderState>(rsp).await?;
97        Ok(())
98    }
99
100    /// Get the certificate for this order
101    ///
102    /// If the cached order state is in `ready` or `processing` state, this will poll the server
103    /// for the latest state. If the order is still in `processing` state after that, this will
104    /// return `Ok(None)`. If the order is in `valid` state, this will attempt to retrieve
105    /// the certificate from the server and return it as a `String`. If the order contains
106    /// an error or ends up in any state other than `valid` or `processing`, return an error.
107    pub async fn certificate(&mut self) -> Result<Option<String>, Error> {
108        if matches!(self.state.status, OrderStatus::Processing) {
109            let rsp = self.account.post(None::<&Empty>, self.nonce.take(), &self.url).await?;
110            self.nonce = nonce_from_response(&rsp);
111            self.state = Problem::check::<OrderState>(rsp).await?;
112        }
113
114        if let Some(error) = &self.state.error {
115            return Err(Error::Api(error.clone()));
116        } else if self.state.status == OrderStatus::Processing {
117            return Ok(None);
118        } else if self.state.status != OrderStatus::Valid {
119            return Err(Error::Str("invalid order state"));
120        }
121
122        let cert_url = match &self.state.certificate {
123            Some(cert_url) => cert_url,
124            None => return Err(Error::Str("no certificate URL found")),
125        };
126
127        let rsp = self.account.post(None::<&Empty>, self.nonce.take(), cert_url).await?;
128
129        // let body = rsp.bytes().await?;
130        let body = Problem::from_response(rsp).await?;
131        Ok(Some(
132            String::from_utf8(body.to_vec()).map_err(|_| "unable to decode certificate as UTF-8")?,
133        ))
134    }
135
136    /// Notify the server that the given challenge is ready to be completed
137    ///
138    /// `challenge_url` should be the `Challenge::url` field.
139    pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
140        let rsp = self
141            .account
142            .post(Some(&Empty {}), self.nonce.take(), challenge_url)
143            .await?;
144
145        self.nonce = nonce_from_response(&rsp);
146        let _ = Problem::check::<Challenge>(rsp).await?;
147        Ok(())
148    }
149
150    /// Get the current state of the given challenge
151    pub async fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
152        self.account.get(&mut self.nonce, challenge_url).await
153    }
154
155    /// Refresh the current state of the order
156    pub async fn refresh(&mut self) -> Result<&OrderState, Error> {
157        let rsp = self.account.post(None::<&Empty>, self.nonce.take(), &self.url).await?;
158
159        self.nonce = nonce_from_response(&rsp);
160        self.state = Problem::check::<OrderState>(rsp).await?;
161        Ok(&self.state)
162    }
163
164    /// Get the last known state of the order
165    ///
166    /// Call `refresh()` to get the latest state from the server.
167    pub fn state(&mut self) -> &OrderState {
168        &self.state
169    }
170
171    /// Get the URL of the order
172    pub fn url(&self) -> &str {
173        &self.url
174    }
175}
176
177/// An ACME account as described in RFC 8555 (section 7.1.2)
178///
179/// Create an [`Account`] with [`Account::create()`] or restore it from serialized data
180/// by passing deserialized [`AccountCredentials`] to [`Account::from_credentials()`].
181///
182/// The [`Account`] type is cheap to clone.
183///
184/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2>
185#[derive(Clone)]
186pub struct Account {
187    inner: Arc<AccountInner>,
188}
189
190impl Account {
191    /// Restore an existing account from the given credentials
192    ///
193    /// The [`AccountCredentials`] type is opaque, but supports deserialization.
194    // #[cfg(feature = "hyper-rustls")]
195    pub async fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
196        Ok(Self {
197            inner: Arc::new(AccountInner::from_credentials(credentials, reqwest::Client::new()).await?),
198        })
199    }
200
201    /// Restore an existing account from the given credentials and HTTP client
202    ///
203    /// The [`AccountCredentials`] type is opaque, but supports deserialization.
204    pub async fn from_credentials_and_http(
205        credentials: AccountCredentials,
206        http: reqwest::Client,
207    ) -> Result<Self, Error> {
208        Ok(Self {
209            inner: Arc::new(AccountInner::from_credentials(credentials, http).await?),
210        })
211    }
212
213    /// Restore an existing account from the given ID, private key, server URL and HTTP client
214    ///
215    /// The key must be provided in DER-encoded PKCS#8. This is usually how ECDSA keys are
216    /// encoded in PEM files. Use a crate like rustls-pemfile to decode from PEM to DER.
217    pub async fn from_parts(
218        id: String,
219        key_pkcs8_der: &[u8],
220        directory_url: &str,
221        http: reqwest::Client,
222    ) -> Result<Self, Error> {
223        Ok(Self {
224            inner: Arc::new(AccountInner {
225                id,
226                key: Key::from_pkcs8_der(key_pkcs8_der)?,
227                client: Client::new(directory_url, http).await?,
228            }),
229        })
230    }
231
232    /// Create a new account on the `server_url` with the information in [`NewAccount`]
233    ///
234    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
235    /// Use [`Account::from_credentials()`] to restore the account from the credentials.
236    #[cfg(feature = "hyper-rustls")]
237    pub async fn create(
238        account: &NewAccount<'_>,
239        server_url: &str,
240        external_account: Option<&ExternalAccountKey>,
241    ) -> Result<(Account, AccountCredentials), Error> {
242        Self::create_inner(
243            account,
244            external_account,
245            Client::new(server_url, Box::<DefaultClient>::default()).await?,
246            server_url,
247        )
248        .await
249    }
250
251    /// Create a new account with a custom HTTP client
252    ///
253    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
254    /// Use [`Account::from_credentials()`] to restore the account from the credentials.
255    pub async fn create_with_http(
256        account: &NewAccount<'_>,
257        server_url: &str,
258        external_account: Option<&ExternalAccountKey>,
259        http: reqwest::Client,
260    ) -> Result<(Account, AccountCredentials), Error> {
261        Self::create_inner(account, external_account, Client::new(server_url, http).await?, server_url).await
262    }
263
264    async fn create_inner(
265        account: &NewAccount<'_>,
266        external_account: Option<&ExternalAccountKey>,
267        client: Client,
268        server_url: &str,
269    ) -> Result<(Account, AccountCredentials), Error> {
270        let (key, key_pkcs8) = Key::generate()?;
271        let payload = NewAccountPayload {
272            new_account: account,
273            external_account_binding: external_account
274                .map(|eak| {
275                    JoseJson::new(
276                        Some(&Jwk::new(&key.inner.public_key())),
277                        eak.header(None, &client.urls.new_account),
278                        eak,
279                    )
280                })
281                .transpose()?,
282        };
283
284        let rsp = client
285            .post(Some(&payload), None, &key, &client.urls.new_account)
286            .await?;
287
288        let account_url = rsp
289            .headers()
290            .get(LOCATION)
291            .and_then(|hv| hv.to_str().ok())
292            .map(|s| s.to_owned());
293
294        // The response redirects, we don't need the body
295        let _ = Problem::from_response(rsp).await?;
296        let id = account_url.ok_or("failed to get account URL")?;
297        let credentials = AccountCredentials {
298            id: id.clone(),
299            key_pkcs8: key_pkcs8.to_vec(),
300            directory: Some(server_url.to_owned()),
301            // We support deserializing URLs for compatibility with versions pre 0.4,
302            // but we prefer to get fresh URLs from the `server_url` for newer credentials.
303            urls: None,
304        };
305
306        let account = AccountInner {
307            client,
308            key,
309            id: id.clone(),
310        };
311
312        Ok((
313            Self {
314                inner: Arc::new(account),
315            },
316            credentials,
317        ))
318    }
319
320    /// Create a new order based on the given [`NewOrder`]
321    ///
322    /// Returns an [`Order`] instance. Use the [`Order::state()`] method to inspect its state.
323    pub async fn new_order<'a>(&'a self, order: &NewOrder<'_>) -> Result<Order, Error> {
324        let rsp = self
325            .inner
326            .post(Some(order), None, &self.inner.client.urls.new_order)
327            .await?;
328
329        let nonce = nonce_from_response(&rsp);
330        let order_url = rsp
331            .headers()
332            .get(LOCATION)
333            .and_then(|hv| hv.to_str().ok())
334            .map(|s| s.to_owned());
335
336        Ok(Order {
337            account: self.inner.clone(),
338            nonce,
339            // Order of fields matters! We return errors from Problem::check
340            // before emitting an error if there is no order url. Or the
341            // simple no url error hides the causing error in `Problem::check`.
342            state: Problem::check::<OrderState>(rsp).await?,
343            url: order_url.ok_or("no order URL found")?,
344        })
345    }
346
347    // /// Revokes a previously issued certificate
348    // pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
349    //     let revoke_url = match self.inner.client.urls.revoke_cert.as_deref() {
350    //         Some(url) => url,
351    //         // This happens because the current account credentials were deserialized from an
352    //         // older version which only serialized a subset of the directory URLs. You should
353    //         // make sure the account credentials include a `directory` field containing a
354    //         // string with the server's directory URL.
355    //         None => return Err("no revokeCert URL found".into()),
356    //     };
357
358    //     let rsp = self.inner.post(Some(payload), None, revoke_url).await?;
359    //     // The body is empty if the request was successful
360    //     let _ = Problem::from_response(rsp).await?;
361    //     Ok(())
362    // }
363}
364
365struct AccountInner {
366    client: Client,
367    key: Key,
368    id: String,
369}
370
371impl AccountInner {
372    async fn from_credentials(credentials: AccountCredentials, http: reqwest::Client) -> Result<Self, Error> {
373        Ok(Self {
374            id: credentials.id,
375            key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
376            client: match (credentials.directory, credentials.urls) {
377                (Some(server_url), _) => Client::new(&server_url, http).await?,
378                (None, Some(urls)) => Client {
379                    http,
380                    urls,
381                },
382                (None, None) => return Err("no server URLs found".into()),
383            },
384        })
385    }
386
387    async fn get<T: DeserializeOwned>(&self, nonce: &mut Option<String>, url: &str) -> Result<T, Error> {
388        let rsp = self.post(None::<&Empty>, nonce.take(), url).await?;
389        *nonce = nonce_from_response(&rsp);
390        Problem::check(rsp).await
391    }
392
393    async fn post(
394        &self,
395        payload: Option<&impl Serialize>,
396        nonce: Option<String>,
397        url: &str,
398    ) -> Result<Response, Error> {
399        self.client.post(payload, nonce, self, url).await
400    }
401}
402
403impl Signer for AccountInner {
404    type Signature = <Key as Signer>::Signature;
405
406    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
407        debug_assert!(nonce.is_some());
408        Header {
409            alg: self.key.signing_algorithm,
410            key: KeyOrKeyId::KeyId(&self.id),
411            nonce,
412            url,
413        }
414    }
415
416    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
417        self.key.sign(payload)
418    }
419}
420
421struct Client {
422    http: reqwest::Client,
423    urls: DirectoryUrls,
424}
425
426impl Client {
427    async fn new(server_url: &str, http: reqwest::Client) -> Result<Self, Error> {
428        // let req = Request::new(Method::GET, )
429        //     .body(Body::empty())
430        //     .unwrap();
431        let res = http.get(server_url).send().await?;
432
433        // .request(req).await?;
434        let urls = res.json().await?;
435        Ok(Client {
436            http,
437            urls,
438        })
439    }
440
441    async fn post(
442        &self,
443        payload: Option<&impl Serialize>,
444        nonce: Option<String>,
445        signer: &impl Signer,
446        url: &str,
447    ) -> Result<Response, Error> {
448        let nonce = self.nonce(nonce).await?;
449        let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
450
451        let request = self
452            .http
453            .request(Method::POST, url)
454            .header(CONTENT_TYPE, JOSE_JSON)
455            .json(&body)
456            .build()?;
457        // let request = RequestBuilder::()
458        //     .method(Method::POST)
459        //     .uri(url)
460        //     .header(CONTENT_TYPE, JOSE_JSON)
461        //     .body(Body::from(serde_json::to_vec(&body)?))
462        //     .unwrap();
463
464        Ok(self.http.execute(request).await?)
465    }
466
467    async fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
468        if let Some(nonce) = nonce {
469            return Ok(nonce);
470        }
471
472        let rsp = self.http.head(&self.urls.new_nonce).send().await?;
473
474        // https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
475        // "The server's response MUST include a Replay-Nonce header field containing a fresh
476        // nonce and SHOULD have status code 200 (OK)."
477        if rsp.status() != StatusCode::OK {
478            return Err("error response from newNonce resource".into());
479        }
480
481        match nonce_from_response(&rsp) {
482            Some(nonce) => Ok(nonce),
483            None => Err("no nonce found in newNonce response".into()),
484        }
485    }
486}
487
488impl fmt::Debug for Client {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        f.debug_struct("Client")
491            .field("client", &"..")
492            .field("urls", &self.urls)
493            .finish()
494    }
495}
496
497struct Key {
498    signing_algorithm: SigningAlgorithm,
499    inner: PrivateKey,
500    thumb: String,
501}
502
503impl Key {
504    fn generate() -> Result<(Self, [u8; 138]), Error> {
505        let inner = PrivateKey::generate().map_err(|_| Error::Crypto)?;
506        let pkcs8_der = pkcs8::encode_p256_pkcs8_der(&inner).map_err(|_| Error::Crypto)?;
507        let thumb = base64::encode(&Jwk::thumb_sha256(&inner.public_key())?, base64::Alphabet::UrlNoPadding);
508
509        Ok((
510            Self {
511                signing_algorithm: SigningAlgorithm::Es256,
512                inner,
513                thumb,
514            },
515            pkcs8_der,
516        ))
517    }
518
519    fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
520        let inner = pkcs8::decode_p256_pkcs8_der(pkcs8_der).map_err(|_| Error::CryptoKey)?;
521        let thumb = base64::encode(&Jwk::thumb_sha256(&inner.public_key())?, base64::Alphabet::UrlNoPadding);
522
523        Ok(Self {
524            signing_algorithm: SigningAlgorithm::Es256,
525            inner,
526            thumb,
527        })
528    }
529}
530
531impl Signer for Key {
532    type Signature = [u8; 64];
533
534    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
535        debug_assert!(nonce.is_some());
536        Header {
537            alg: self.signing_algorithm,
538            key: KeyOrKeyId::from_key(&self.inner.public_key()),
539            nonce,
540            url,
541        }
542    }
543
544    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
545        self.inner.sign(payload).map_err(|_| Error::Crypto)
546    }
547}
548
549/// The response value to use for challenge responses
550///
551/// Refer to the methods below to see which encoding to use for your challenge type.
552///
553/// <https://datatracker.ietf.org/doc/html/rfc8555#section-8.1>
554pub struct KeyAuthorization(String);
555
556impl KeyAuthorization {
557    fn new(challenge: &Challenge, key: &Key) -> Self {
558        Self(format!("{}.{}", challenge.token, &key.thumb))
559    }
560
561    /// Get the key authorization value
562    ///
563    /// This can be used for HTTP-01 challenge responses.
564    pub fn as_str(&self) -> &str {
565        &self.0
566    }
567
568    /// Get the SHA-256 digest of the key authorization
569    ///
570    /// This can be used for TLS-ALPN-01 challenge responses.
571    ///
572    /// <https://datatracker.ietf.org/doc/html/rfc8737#section-3>
573    pub fn digest(&self) -> impl AsRef<[u8]> {
574        let hash = Sha256::hash(self.0.as_bytes());
575        let mut out = [0u8; 32];
576        out.copy_from_slice(&hash.as_ref()[..32]);
577        out
578    }
579
580    /// Get the base64-encoded SHA256 digest of the key authorization
581    ///
582    /// This can be used for DNS-01 challenge responses.
583    pub fn dns_value(&self) -> String {
584        base64::encode(self.digest().as_ref(), base64::Alphabet::UrlNoPadding)
585    }
586}
587
588impl fmt::Debug for KeyAuthorization {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        f.debug_tuple("KeyAuthorization").finish()
591    }
592}
593
594/// A HMAC key used to link account creation requests to an external account
595///
596/// See RFC 8555 section 7.3.4 for more information.
597pub struct ExternalAccountKey {
598    id: String,
599    key_value: Vec<u8>,
600}
601
602impl ExternalAccountKey {
603    /// Create a new external account key
604    pub fn new(id: String, key_value: &[u8]) -> Self {
605        Self {
606            id,
607            key_value: key_value.to_vec(),
608        }
609    }
610}
611
612impl Signer for ExternalAccountKey {
613    type Signature = [u8; 32];
614
615    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
616        debug_assert_eq!(nonce, None);
617        Header {
618            alg: SigningAlgorithm::Hs256,
619            key: KeyOrKeyId::KeyId(&self.id),
620            nonce,
621            url,
622        }
623    }
624
625    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
626        let h = Hmac::<Sha256>::mac(&self.key_value, payload);
627        let mut out = [0u8; 32];
628        out.copy_from_slice(&h.as_ref()[..32]);
629        Ok(out)
630    }
631}
632
633// fn nonce_from_response(rsp: &Response<Body>) -> Option<String> {
634//     rsp.headers()
635//         .get(REPLAY_NONCE)
636//         .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
637// }
638
639fn nonce_from_response(res: &Response) -> Option<String> {
640    res.headers()
641        .get(REPLAY_NONCE)
642        .map(|header| header.to_str().unwrap_or_default().to_string())
643}
644
645#[cfg(feature = "hyper-rustls")]
646struct DefaultClient(hyper::Client<hyper_rustls::HttpsConnector<HttpConnector>>);
647
648#[cfg(feature = "hyper-rustls")]
649impl HttpClient for DefaultClient {
650    fn request(&self, req: Request<Body>) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
651        Box::pin(self.0.request(req))
652    }
653}
654
655#[cfg(feature = "hyper-rustls")]
656impl Default for DefaultClient {
657    fn default() -> Self {
658        Self(
659            hyper::Client::builder().build(
660                hyper_rustls::HttpsConnectorBuilder::new()
661                    .with_native_roots()
662                    .https_only()
663                    .enable_http1()
664                    .enable_http2()
665                    .build(),
666            ),
667        )
668    }
669}
670
671// /// A HTTP client based on [`hyper::Client`]
672// pub trait HttpClient: Send + Sync + 'static {
673//     /// Send the given request and return the response
674//     fn request(
675//         &self,
676//         req: Request<Body>,
677//     ) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>>;
678// }
679
680// impl<C> HttpClient for hyper::Client<C>
681// where
682//     C: Connect + Clone + Send + Sync + 'static,
683// {
684//     fn request(
685//         &self,
686//         req: Request<Body>,
687//     ) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
688//         Box::pin(<hyper::Client<C>>::request(self, req))
689//     }
690// }
691
692const JOSE_JSON: &str = "application/jose+json";
693const REPLAY_NONCE: &str = "Replay-Nonce";
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698
699    #[tokio::test]
700    async fn deserialize_old_credentials() -> Result<(), Error> {
701        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"}}"#;
702        Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
703        Ok(())
704    }
705
706    #[tokio::test]
707    async fn deserialize_new_credentials() -> Result<(), Error> {
708        const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
709        Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
710        Ok(())
711    }
712}