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}