instant_acme/
account.rs

1#[cfg(feature = "hyper-rustls")]
2use std::path::Path;
3use std::sync::Arc;
4#[cfg(feature = "time")]
5use std::time::{Duration, SystemTime};
6
7use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
8use http::header::LOCATION;
9#[cfg(feature = "time")]
10use http::header::USER_AGENT;
11#[cfg(feature = "time")]
12use http::{Method, Request};
13#[cfg(feature = "hyper-rustls")]
14use rustls::RootCertStore;
15#[cfg(feature = "hyper-rustls")]
16use rustls_pki_types::CertificateDer;
17#[cfg(feature = "hyper-rustls")]
18use rustls_pki_types::pem::PemObject;
19use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
20use serde::de::DeserializeOwned;
21use serde::{Deserialize, Serialize};
22
23#[cfg(feature = "hyper-rustls")]
24use crate::DefaultClient;
25use crate::order::Order;
26use crate::types::{
27    AccountCredentials, AuthorizationStatus, Empty, Header, JoseJson, Jwk, KeyOrKeyId, NewAccount,
28    NewAccountPayload, NewOrder, OrderState, Problem, ProfileMeta, RevocationRequest, Signer,
29    SigningAlgorithm,
30};
31#[cfg(feature = "time")]
32use crate::types::{CertificateIdentifier, RenewalInfo};
33#[cfg(feature = "time")]
34use crate::{BodyWrapper, CRATE_USER_AGENT, retry_after};
35use crate::{BytesResponse, Client, Error, HttpClient, crypto, nonce_from_response};
36
37/// An ACME account as described in RFC 8555 (section 7.1.2)
38///
39/// Create an [`Account`] via [`Account::builder()`] or [`Account::builder_with_http()`],
40/// then use [`AccountBuilder::create()`] to create a new account or restore one from
41/// serialized data by passing deserialized [`AccountCredentials`] to
42/// [`AccountBuilder::from_credentials()`].
43///
44/// The [`Account`] type is cheap to clone.
45///
46/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2>
47#[derive(Clone)]
48pub struct Account {
49    inner: Arc<AccountInner>,
50}
51
52impl Account {
53    /// Create an account builder with the default HTTP client
54    #[cfg(feature = "hyper-rustls")]
55    pub fn builder() -> Result<AccountBuilder, Error> {
56        Ok(AccountBuilder {
57            http: Box::new(DefaultClient::try_new()?),
58        })
59    }
60
61    /// Create an account builder with an HTTP client configured using a custom root CA
62    ///
63    /// This is useful if your ACME server uses a testing PKI and not a certificate
64    /// chain issued by a publicly trusted CA.
65    #[cfg(feature = "hyper-rustls")]
66    pub fn builder_with_root(pem_path: impl AsRef<Path>) -> Result<AccountBuilder, Error> {
67        let root_der = match CertificateDer::from_pem_file(pem_path) {
68            Ok(root_der) => root_der,
69            Err(err) => return Err(Error::Other(err.into())),
70        };
71
72        let mut roots = RootCertStore::empty();
73        match roots.add(root_der) {
74            Ok(()) => Ok(AccountBuilder {
75                http: Box::new(DefaultClient::with_roots(roots)?),
76            }),
77            Err(err) => Err(Error::Other(err.into())),
78        }
79    }
80
81    /// Create an account builder with the given HTTP client
82    pub fn builder_with_http(http: Box<dyn HttpClient>) -> AccountBuilder {
83        AccountBuilder { http }
84    }
85
86    /// Create a new order based on the given [`NewOrder`]
87    ///
88    /// Returns an [`Order`] instance. Use the [`Order::state()`] method to inspect its state.
89    pub async fn new_order(&self, order: &NewOrder<'_>) -> Result<Order, Error> {
90        if order.replaces.is_some() && self.inner.client.directory.renewal_info.is_none() {
91            return Err(Error::Unsupported("ACME renewal information (ARI)"));
92        }
93
94        let rsp = self
95            .inner
96            .post(Some(order), None, &self.inner.client.directory.new_order)
97            .await?;
98
99        let nonce = nonce_from_response(&rsp);
100        let order_url = rsp
101            .parts
102            .headers
103            .get(LOCATION)
104            .and_then(|hv| hv.to_str().ok())
105            .map(|s| s.to_owned());
106
107        // We return errors from Problem::check before emitting an error for any further
108        // issues (e.g. no order URL, missing replacement field).
109        let state = Problem::check::<OrderState>(rsp).await?;
110
111        // Per the ARI spec:
112        // "If the Server accepts a new-order request with a "replaces" field, it MUST reflect
113        // that field in the response and in subsequent requests for the corresponding Order
114        // object."
115        if order.replaces.is_some() && order.replaces != state.replaces {
116            return Err(Error::Other(
117                format!(
118                    "replaces field mismatch: expected {expected:?}, found {found:?}",
119                    expected = order.replaces,
120                    found = state.replaces,
121                )
122                .into(),
123            ));
124        }
125
126        Ok(Order {
127            account: self.inner.clone(),
128            nonce,
129            retry_after: None,
130            state,
131            url: order_url.ok_or("no order URL found")?,
132        })
133    }
134
135    /// Fetch the order state for an existing order based on the given `url`
136    ///
137    /// This might fail if the given URL's order belongs to a different account.
138    ///
139    /// Returns an [`Order`] instance. Use the [`Order::state`] method to inspect its state.
140    pub async fn order(&self, url: String) -> Result<Order, Error> {
141        let rsp = self.inner.post(None::<&Empty>, None, &url).await?;
142        Ok(Order {
143            account: self.inner.clone(),
144            nonce: nonce_from_response(&rsp),
145            retry_after: None,
146            // Order of fields matters! We return errors from Problem::check
147            // before emitting an error if there is no order url. Or the
148            // simple no url error hides the causing error in `Problem::check`.
149            state: Problem::check::<OrderState>(rsp).await?,
150            url,
151        })
152    }
153
154    /// Revokes a previously issued certificate
155    pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
156        let revoke_url = match self.inner.client.directory.revoke_cert.as_deref() {
157            Some(url) => url,
158            // This happens because the current account credentials were deserialized from an
159            // older version which only serialized a subset of the directory URLs. You should
160            // make sure the account credentials include a `directory` field containing a
161            // string with the server's directory URL.
162            None => return Err("no revokeCert URL found".into()),
163        };
164
165        let rsp = self.inner.post(Some(payload), None, revoke_url).await?;
166        // The body is empty if the request was successful
167        let _ = Problem::from_response(rsp).await?;
168        Ok(())
169    }
170
171    /// Fetch `RenewalInfo` with a suggested window for renewing an identified certificate
172    ///
173    /// Clients may use this information to determine when to renew a certificate. If the renewal
174    /// window starts in the past, then renewal should be attempted immediately. Otherwise, a
175    /// uniformly random point between the window start/end should be selected and used to
176    /// schedule a renewal in the future.
177    ///
178    /// The `Duration` is a hint from the ACME server when to again fetch the renewal window.
179    /// See <https://www.rfc-editor.org/rfc/rfc9773.html#section-4.3.2> for details.
180    ///
181    /// This is only supported by some ACME servers. If the server does not support this feature,
182    /// this method will return `Error::Unsupported`.
183    ///
184    /// See <https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2-4> for more information.
185    #[cfg(feature = "time")]
186    pub async fn renewal_info(
187        &self,
188        certificate_id: &CertificateIdentifier<'_>,
189    ) -> Result<(RenewalInfo, Duration), Error> {
190        let renewal_info_url = match self.inner.client.directory.renewal_info.as_deref() {
191            Some(url) => url,
192            None => return Err(Error::Unsupported("ACME renewal information (ARI)")),
193        };
194
195        // Note: unlike other ACME endpoints, the renewal info endpoint does not require a nonce
196        // or any JWS authentication. It's just a Plain-Old-HTTP-GET.
197        let request = Request::builder()
198            .method(Method::GET)
199            .uri(format!("{renewal_info_url}/{certificate_id}"))
200            .header(USER_AGENT, CRATE_USER_AGENT)
201            .body(BodyWrapper::default())?;
202
203        let rsp = self.inner.client.http.request(request).await?;
204        let Some(retry_after) = retry_after(&rsp) else {
205            return Err(Error::Str("missing Retry-After header"));
206        };
207
208        let delay = retry_after
209            .duration_since(SystemTime::now())
210            .unwrap_or(Duration::ZERO);
211
212        Ok((Problem::check::<RenewalInfo>(rsp).await?, delay))
213    }
214
215    /// Update the account's authentication key
216    ///
217    /// This is useful if you want to change the ACME account key of an existing account, e.g.
218    /// to mitigate the risk of a key compromise. This method creates a new client key and changes
219    /// the key associated with the existing account. `self` will be updated with the new key,
220    /// and a fresh set of [`AccountCredentials`] will be returned to update stored credentials.
221    ///
222    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5> for more information.
223    pub async fn update_key(&mut self) -> Result<AccountCredentials, Error> {
224        let new_key_url = match self.inner.client.directory.key_change.as_deref() {
225            Some(url) => url,
226            None => return Err("Account key rollover not supported by ACME CA".into()),
227        };
228
229        #[derive(Debug, Serialize)]
230        struct NewKey<'a> {
231            account: &'a str,
232            #[serde(rename = "oldKey")]
233            old_key: Jwk,
234        }
235
236        let (new_key, new_key_pkcs8) = Key::generate_pkcs8()?;
237        let mut header = new_key.header(Some("nonce"), new_key_url);
238        header.nonce = None;
239        let payload = NewKey {
240            account: &self.inner.id,
241            old_key: Jwk::new(&self.inner.key.inner),
242        };
243
244        let body = JoseJson::new(Some(&payload), header, &new_key)?;
245        let rsp = self.inner.post(Some(&body), None, new_key_url).await?;
246        let _ = Problem::from_response(rsp).await?;
247
248        self.inner = Arc::new(AccountInner {
249            client: self.inner.client.clone(),
250            key: new_key,
251            id: self.inner.id.clone(),
252        });
253
254        let (directory, urls) = match &self.inner.client.directory_url {
255            Some(directory_url) => (Some(directory_url.clone()), None),
256            None => (None, Some(self.inner.client.directory.clone())),
257        };
258
259        Ok(AccountCredentials {
260            id: self.inner.id.clone(),
261            key_pkcs8: new_key_pkcs8,
262            directory,
263            urls,
264        })
265    }
266
267    /// Updates the account contacts
268    ///
269    /// This is useful if you want to update the contact information of an existing account
270    /// on the ACME server. The contacts argument replaces existing contacts on
271    /// the server. By providing an empty array the contacts are removed from the server.
272    ///
273    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2> for more information.
274    pub async fn update_contacts<'a>(&self, contacts: &'a [&'a str]) -> Result<(), Error> {
275        #[derive(Debug, Serialize)]
276        struct Contacts<'a> {
277            contact: &'a [&'a str],
278        }
279
280        let payload = Contacts { contact: contacts };
281        let rsp = self
282            .inner
283            .post(Some(&payload), None, &self.inner.id)
284            .await?;
285
286        #[derive(Debug, Deserialize)]
287        struct Account {
288            status: AuthorizationStatus,
289        }
290
291        let response = Problem::check::<Account>(rsp).await?;
292        match response.status {
293            AuthorizationStatus::Valid => Ok(()),
294            _ => Err("Unexpected account status after updating contact information".into()),
295        }
296    }
297
298    /// Deactivate the account with the ACME server
299    ///
300    /// This is useful when you want to cancel an account with the ACME server
301    /// because you don't intend to use it further, or because the account key was
302    /// compromised.
303    ///
304    /// After this point no further operations can be performed with the account.
305    /// Any existing orders or authorizations created with the ACME server will be
306    /// invalidated.
307    pub async fn deactivate(self) -> Result<(), Error> {
308        #[derive(Serialize)]
309        struct DeactivateRequest<'a> {
310            status: &'a str,
311        }
312
313        let _ = self
314            .inner
315            .post(
316                Some(&DeactivateRequest {
317                    status: "deactivated",
318                }),
319                None,
320                self.id(),
321            )
322            .await?;
323
324        Ok(())
325    }
326
327    /// Yield the profiles supported according to the account's server directory
328    pub fn profiles(&self) -> impl Iterator<Item = ProfileMeta<'_>> {
329        self.inner
330            .client
331            .directory
332            .meta
333            .profiles
334            .iter()
335            .map(|(name, description)| ProfileMeta { name, description })
336    }
337
338    /// Get the account ID
339    pub fn id(&self) -> &str {
340        &self.inner.id
341    }
342
343    /// Get the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) account key thumbprint
344    pub fn key_thumbprint(&self) -> &str {
345        &self.inner.key.thumb
346    }
347}
348
349pub(crate) struct AccountInner {
350    client: Arc<Client>,
351    pub(crate) key: Key,
352    id: String,
353}
354
355impl AccountInner {
356    async fn from_credentials(
357        credentials: AccountCredentials,
358        http: Box<dyn HttpClient>,
359    ) -> Result<Self, Error> {
360        Ok(Self {
361            id: credentials.id,
362            key: Key::from_pkcs8_der(credentials.key_pkcs8)?,
363            client: Arc::new(match (credentials.directory, credentials.urls) {
364                (Some(directory_url), _) => Client::new(directory_url, http).await?,
365                (None, Some(directory)) => Client {
366                    http,
367                    directory,
368                    directory_url: None,
369                },
370                (None, None) => return Err("no server URLs found".into()),
371            }),
372        })
373    }
374
375    pub(crate) async fn get<T: DeserializeOwned>(
376        &self,
377        nonce: &mut Option<String>,
378        url: &str,
379    ) -> Result<T, Error> {
380        let rsp = self.post(None::<&Empty>, nonce.take(), url).await?;
381        *nonce = nonce_from_response(&rsp);
382        Problem::check(rsp).await
383    }
384
385    pub(crate) async fn post(
386        &self,
387        payload: Option<&impl Serialize>,
388        nonce: Option<String>,
389        url: &str,
390    ) -> Result<BytesResponse, Error> {
391        self.client.post(payload, nonce, self, url).await
392    }
393}
394
395impl Signer for AccountInner {
396    type Signature = <Key as Signer>::Signature;
397
398    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
399        debug_assert!(nonce.is_some());
400        Header {
401            alg: self.key.signing_algorithm,
402            key: KeyOrKeyId::KeyId(&self.id),
403            nonce,
404            url,
405        }
406    }
407
408    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
409        self.key.sign(payload)
410    }
411}
412
413/// Builder for `Account` values
414///
415/// Create one via [`Account::builder()`] or [`Account::builder_with_http()`].
416pub struct AccountBuilder {
417    http: Box<dyn HttpClient>,
418}
419
420impl AccountBuilder {
421    /// Restore an existing account from the given credentials
422    ///
423    /// The [`AccountCredentials`] type is opaque, but supports deserialization.
424    #[allow(clippy::wrong_self_convention)]
425    pub async fn from_credentials(self, credentials: AccountCredentials) -> Result<Account, Error> {
426        Ok(Account {
427            inner: Arc::new(AccountInner::from_credentials(credentials, self.http).await?),
428        })
429    }
430
431    /// Create a new account on the `directory_url` with the information in [`NewAccount`]
432    ///
433    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
434    /// Use [`AccountBuilder::from_credentials()`] to restore the account from the credentials.
435    pub async fn create(
436        self,
437        account: &NewAccount<'_>,
438        directory_url: String,
439        external_account: Option<&ExternalAccountKey>,
440    ) -> Result<(Account, AccountCredentials), Error> {
441        let (key, key_pkcs8) = Key::generate_pkcs8()?;
442        Self::create_inner(
443            account,
444            (key, key_pkcs8),
445            external_account,
446            Client::new(directory_url, self.http).await?,
447        )
448        .await
449    }
450
451    /// Load an existing account for the given private key
452    ///
453    /// The returned [`AccountCredentials`] can be serialized and stored for later use.
454    /// Use [`AccountBuilder::from_credentials()`] to restore the account from the credentials.
455    ///
456    /// Yields an error if no account matching the given key exists on the server.
457    pub async fn from_key(
458        self,
459        key: (Key, PrivateKeyDer<'static>),
460        directory_url: String,
461    ) -> Result<(Account, AccountCredentials), Error> {
462        Self::create_inner(
463            &NewAccount {
464                contact: &[],
465                terms_of_service_agreed: true,
466                only_return_existing: true,
467            },
468            match key {
469                (key, PrivateKeyDer::Pkcs8(pkcs8)) => (key, pkcs8),
470                _ => return Err("unsupported key format, expected PKCS#8".into()),
471            },
472            None,
473            Client::new(directory_url, self.http).await?,
474        )
475        .await
476    }
477
478    /// Restore an existing account from the given ID, private key, server URL and HTTP client
479    ///
480    /// The key must be provided in DER-encoded PKCS#8. This is usually how ECDSA keys are
481    /// encoded in PEM files. Use a crate like rustls-pemfile to decode from PEM to DER.
482    #[allow(clippy::wrong_self_convention)]
483    pub async fn from_parts(
484        self,
485        id: String,
486        key_pkcs8_der: PrivatePkcs8KeyDer<'_>,
487        directory_url: String,
488    ) -> Result<Account, Error> {
489        Ok(Account {
490            inner: Arc::new(AccountInner {
491                id,
492                key: Key::from_pkcs8_der(key_pkcs8_der)?,
493                client: Arc::new(Client::new(directory_url, self.http).await?),
494            }),
495        })
496    }
497
498    async fn create_inner(
499        account: &NewAccount<'_>,
500        (key, key_pkcs8): (Key, PrivatePkcs8KeyDer<'static>),
501        external_account: Option<&ExternalAccountKey>,
502        client: Client,
503    ) -> Result<(Account, AccountCredentials), Error> {
504        let payload = NewAccountPayload {
505            new_account: account,
506            external_account_binding: external_account
507                .map(|eak| {
508                    JoseJson::new(
509                        Some(&Jwk::new(&key.inner)),
510                        eak.header(None, &client.directory.new_account),
511                        eak,
512                    )
513                })
514                .transpose()?,
515        };
516
517        let rsp = client
518            .post(Some(&payload), None, &key, &client.directory.new_account)
519            .await?;
520
521        let account_url = rsp
522            .parts
523            .headers
524            .get(LOCATION)
525            .and_then(|hv| hv.to_str().ok())
526            .map(|s| s.to_owned());
527
528        // The response redirects, we don't need the body
529        let _ = Problem::from_response(rsp).await?;
530        let id = account_url.ok_or("failed to get account URL")?;
531        let credentials = AccountCredentials {
532            id: id.clone(),
533            key_pkcs8,
534            directory: Some(client.directory_url.clone().unwrap()), // New clients always have `directory_url`
535            // We support deserializing URLs for compatibility with versions pre 0.4,
536            // but we prefer to get fresh URLs from the `directory_url` for newer credentials.
537            urls: None,
538        };
539
540        let account = AccountInner {
541            client: Arc::new(client),
542            key,
543            id: id.clone(),
544        };
545
546        Ok((
547            Account {
548                inner: Arc::new(account),
549            },
550            credentials,
551        ))
552    }
553}
554
555/// Private account key used to sign requests
556pub struct Key {
557    rng: crypto::SystemRandom,
558    signing_algorithm: SigningAlgorithm,
559    inner: crypto::EcdsaKeyPair,
560    pub(crate) thumb: String,
561}
562
563impl Key {
564    /// Generate a new ECDSA P-256 key pair
565    #[deprecated(since = "0.8.3", note = "use `generate_pkcs8()` instead")]
566    pub fn generate() -> Result<(Self, PrivateKeyDer<'static>), Error> {
567        let (key, pkcs8) = Self::generate_pkcs8()?;
568        Ok((key, PrivateKeyDer::Pkcs8(pkcs8)))
569    }
570
571    /// Generate a new ECDSA P-256 key pair
572    pub fn generate_pkcs8() -> Result<(Self, PrivatePkcs8KeyDer<'static>), Error> {
573        let rng = crypto::SystemRandom::new();
574        let pkcs8 =
575            crypto::EcdsaKeyPair::generate_pkcs8(&crypto::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
576                .map_err(|_| Error::Crypto)?;
577        Ok((
578            Self::new(pkcs8.as_ref(), rng)?,
579            PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec()),
580        ))
581    }
582
583    /// Create a new key from the given PKCS#8 DER-encoded private key
584    ///
585    /// Currently, only ECDSA P-256 keys are supported.
586    pub fn from_pkcs8_der(pkcs8_der: PrivatePkcs8KeyDer<'_>) -> Result<Self, Error> {
587        Self::new(pkcs8_der.secret_pkcs8_der(), crypto::SystemRandom::new())
588    }
589
590    fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result<Self, Error> {
591        let inner = crypto::p256_key_pair_from_pkcs8(pkcs8_der, &rng)?;
592        let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&inner)?);
593        Ok(Self {
594            rng,
595            signing_algorithm: SigningAlgorithm::Es256,
596            inner,
597            thumb,
598        })
599    }
600}
601
602impl Signer for Key {
603    type Signature = crypto::Signature;
604
605    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
606        debug_assert!(nonce.is_some());
607        Header {
608            alg: self.signing_algorithm,
609            key: KeyOrKeyId::from_key(&self.inner),
610            nonce,
611            url,
612        }
613    }
614
615    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
616        self.inner
617            .sign(&self.rng, payload)
618            .map_err(|_| Error::Crypto)
619    }
620}
621
622/// A HMAC key used to link account creation requests to an external account
623///
624/// See RFC 8555 section 7.3.4 for more information.
625pub struct ExternalAccountKey {
626    id: String,
627    key: crypto::hmac::Key,
628}
629
630impl ExternalAccountKey {
631    /// Create a new external account key
632    ///
633    /// Note that the `key_value` argument represents the raw key value, so if the caller holds
634    /// an encoded key value (for example, using base64), decode it before passing it in.
635    pub fn new(id: String, key_value: &[u8]) -> Self {
636        Self {
637            id,
638            key: crypto::hmac::Key::new(crypto::hmac::HMAC_SHA256, key_value),
639        }
640    }
641}
642
643impl Signer for ExternalAccountKey {
644    type Signature = crypto::hmac::Tag;
645
646    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
647        debug_assert_eq!(nonce, None);
648        Header {
649            alg: SigningAlgorithm::Hs256,
650            key: KeyOrKeyId::KeyId(&self.id),
651            nonce,
652            url,
653        }
654    }
655
656    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
657        Ok(crypto::hmac::sign(&self.key, payload))
658    }
659}