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::SecretKey, 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        crypto_rustls::install_default_rustls_provider();
197        Ok(Self {
198            inner: Arc::new(AccountInner::from_credentials(credentials, reqwest::Client::new()).await?),
199        })
200    }
201
202    /// Restore an existing account from the given credentials and HTTP client
203    ///
204    /// The [`AccountCredentials`] type is opaque, but supports deserialization.
205    pub async fn from_credentials_and_http(
206        credentials: AccountCredentials,
207        http: reqwest::Client,
208    ) -> Result<Self, Error> {
209        Ok(Self {
210            inner: Arc::new(AccountInner::from_credentials(credentials, http).await?),
211        })
212    }
213
214    /// Restore an existing account from the given ID, private key, server URL and HTTP client
215    ///
216    /// The key must be provided in DER-encoded PKCS#8. This is usually how ECDSA keys are
217    /// encoded in PEM files. Use a crate like rustls-pemfile to decode from PEM to DER.
218    pub async fn from_parts(
219        id: String,
220        key_pkcs8_der: &[u8],
221        directory_url: &str,
222        http: reqwest::Client,
223    ) -> Result<Self, Error> {
224        Ok(Self {
225            inner: Arc::new(AccountInner {
226                id,
227                key: Key::from_pkcs8_der(key_pkcs8_der)?,
228                client: Client::new(directory_url, http).await?,
229            }),
230        })
231    }
232
233    /// Create a new account on the `server_url` with the information in [`NewAccount`]
234    ///
235    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
236    /// Use [`Account::from_credentials()`] to restore the account from the credentials.
237    // #[cfg(feature = "hyper-rustls")]
238    // pub async fn create(
239    //     account: &NewAccount<'_>,
240    //     server_url: &str,
241    //     external_account: Option<&ExternalAccountKey>,
242    // ) -> Result<(Account, AccountCredentials), Error> {
243    //     Self::create_inner(
244    //         account,
245    //         external_account,
246    //         Client::new(server_url, Box::<DefaultClient>::default()).await?,
247    //         server_url,
248    //     )
249    //     .await
250    // }
251
252    /// Create a new account with a custom HTTP client
253    ///
254    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
255    /// Use [`Account::from_credentials()`] to restore the account from the credentials.
256    pub async fn create(
257        account: &NewAccount<'_>,
258        server_url: &str,
259        external_account: Option<&ExternalAccountKey>,
260        http: reqwest::Client,
261    ) -> Result<(Account, AccountCredentials), Error> {
262        Self::create_inner(account, external_account, Client::new(server_url, http).await?, server_url).await
263    }
264
265    async fn create_inner(
266        account: &NewAccount<'_>,
267        external_account: Option<&ExternalAccountKey>,
268        client: Client,
269        server_url: &str,
270    ) -> Result<(Account, AccountCredentials), Error> {
271        let (key, key_pkcs8) = Key::generate()?;
272        let payload = NewAccountPayload {
273            new_account: account,
274            external_account_binding: external_account
275                .map(|eak| {
276                    JoseJson::new(
277                        Some(&Jwk::new(&key.inner.public_key())),
278                        eak.header(None, &client.urls.new_account),
279                        eak,
280                    )
281                })
282                .transpose()?,
283        };
284
285        let rsp = client
286            .post(Some(&payload), None, &key, &client.urls.new_account)
287            .await?;
288
289        let account_url = rsp
290            .headers()
291            .get(LOCATION)
292            .and_then(|hv| hv.to_str().ok())
293            .map(|s| s.to_owned());
294
295        // The response redirects, we don't need the body
296        let _ = Problem::from_response(rsp).await?;
297        let id = account_url.ok_or("failed to get account URL")?;
298        let credentials = AccountCredentials {
299            id: id.clone(),
300            key_pkcs8: key_pkcs8.to_vec(),
301            directory: Some(server_url.to_owned()),
302            // We support deserializing URLs for compatibility with versions pre 0.4,
303            // but we prefer to get fresh URLs from the `server_url` for newer credentials.
304            urls: None,
305        };
306
307        let account = AccountInner {
308            client,
309            key,
310            id: id.clone(),
311        };
312
313        Ok((
314            Self {
315                inner: Arc::new(account),
316            },
317            credentials,
318        ))
319    }
320
321    /// Create a new order based on the given [`NewOrder`]
322    ///
323    /// Returns an [`Order`] instance. Use the [`Order::state()`] method to inspect its state.
324    pub async fn new_order<'a>(&'a self, order: &NewOrder<'_>) -> Result<Order, Error> {
325        let rsp = self
326            .inner
327            .post(Some(order), None, &self.inner.client.urls.new_order)
328            .await?;
329
330        let nonce = nonce_from_response(&rsp);
331        let order_url = rsp
332            .headers()
333            .get(LOCATION)
334            .and_then(|hv| hv.to_str().ok())
335            .map(|s| s.to_owned());
336
337        Ok(Order {
338            account: self.inner.clone(),
339            nonce,
340            // Order of fields matters! We return errors from Problem::check
341            // before emitting an error if there is no order url. Or the
342            // simple no url error hides the causing error in `Problem::check`.
343            state: Problem::check::<OrderState>(rsp).await?,
344            url: order_url.ok_or("no order URL found")?,
345        })
346    }
347
348    // /// Revokes a previously issued certificate
349    // pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
350    //     let revoke_url = match self.inner.client.urls.revoke_cert.as_deref() {
351    //         Some(url) => url,
352    //         // This happens because the current account credentials were deserialized from an
353    //         // older version which only serialized a subset of the directory URLs. You should
354    //         // make sure the account credentials include a `directory` field containing a
355    //         // string with the server's directory URL.
356    //         None => return Err("no revokeCert URL found".into()),
357    //     };
358
359    //     let rsp = self.inner.post(Some(payload), None, revoke_url).await?;
360    //     // The body is empty if the request was successful
361    //     let _ = Problem::from_response(rsp).await?;
362    //     Ok(())
363    // }
364}
365
366struct AccountInner {
367    client: Client,
368    key: Key,
369    id: String,
370}
371
372impl AccountInner {
373    async fn from_credentials(credentials: AccountCredentials, http: reqwest::Client) -> Result<Self, Error> {
374        Ok(Self {
375            id: credentials.id,
376            key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
377            client: match (credentials.directory, credentials.urls) {
378                (Some(server_url), _) => Client::new(&server_url, http).await?,
379                (None, Some(urls)) => Client {
380                    http,
381                    urls,
382                },
383                (None, None) => return Err("no server URLs found".into()),
384            },
385        })
386    }
387
388    async fn get<T: DeserializeOwned>(&self, nonce: &mut Option<String>, url: &str) -> Result<T, Error> {
389        let rsp = self.post(None::<&Empty>, nonce.take(), url).await?;
390        *nonce = nonce_from_response(&rsp);
391        Problem::check(rsp).await
392    }
393
394    async fn post(
395        &self,
396        payload: Option<&impl Serialize>,
397        nonce: Option<String>,
398        url: &str,
399    ) -> Result<Response, Error> {
400        self.client.post(payload, nonce, self, url).await
401    }
402}
403
404impl Signer for AccountInner {
405    type Signature = <Key as Signer>::Signature;
406
407    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
408        debug_assert!(nonce.is_some());
409        Header {
410            alg: self.key.signing_algorithm,
411            key: KeyOrKeyId::KeyId(&self.id),
412            nonce,
413            url,
414        }
415    }
416
417    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
418        self.key.sign(payload)
419    }
420}
421
422struct Client {
423    http: reqwest::Client,
424    urls: DirectoryUrls,
425}
426
427impl Client {
428    async fn new(server_url: &str, http: reqwest::Client) -> Result<Self, Error> {
429        // let req = Request::new(Method::GET, )
430        //     .body(Body::empty())
431        //     .unwrap();
432        let res = http.get(server_url).send().await?;
433
434        // .request(req).await?;
435        let urls = res.json().await?;
436        Ok(Client {
437            http,
438            urls,
439        })
440    }
441
442    async fn post(
443        &self,
444        payload: Option<&impl Serialize>,
445        nonce: Option<String>,
446        signer: &impl Signer,
447        url: &str,
448    ) -> Result<Response, Error> {
449        let nonce = self.nonce(nonce).await?;
450        let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
451
452        let request = self
453            .http
454            .request(Method::POST, url)
455            .header(CONTENT_TYPE, JOSE_JSON)
456            .json(&body)
457            .build()?;
458        // let request = RequestBuilder::()
459        //     .method(Method::POST)
460        //     .uri(url)
461        //     .header(CONTENT_TYPE, JOSE_JSON)
462        //     .body(Body::from(serde_json::to_vec(&body)?))
463        //     .unwrap();
464
465        Ok(self.http.execute(request).await?)
466    }
467
468    async fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
469        if let Some(nonce) = nonce {
470            return Ok(nonce);
471        }
472
473        let rsp = self.http.head(&self.urls.new_nonce).send().await?;
474
475        // https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
476        // "The server's response MUST include a Replay-Nonce header field containing a fresh
477        // nonce and SHOULD have status code 200 (OK)."
478        if rsp.status() != StatusCode::OK {
479            return Err("error response from newNonce resource".into());
480        }
481
482        match nonce_from_response(&rsp) {
483            Some(nonce) => Ok(nonce),
484            None => Err("no nonce found in newNonce response".into()),
485        }
486    }
487}
488
489impl fmt::Debug for Client {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        f.debug_struct("Client")
492            .field("client", &"..")
493            .field("urls", &self.urls)
494            .finish()
495    }
496}
497
498struct Key {
499    signing_algorithm: SigningAlgorithm,
500    inner: SecretKey,
501    thumb: String,
502}
503
504impl Key {
505    fn generate() -> Result<(Self, [u8; 138]), Error> {
506        let inner = SecretKey::generate().map_err(|_| Error::Crypto)?;
507        let pkcs8_der = pkcs8::encode_p256_pkcs8_der(&inner).map_err(|_| Error::Crypto)?;
508        let thumb = base64::encode(Jwk::thumb_sha256(&inner.public_key())?, base64::Alphabet::UrlNoPadding);
509
510        Ok((
511            Self {
512                signing_algorithm: SigningAlgorithm::Es256,
513                inner,
514                thumb,
515            },
516            pkcs8_der,
517        ))
518    }
519
520    fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
521        let inner = pkcs8::decode_p256_pkcs8_der(pkcs8_der).map_err(|_| Error::CryptoKey)?;
522        let thumb = base64::encode(Jwk::thumb_sha256(&inner.public_key())?, base64::Alphabet::UrlNoPadding);
523
524        Ok(Self {
525            signing_algorithm: SigningAlgorithm::Es256,
526            inner,
527            thumb,
528        })
529    }
530}
531
532impl Signer for Key {
533    type Signature = [u8; 64];
534
535    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
536        debug_assert!(nonce.is_some());
537        Header {
538            alg: self.signing_algorithm,
539            key: KeyOrKeyId::from_key(&self.inner.public_key()),
540            nonce,
541            url,
542        }
543    }
544
545    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
546        self.inner.sign(payload).map_err(|_| Error::Crypto)
547    }
548}
549
550/// The response value to use for challenge responses
551///
552/// Refer to the methods below to see which encoding to use for your challenge type.
553///
554/// <https://datatracker.ietf.org/doc/html/rfc8555#section-8.1>
555pub struct KeyAuthorization(String);
556
557impl KeyAuthorization {
558    fn new(challenge: &Challenge, key: &Key) -> Self {
559        Self(format!("{}.{}", challenge.token, &key.thumb))
560    }
561
562    /// Get the key authorization value
563    ///
564    /// This can be used for HTTP-01 challenge responses.
565    pub fn as_str(&self) -> &str {
566        &self.0
567    }
568
569    /// Get the SHA-256 digest of the key authorization
570    ///
571    /// This can be used for TLS-ALPN-01 challenge responses.
572    ///
573    /// <https://datatracker.ietf.org/doc/html/rfc8737#section-3>
574    pub fn digest(&self) -> impl AsRef<[u8]> {
575        let hash = Sha256::hash(self.0.as_bytes());
576        let mut out = [0u8; 32];
577        out.copy_from_slice(&hash.as_ref()[..32]);
578        out
579    }
580
581    /// Get the base64-encoded SHA256 digest of the key authorization
582    ///
583    /// This can be used for DNS-01 challenge responses.
584    pub fn dns_value(&self) -> String {
585        base64::encode(self.digest().as_ref(), base64::Alphabet::UrlNoPadding)
586    }
587}
588
589impl fmt::Debug for KeyAuthorization {
590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591        f.debug_tuple("KeyAuthorization").finish()
592    }
593}
594
595/// A HMAC key used to link account creation requests to an external account
596///
597/// See RFC 8555 section 7.3.4 for more information.
598pub struct ExternalAccountKey {
599    id: String,
600    key_value: Vec<u8>,
601}
602
603impl ExternalAccountKey {
604    /// Create a new external account key
605    pub fn new(id: String, key_value: &[u8]) -> Self {
606        Self {
607            id,
608            key_value: key_value.to_vec(),
609        }
610    }
611}
612
613impl Signer for ExternalAccountKey {
614    type Signature = [u8; 32];
615
616    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
617        debug_assert_eq!(nonce, None);
618        Header {
619            alg: SigningAlgorithm::Hs256,
620            key: KeyOrKeyId::KeyId(&self.id),
621            nonce,
622            url,
623        }
624    }
625
626    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
627        let h = Hmac::<Sha256>::mac(&self.key_value, payload);
628        let mut out = [0u8; 32];
629        out.copy_from_slice(&h.as_ref()[..32]);
630        Ok(out)
631    }
632}
633
634// fn nonce_from_response(rsp: &Response<Body>) -> Option<String> {
635//     rsp.headers()
636//         .get(REPLAY_NONCE)
637//         .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
638// }
639
640fn nonce_from_response(res: &Response) -> Option<String> {
641    res.headers()
642        .get(REPLAY_NONCE)
643        .map(|header| header.to_str().unwrap_or_default().to_string())
644}
645
646// #[cfg(feature = "hyper-rustls")]
647// struct DefaultClient(hyper::Client<hyper_rustls::HttpsConnector<HttpConnector>>);
648
649// #[cfg(feature = "hyper-rustls")]
650// impl HttpClient for DefaultClient {
651//     fn request(&self, req: Request<Body>) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
652//         Box::pin(self.0.request(req))
653//     }
654// }
655
656// #[cfg(feature = "hyper-rustls")]
657// impl Default for DefaultClient {
658//     fn default() -> Self {
659//         Self(
660//             hyper::Client::builder().build(
661//                 hyper_rustls::HttpsConnectorBuilder::new()
662//                     .with_native_roots()
663//                     .https_only()
664//                     .enable_http1()
665//                     .enable_http2()
666//                     .build(),
667//             ),
668//         )
669//     }
670// }
671
672// /// A HTTP client based on [`hyper::Client`]
673// pub trait HttpClient: Send + Sync + 'static {
674//     /// Send the given request and return the response
675//     fn request(
676//         &self,
677//         req: Request<Body>,
678//     ) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>>;
679// }
680
681// impl<C> HttpClient for hyper::Client<C>
682// where
683//     C: Connect + Clone + Send + Sync + 'static,
684// {
685//     fn request(
686//         &self,
687//         req: Request<Body>,
688//     ) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
689//         Box::pin(<hyper::Client<C>>::request(self, req))
690//     }
691// }
692
693const JOSE_JSON: &str = "application/jose+json";
694const REPLAY_NONCE: &str = "Replay-Nonce";
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    #[tokio::test]
701    async fn deserialize_old_credentials() -> Result<(), Error> {
702        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"}}"#;
703        Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
704        Ok(())
705    }
706
707    #[tokio::test]
708    async fn deserialize_new_credentials() -> Result<(), Error> {
709        const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
710        Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
711        Ok(())
712    }
713}