acme/acc/
mod.rs

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