acme_lite/acc/
mod.rs

1use std::sync::Arc;
2
3use base64::prelude::*;
4use zeroize::Zeroizing;
5
6use crate::{
7    api::{ApiAccount, ApiDirectory, ApiIdentifier, ApiOrder, ApiRevocation},
8    cert::Certificate,
9    order::{NewOrder, Order},
10    req::req_expect_header,
11    trans::Transport,
12};
13
14mod acme_key;
15
16pub(crate) use self::acme_key::AcmeKey;
17
18#[derive(Clone, Debug)]
19pub(crate) struct AccountInner {
20    pub transport: Transport,
21    pub api_account: ApiAccount,
22    pub api_directory: ApiDirectory,
23}
24
25/// Account with an ACME provider.
26///
27/// Accounts are created using [`Directory::account`] and consist of a contact
28/// email address and a private key for signing requests to the ACME API.
29///
30/// acme-lib uses elliptic curve P-256 for accessing the account. This
31/// does not affect which key algorithms that can be used for the
32/// issued certificates.
33///
34/// The advantage of using elliptic curve cryptography is that the signed
35/// requests against the ACME lib are kept small and that the public key
36/// can be derived from the private.
37///
38/// [`Directory::account`]: struct.Directory.html#method.account
39#[derive(Clone)]
40pub struct Account {
41    inner: Arc<AccountInner>,
42}
43
44impl Account {
45    pub(crate) fn new(
46        transport: Transport,
47        api_account: ApiAccount,
48        api_directory: ApiDirectory,
49    ) -> Self {
50        Account {
51            inner: Arc::new(AccountInner {
52                transport,
53                api_account,
54                api_directory,
55            }),
56        }
57    }
58
59    /// Signing key for this account.
60    ///
61    /// The key is an elliptic curve signing key.
62    pub fn acme_signing_key_pem(&self) -> eyre::Result<Zeroizing<String>> {
63        self.inner.transport.acme_key().to_pem()
64    }
65
66    /// Create a new order to issue a certificate for this account.
67    ///
68    /// Each order has a required `primary_name` (which will be set as the certificates `CN`)
69    /// and a variable number of `alt_names`.
70    ///
71    /// This library doesn't constrain the number of `alt_names`, but it is limited by the ACME
72    /// API provider. Let's Encrypt [sets a max of 100 names] per certificate.
73    ///
74    /// Every call creates a new order with the ACME API provider, even when the domain
75    /// names supplied are exactly the same.
76    ///
77    /// [sets a max of 100 names]: https://letsencrypt.org/docs/rate-limits/
78    pub async fn new_order(
79        &self,
80        primary_name: &str,
81        alt_names: &[&str],
82    ) -> eyre::Result<NewOrder> {
83        // construct the identifiers
84        let prim_arr = [primary_name];
85        let domains = prim_arr.iter().chain(alt_names);
86        let identifiers = domains
87            .map(|&domain| ApiIdentifier {
88                _type: "dns".to_owned(),
89                value: domain.to_owned(),
90            })
91            .collect();
92        let order = ApiOrder {
93            identifiers,
94            ..Default::default()
95        };
96
97        let new_order_url = self.inner.api_directory.new_order.as_str();
98
99        let res = self.inner.transport.call(new_order_url, &order).await?;
100        let order_url = req_expect_header(&res, "location")?;
101        let api_order = res.json::<ApiOrder>().await?;
102
103        let order = Order::new(&self.inner, api_order, order_url);
104        Ok(NewOrder { order })
105    }
106
107    /// Revoke a certificate for the reason given.
108    pub async fn revoke_certificate(
109        &self,
110        cert: &Certificate,
111        reason: RevocationReason,
112    ) -> eyre::Result<()> {
113        // convert to base64url of the DER (which is not PEM).
114        let certificate = BASE64_URL_SAFE_NO_PAD.encode(cert.certificate_der()?);
115
116        let revoc = ApiRevocation {
117            certificate,
118            reason: reason as usize,
119        };
120
121        let url = &self.inner.api_directory.revoke_cert;
122        self.inner.transport.call(url, &revoc).await?;
123
124        Ok(())
125    }
126
127    /// Access the underlying JSON object for debugging.
128    pub fn api_account(&self) -> &ApiAccount {
129        &self.inner.api_account
130    }
131}
132
133/// Enumeration of reasons for revocation.
134///
135/// The reason codes are taken from [rfc5280](https://tools.ietf.org/html/rfc5280#section-5.3.1).
136pub enum RevocationReason {
137    Unspecified = 0,
138    KeyCompromise = 1,
139    CACompromise = 2,
140    AffiliationChanged = 3,
141    Superseded = 4,
142    CessationOfOperation = 5,
143    CertificateHold = 6,
144    // value 7 is not used
145    RemoveFromCRL = 8,
146    PrivilegeWithdrawn = 9,
147    AACompromise = 10,
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::{Directory, DirectoryUrl};
153
154    #[tokio::test]
155    async fn test_create_order() {
156        let server = crate::test::with_directory_server();
157
158        let url = DirectoryUrl::Other(&server.dir_url);
159        let dir = Directory::from_url(url).await.unwrap();
160
161        let acc = dir
162            .register_account(Some(vec!["mailto:foo@bar.com".to_owned()]))
163            .await
164            .unwrap();
165
166        let _order = acc.new_order("acmetest.example.com", &[]).await.unwrap();
167    }
168}