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