acme_lite/
dir.rs

1use std::sync::Arc;
2
3use crate::{
4    acc::AcmeKey,
5    api::{ApiAccount, ApiDirectory},
6    req::{req_expect_header, req_get, req_handle_error},
7    trans::{NoncePool, Transport},
8    Account,
9};
10
11const LETSENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
12const LETSENCRYPT_STAGING_URL: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
13
14/// Enumeration of known ACME API directories.
15#[derive(Debug, Clone)]
16pub enum DirectoryUrl<'a> {
17    /// The main Let's Encrypt directory.
18    ///
19    /// Not appropriate for testing / development.
20    LetsEncrypt,
21
22    /// The staging Let's Encrypt directory.
23    ///
24    /// Use for testing and development. Doesn't issue "valid" certificates. The root signing
25    /// certificate is not supposed to be in any trust chains.
26    LetsEncryptStaging,
27
28    /// Provide an arbitrary director URL to connect to.
29    Other(&'a str),
30}
31
32impl<'a> DirectoryUrl<'a> {
33    fn to_url(&self) -> &str {
34        match self {
35            DirectoryUrl::LetsEncrypt => LETSENCRYPT_URL,
36            DirectoryUrl::LetsEncryptStaging => LETSENCRYPT_STAGING_URL,
37            DirectoryUrl::Other(url) => url,
38        }
39    }
40}
41
42/// Entry point for accessing an ACME API.
43#[derive(Clone)]
44pub struct Directory {
45    nonce_pool: Arc<NoncePool>,
46    api_directory: ApiDirectory,
47}
48
49impl Directory {
50    /// Create a directory over a persistence implementation and directory url.
51    pub async fn from_url(url: DirectoryUrl<'_>) -> eyre::Result<Directory> {
52        let res = req_handle_error(req_get(url.to_url()).await).await?;
53        let api_directory = res.json::<ApiDirectory>().await?;
54        let nonce_pool = Arc::new(NoncePool::new(&api_directory.new_nonce));
55
56        Ok(Directory {
57            nonce_pool,
58            api_directory,
59        })
60    }
61
62    pub async fn register_account(&self, contact: Option<Vec<String>>) -> eyre::Result<Account> {
63        let acme_key = AcmeKey::new();
64        self.upsert_account(acme_key, contact).await
65    }
66
67    pub async fn load_account(
68        &self,
69        signing_key_pem: &str,
70        contact: Option<Vec<String>>,
71    ) -> eyre::Result<Account> {
72        let acme_key = AcmeKey::from_pem(signing_key_pem)?;
73        self.upsert_account(acme_key, contact).await
74    }
75
76    async fn upsert_account(
77        &self,
78        acme_key: AcmeKey,
79        contact: Option<Vec<String>>,
80    ) -> eyre::Result<Account> {
81        // Prepare making a call to newAccount. This is fine to do both for
82        // new keys and existing. For existing the spec says to return a 200
83        // with the Location header set to the key id (kid).
84        let acc = ApiAccount {
85            // TODO: ensure email contains no hfields or more than one addr-spec in the to component
86            // see https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
87            contact,
88            terms_of_service_agreed: Some(true),
89            ..Default::default()
90        };
91
92        let mut transport = Transport::new(Arc::clone(&self.nonce_pool), acme_key);
93        let res = transport
94            .call_jwk(&self.api_directory.new_account, &acc)
95            .await?;
96
97        let kid = req_expect_header(&res, "location")?;
98        log::debug!("Key ID is: {kid}");
99        let api_account = res.json::<ApiAccount>().await?;
100
101        // fill in the server returned key id
102        transport.set_key_id(kid);
103
104        // The finished account
105        Ok(Account::new(
106            transport,
107            api_account,
108            self.api_directory.clone(),
109        ))
110    }
111
112    /// Access the underlying JSON object for debugging.
113    pub fn api_directory(&self) -> &ApiDirectory {
114        &self.api_directory
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[tokio::test]
123    async fn test_create_directory() {
124        let server = crate::test::with_directory_server();
125
126        let url = DirectoryUrl::Other(&server.dir_url);
127        let _dir = Directory::from_url(url).await.unwrap();
128    }
129
130    #[tokio::test]
131    async fn test_create_account() {
132        let server = crate::test::with_directory_server();
133
134        let url = DirectoryUrl::Other(&server.dir_url);
135        let dir = Directory::from_url(url).await.unwrap();
136
137        let _acc = dir
138            .register_account(Some(vec!["mailto:foo@bar.com".to_owned()]))
139            .await
140            .unwrap();
141    }
142
143    // #[test]
144    // fn test_the_whole_hog() {
145    //     std::env::set_var("RUST_LOG", "acme_lite=trace");
146    //     let _ = env_logger::try_init();
147
148    //     use crate::cert::create_p384_key;
149
150    //     let url = DirectoryUrl::LetsEncryptStaging;
151    //     let persist = FilePersist::new(".");
152    //     let dir = Directory::from_url(persist, url)?;
153    //     let acc = dir.account("foo@bar.com")?;
154
155    //     let mut ord = acc.new_order("myspecialsite.com", &[])?;
156
157    //     let ord = loop {
158    //         if let Some(ord) = ord.confirm_validations() {
159    //             break ord;
160    //         }
161
162    //         let auths = ord.authorizations()?;
163    //         let chall = auths[0].dns_challenge();
164
165    //         info!("Proof: {}", chall.dns_proof());
166
167    //         use std::thread;
168    //         use std::time::Duration;
169    //         tokio::time::sleep(Duration::from_millis(60_000)).await;
170
171    //         chall.validate(5000)?;
172
173    //         ord.refresh()?;
174    //     };
175
176    //     let (pkey_pri, pkey_pub) = create_p384_key();
177
178    //     let ord = ord.finalize_signing_key(pkey_pri, pkey_pub, 5000)?;
179
180    //     let cert = ord.download_and_save_cert()?;
181    //     println!(
182    //         "{}{}{}",
183    //         cert.private_key(),
184    //         cert.certificate(),
185    //         cert.valid_days_left()
186    //     );
187    //     Ok(())
188    // }
189}