diff --git a/Cargo.lock b/Cargo.lock index a094835..c05bdc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2146,6 +2146,7 @@ dependencies = [ "tokio 1.14.0", "tokio-rustls", "tokio-util", + "uuid", ] [[package]] @@ -2233,6 +2234,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 9ed93c0..0d1374f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,4 @@ rcgen = "0.8" accept-encoding-fork = "0.2.0-alpha.3" async-compression = { version = "0.3", features = ["tokio", "gzip", "zstd", "deflate", "brotli"] } tokio-util = { version = "0.6", features = ["io"] } +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/src/cert_store.rs b/src/cert_store.rs index c272759..d561605 100644 --- a/src/cert_store.rs +++ b/src/cert_store.rs @@ -205,6 +205,12 @@ impl CertStore { bail!("Lock is already taken, not renewing for now."); } + // ---- Accessibility check ---- + // We don't want to ask Let's encrypt for a domain that + // is not configured to point here. This can happen with wildcards: someone can send + // a fake SNI to a domain that is not ours. We have to detect it here. + self.check_domain_accessibility(domain, &session).await?; + // ---- Do let's encrypt stuff ---- let dir = Directory::from_url(DirectoryUrl::LetsEncrypt)?; @@ -287,6 +293,46 @@ impl CertStore { Ok(()) } + async fn check_domain_accessibility(&self, domain: &str, session: &str) -> Result<()> { + // Returns Ok(()) only if domain is a correct domain name that + // redirects to this server + let self_challenge_id = uuid::Uuid::new_v4().to_string(); + let self_challenge_key = format!("challenge/{}", self_challenge_id); + let self_challenge_resp = uuid::Uuid::new_v4().to_string(); + + self.consul + .acquire( + &self_challenge_key, + self_challenge_resp.as_bytes().to_vec().into(), + session, + ) + .await?; + + let httpcli = reqwest::Client::new(); + let chall_url = format!( + "http://{}/.well-known/acme-challenge/{}", + domain, self_challenge_id + ); + + for i in 1..=4 { + tokio::time::sleep(Duration::from_secs(2)).await; + info!("({}) Accessibility check {}/4", domain, i); + + let httpresp = httpcli.get(&chall_url).send().await?; + if httpresp.status() == reqwest::StatusCode::OK + && httpresp.bytes().await? == self_challenge_resp.as_bytes() + { + // Challenge successfully validated + info!("({}) Accessibility check successfull", domain); + return Ok(()); + } + + tokio::time::sleep(Duration::from_secs(2)).await; + } + + bail!("Unable to validate self-challenge for domain accessibility check"); + } + fn gen_self_signed_certificate(&self, domain: &str) -> Result> { let subject_alt_names = vec![domain.to_string(), "localhost".to_string()]; let cert = rcgen::generate_simple_self_signed(subject_alt_names)?;