ferron/setup/
acme.rs

1use std::{
2  collections::{HashMap, HashSet},
3  error::Error,
4  path::PathBuf,
5  str::FromStr,
6  sync::Arc,
7  time::Duration,
8};
9
10use base64::Engine;
11use instant_acme::{ExternalAccountKey, LetsEncrypt};
12use rustls::{client::WebPkiServerVerifier, crypto::CryptoProvider, ClientConfig};
13use rustls_platform_verifier::BuilderVerifierExt;
14use tokio::sync::RwLock;
15use xxhash_rust::xxh3::xxh3_128;
16
17use crate::acme::{
18  add_domain_to_cache, convert_on_demand_config, provision_certificate, AcmeConfig, AcmeOnDemandConfig,
19};
20use ferron_common::{get_entry, get_value, util::match_hostname};
21use ferron_common::{logging::ErrorLogger, util::NoServerVerifier};
22
23/// Builds a Rustls client configuration for ACME.
24pub fn build_rustls_client_config(
25  server_configuration: &ferron_common::config::ServerConfiguration,
26  crypto_provider: Arc<CryptoProvider>,
27) -> Result<ClientConfig, Box<dyn Error + Send + Sync>> {
28  build_raw_rustls_client_config(
29    get_value!("auto_tls_no_verification", server_configuration)
30      .and_then(|v| v.as_bool())
31      .unwrap_or(false),
32    crypto_provider,
33  )
34}
35
36/// Builds a raw Rustls client configuration for ACME.
37fn build_raw_rustls_client_config(
38  no_verification: bool,
39  crypto_provider: Arc<CryptoProvider>,
40) -> Result<ClientConfig, Box<dyn Error + Send + Sync>> {
41  Ok(
42    (if no_verification {
43      ClientConfig::builder_with_provider(crypto_provider.clone())
44        .with_safe_default_protocol_versions()?
45        .dangerous()
46        .with_custom_certificate_verifier(Arc::new(NoServerVerifier::new()))
47    } else if let Ok(client_config) = BuilderVerifierExt::with_platform_verifier(
48      ClientConfig::builder_with_provider(crypto_provider.clone()).with_safe_default_protocol_versions()?,
49    ) {
50      client_config
51    } else {
52      ClientConfig::builder_with_provider(crypto_provider.clone())
53        .with_safe_default_protocol_versions()?
54        .with_webpki_verifier(
55          WebPkiServerVerifier::builder(Arc::new(rustls::RootCertStore {
56            roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
57          }))
58          .build()?,
59        )
60    })
61    .with_no_client_auth(),
62  )
63}
64
65/// Resolves the ACME directory URL based on the server configuration.
66pub fn resolve_acme_directory(server_configuration: &ferron_common::config::ServerConfiguration) -> String {
67  if let Some(directory) = get_value!("auto_tls_directory", server_configuration).and_then(|v| v.as_str()) {
68    directory.to_string()
69  } else if get_value!("auto_tls_letsencrypt_production", server_configuration)
70    .and_then(|v| v.as_bool())
71    .unwrap_or(true)
72  {
73    LetsEncrypt::Production.url().to_string()
74  } else {
75    LetsEncrypt::Staging.url().to_string()
76  }
77}
78
79/// Parses the External Account Binding (EAB) key and secret from the server configuration.
80pub fn parse_eab(
81  server_configuration: &ferron_common::config::ServerConfiguration,
82) -> Result<Option<Arc<ExternalAccountKey>>, anyhow::Error> {
83  Ok(
84    if let Some((Some(eab_key_id), Some(eab_key_hmac))) =
85      get_entry!("auto_tls_eab", server_configuration).map(|entry| {
86        (
87          entry.values.first().and_then(|v| v.as_str()),
88          entry.values.get(1).and_then(|v| v.as_str()),
89        )
90      })
91    {
92      match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(eab_key_hmac.trim_end_matches('=')) {
93        Ok(decoded_key) => Some(Arc::new(ExternalAccountKey::new(eab_key_id.to_string(), &decoded_key))),
94        Err(err) => Err(anyhow::anyhow!("Failed to decode EAB key HMAC: {}", err))?,
95      }
96    } else {
97      None
98    },
99  )
100}
101
102pub fn resolve_acme_cache_path(
103  server_configuration: &ferron_common::config::ServerConfiguration,
104) -> Result<Option<PathBuf>, anyhow::Error> {
105  let acme_default_directory = dirs::data_local_dir().and_then(|mut p| {
106    p.push("ferron-acme");
107    p.into_os_string().into_string().ok()
108  });
109  Ok(
110    if let Some(acme_cache_path_str) =
111      get_value!("auto_tls_cache", server_configuration).map_or(acme_default_directory.as_deref(), |v| {
112        if v.is_null() {
113          None
114        } else if let Some(v) = v.as_str() {
115          Some(v)
116        } else {
117          acme_default_directory.as_deref()
118        }
119      })
120    {
121      Some(PathBuf::from_str(acme_cache_path_str).map_err(|_| anyhow::anyhow!("Invalid ACME cache path"))?)
122    } else {
123      None
124    },
125  )
126}
127
128/// Resolves the paths to account and certificate caches.
129pub fn resolve_cache_paths(
130  server_configuration: &ferron_common::config::ServerConfiguration,
131  port: u16,
132  sni_hostname: &str,
133) -> Result<(Option<PathBuf>, Option<PathBuf>), anyhow::Error> {
134  let acme_cache_path_option = resolve_acme_cache_path(server_configuration)?;
135  let (account_cache_path, cert_cache_path) = if let Some(mut pathbuf) = acme_cache_path_option {
136    let base_pathbuf = pathbuf.clone();
137    let append_hash = base64::engine::general_purpose::URL_SAFE_NO_PAD
138      .encode(xxh3_128(format!("{port}-{sni_hostname}").as_bytes()).to_be_bytes());
139    pathbuf.push(append_hash);
140    (Some(base_pathbuf), Some(pathbuf))
141  } else {
142    (None, None)
143  };
144  Ok((account_cache_path, cert_cache_path))
145}
146
147/// Performs background automatic TLS tasks.
148#[allow(clippy::too_many_arguments)]
149pub async fn background_acme_task(
150  acme_configs: Vec<AcmeConfig>,
151  acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
152  memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
153  acme_on_demand_rx: async_channel::Receiver<(String, u16)>,
154  on_demand_tls_ask_endpoint: Option<hyper::Uri>,
155  on_demand_tls_ask_endpoint_verify: bool,
156  acme_logger: ErrorLogger,
157  crypto_provider: Arc<CryptoProvider>,
158  existing_combinations: HashSet<(String, u16)>,
159) {
160  let acme_logger = Arc::new(acme_logger);
161
162  // Wrap ACME configurations in a mutex
163  let acme_configs_mutex = Arc::new(tokio::sync::Mutex::new(acme_configs));
164
165  let prevent_file_race_conditions_sem = Arc::new(tokio::sync::Semaphore::new(1));
166
167  if !acme_on_demand_configs.is_empty() {
168    // On-demand TLS
169    tokio::spawn(background_on_demand_acme_task(
170      existing_combinations,
171      acme_on_demand_rx,
172      on_demand_tls_ask_endpoint,
173      on_demand_tls_ask_endpoint_verify,
174      acme_logger.clone(),
175      crypto_provider,
176      acme_configs_mutex.clone(),
177      acme_on_demand_configs,
178      memory_acme_account_cache_data,
179      prevent_file_race_conditions_sem,
180    ));
181  }
182
183  loop {
184    for acme_config in &mut *acme_configs_mutex.lock().await {
185      if let Err(acme_error) = provision_certificate(acme_config, &acme_logger).await {
186        acme_logger
187          .log(&format!("Error while obtaining a TLS certificate: {acme_error}"))
188          .await
189      }
190    }
191    tokio::time::sleep(Duration::from_secs(10)).await;
192  }
193}
194
195/// Performs background automatic TLS on demand tasks.
196#[allow(clippy::too_many_arguments)]
197#[inline]
198pub async fn background_on_demand_acme_task(
199  existing_combinations: HashSet<(String, u16)>,
200  acme_on_demand_rx: async_channel::Receiver<(String, u16)>,
201  on_demand_tls_ask_endpoint: Option<hyper::Uri>,
202  on_demand_tls_ask_endpoint_verify: bool,
203  acme_logger: Arc<ErrorLogger>,
204  crypto_provider: Arc<CryptoProvider>,
205  acme_configs_mutex: Arc<tokio::sync::Mutex<Vec<AcmeConfig>>>,
206  acme_on_demand_configs: Vec<AcmeOnDemandConfig>,
207  memory_acme_account_cache_data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
208  prevent_file_race_conditions_sem: Arc<tokio::sync::Semaphore>,
209) {
210  let acme_on_demand_configs = Arc::new(acme_on_demand_configs);
211  let mut existing_combinations = existing_combinations;
212  while let Ok(received_data) = acme_on_demand_rx.recv().await {
213    let on_demand_tls_ask_endpoint = on_demand_tls_ask_endpoint.clone();
214    if let Some(on_demand_tls_ask_endpoint) = on_demand_tls_ask_endpoint {
215      let mut url_parts = on_demand_tls_ask_endpoint.into_parts();
216      let path_and_query_str = if let Some(path_and_query) = url_parts.path_and_query {
217        let query = path_and_query.query();
218        let query = if let Some(query) = query {
219          format!("{}&domain={}", query, urlencoding::encode(&received_data.0))
220        } else {
221          format!("domain={}", urlencoding::encode(&received_data.0))
222        };
223        format!("{}?{}", path_and_query.path(), query)
224      } else {
225        format!("/?domain={}", urlencoding::encode(&received_data.0))
226      };
227      url_parts.path_and_query = Some(match path_and_query_str.parse() {
228        Ok(parsed) => parsed,
229        Err(err) => {
230          acme_logger
231            .log(&format!(
232              "Error while formatting the URL for on-demand TLS request: {err}"
233            ))
234            .await;
235          continue;
236        }
237      });
238      let endpoint_url = match hyper::Uri::from_parts(url_parts) {
239        Ok(parsed) => parsed,
240        Err(err) => {
241          acme_logger
242            .log(&format!(
243              "Error while formatting the URL for on-demand TLS request: {err}"
244            ))
245            .await;
246          continue;
247        }
248      };
249      let crypto_provider = crypto_provider.clone();
250      let ask_closure = async {
251        let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
252          .build::<_, http_body_util::Empty<hyper::body::Bytes>>(
253          hyper_rustls::HttpsConnectorBuilder::new()
254            .with_tls_config(build_raw_rustls_client_config(
255              !on_demand_tls_ask_endpoint_verify,
256              crypto_provider,
257            )?)
258            .https_or_http()
259            .enable_http1()
260            .enable_http2()
261            .build(),
262        );
263        let request = hyper::Request::builder()
264          .method(hyper::Method::GET)
265          .uri(endpoint_url)
266          .body(http_body_util::Empty::<hyper::body::Bytes>::new())?;
267        let response = client.request(request).await?;
268
269        Ok::<_, Box<dyn Error + Send + Sync>>(response.status().is_success())
270      };
271      match ask_closure.await {
272        Ok(true) => (),
273        Ok(false) => {
274          acme_logger
275            .log(&format!(
276              "The TLS certificate cannot be issued for \"{}\" hostname",
277              &received_data.0
278            ))
279            .await;
280          continue;
281        }
282        Err(err) => {
283          acme_logger
284            .log(&format!(
285              "Error while determining if the TLS certificate can be issued for \"{}\" hostname: {err}",
286              &received_data.0
287            ))
288            .await;
289          continue;
290        }
291      }
292    }
293    if existing_combinations.contains(&received_data) {
294      continue;
295    } else {
296      existing_combinations.insert(received_data.clone());
297    }
298    let (sni_hostname, port) = received_data;
299    let acme_configs_mutex = acme_configs_mutex.clone();
300    let acme_on_demand_configs = acme_on_demand_configs.clone();
301    let memory_acme_account_cache_data = memory_acme_account_cache_data.clone();
302    let prevent_file_race_conditions_sem = prevent_file_race_conditions_sem.clone();
303    tokio::spawn(async move {
304      for acme_on_demand_config in acme_on_demand_configs.iter() {
305        if match_hostname(acme_on_demand_config.sni_hostname.as_deref(), Some(&sni_hostname))
306          && acme_on_demand_config.port == port
307        {
308          let sem_guard = prevent_file_race_conditions_sem.acquire().await;
309          add_domain_to_cache(acme_on_demand_config, &sni_hostname)
310            .await
311            .unwrap_or_default();
312          drop(sem_guard);
313
314          acme_configs_mutex.lock().await.push(
315            convert_on_demand_config(
316              acme_on_demand_config,
317              sni_hostname.clone(),
318              memory_acme_account_cache_data,
319            )
320            .await,
321          );
322          break;
323        }
324      }
325    });
326  }
327}