Now let's take a silly one
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 6.2 kB View raw
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(&params.cache_dir).map_err(|source| AcmeError::Cache { 94 path: params.cache_dir.display().to_string(), 95 source, 96 })?; 97 restrict_cache_dir(&params.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}