Now let's take a silly one
1use std::convert::Infallible;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use futures::StreamExt;
7use rustls::server::ResolvesServerCert;
8use rustls_acme::caches::DirCache;
9use rustls_acme::{AccountCache, AcmeConfig, CertCache};
10use tokio_util::sync::CancellationToken;
11
12#[derive(Debug, thiserror::Error)]
13pub enum AcmeError {
14 #[error("acme cache {path}: {source}")]
15 Cache {
16 path: String,
17 #[source]
18 source: std::io::Error,
19 },
20}
21
22struct RestrictedDirCache {
23 dir: PathBuf,
24 inner: DirCache<PathBuf>,
25}
26
27impl RestrictedDirCache {
28 fn new(dir: PathBuf) -> Self {
29 let inner = DirCache::new(dir.clone());
30 Self { dir, inner }
31 }
32}
33
34#[async_trait]
35impl CertCache for RestrictedDirCache {
36 type EC = std::io::Error;
37
38 async fn load_cert(
39 &self,
40 domains: &[String],
41 directory_url: &str,
42 ) -> Result<Option<Vec<u8>>, Self::EC> {
43 self.inner.load_cert(domains, directory_url).await
44 }
45
46 async fn store_cert(
47 &self,
48 domains: &[String],
49 directory_url: &str,
50 cert: &[u8],
51 ) -> Result<(), Self::EC> {
52 self.inner.store_cert(domains, directory_url, cert).await?;
53 restrict_cache_dir(&self.dir).map_err(std::io::Error::other)
54 }
55}
56
57#[async_trait]
58impl AccountCache for RestrictedDirCache {
59 type EA = std::io::Error;
60
61 async fn load_account(
62 &self,
63 contact: &[String],
64 directory_url: &str,
65 ) -> Result<Option<Vec<u8>>, Self::EA> {
66 self.inner.load_account(contact, directory_url).await
67 }
68
69 async fn store_account(
70 &self,
71 contact: &[String],
72 directory_url: &str,
73 account: &[u8],
74 ) -> Result<(), Self::EA> {
75 self.inner
76 .store_account(contact, directory_url, account)
77 .await?;
78 restrict_cache_dir(&self.dir).map_err(std::io::Error::other)
79 }
80}
81
82pub struct AcmeParams {
83 pub domains: Vec<String>,
84 pub contact: String,
85 pub cache_dir: PathBuf,
86 pub production: bool,
87}
88
89pub fn start(
90 params: AcmeParams,
91 shutdown: CancellationToken,
92) -> Result<Arc<dyn ResolvesServerCert>, AcmeError> {
93 std::fs::create_dir_all(¶ms.cache_dir).map_err(|source| AcmeError::Cache {
94 path: params.cache_dir.display().to_string(),
95 source,
96 })?;
97 restrict_cache_dir(¶ms.cache_dir)?;
98
99 let mut state = AcmeConfig::<Infallible, Infallible>::new(params.domains)
100 .contact_push(format!("mailto:{}", params.contact))
101 .cache(RestrictedDirCache::new(params.cache_dir))
102 .directory_lets_encrypt(params.production)
103 .state();
104 let resolver = state.resolver();
105
106 tokio::spawn(async move {
107 loop {
108 tokio::select! {
109 () = shutdown.cancelled() => break,
110 event = state.next() => match event {
111 Some(Ok(ok)) => tracing::info!("acme: {ok:?}"),
112 Some(Err(error)) => tracing::warn!("acme: {error:?}"),
113 None => {
114 tracing::warn!("acme renewal stream ended, certificates will no longer renew");
115 break;
116 }
117 },
118 }
119 }
120 });
121
122 Ok(resolver)
123}
124
125#[cfg(unix)]
126fn restrict_cache_dir(path: &std::path::Path) -> Result<(), AcmeError> {
127 use std::os::unix::fs::PermissionsExt;
128 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(|source| {
129 AcmeError::Cache {
130 path: path.display().to_string(),
131 source,
132 }
133 })?;
134 std::fs::read_dir(path)
135 .map_err(|source| AcmeError::Cache {
136 path: path.display().to_string(),
137 source,
138 })?
139 .filter_map(Result::ok)
140 .filter(|entry| {
141 entry
142 .file_type()
143 .map(|kind| kind.is_file())
144 .unwrap_or(false)
145 })
146 .try_for_each(|entry| {
147 std::fs::set_permissions(entry.path(), std::fs::Permissions::from_mode(0o600)).map_err(
148 |source| AcmeError::Cache {
149 path: entry.path().display().to_string(),
150 source,
151 },
152 )
153 })
154}
155
156#[cfg(not(unix))]
157fn restrict_cache_dir(_path: &std::path::Path) -> Result<(), AcmeError> {
158 Ok(())
159}
160
161#[cfg(all(test, unix))]
162mod tests {
163 use super::*;
164 use std::os::unix::fs::PermissionsExt;
165
166 #[test]
167 fn the_cache_dir_and_its_files_are_tightened_to_owner_only() {
168 let dir = tempfile::tempdir().unwrap();
169 let key = dir.path().join("account.key");
170 std::fs::write(&key, b"private material").unwrap();
171 std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o644)).unwrap();
172
173 restrict_cache_dir(dir.path()).unwrap();
174
175 assert_eq!(
176 std::fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777,
177 0o700,
178 "the cache directory must be traversable only by its owner"
179 );
180 assert_eq!(
181 std::fs::metadata(&key).unwrap().permissions().mode() & 0o777,
182 0o600,
183 "a cached private key must be readable only by its owner"
184 );
185 }
186
187 #[tokio::test]
188 async fn a_cert_stored_after_boot_is_tightened_to_owner_only() {
189 let dir = tempfile::tempdir().unwrap();
190 let cache = RestrictedDirCache::new(dir.path().to_path_buf());
191 cache
192 .store_cert(
193 &["anemone.knot".to_string()],
194 "https://acme.test/directory",
195 b"private cert material",
196 )
197 .await
198 .unwrap();
199
200 let modes: Vec<u32> = std::fs::read_dir(dir.path())
201 .unwrap()
202 .filter_map(Result::ok)
203 .map(|entry| {
204 std::fs::metadata(entry.path())
205 .unwrap()
206 .permissions()
207 .mode()
208 & 0o777
209 })
210 .collect();
211 assert_eq!(
212 modes,
213 vec![0o600],
214 "a certificate written after boot must be the only entry and owner-only"
215 );
216 }
217}