Skip to content

Commit

Permalink
add OCSP, more refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
blind-oracle committed May 23, 2024
1 parent a545b72 commit 6067fe9
Show file tree
Hide file tree
Showing 11 changed files with 678 additions and 128 deletions.
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ axum-server = { version = "0.6", features = ["tls-rustls"] }
backoff = { version = "0.4", features = ["tokio"] }
bytes = "1.5"
candid = "0.10"
chrono = "0.4"
clap = { version = "4.5", features = ["derive", "string"] }
clap_derive = "4.5"
clickhouse = { version = "0.11", features = ["uuid", "time"] }
ctrlc = { version = "3.4", features = ["termination"] }
# cloudflare v0.11 is broken, master is fixed but unreleased yet.
# see https://github.com/cloudflare/cloudflare-rs/issues/222
cloudflare = { version = "0.10", feature = ["rustls-tls"] }
dashmap = "5.5"
derive-new = "0.6"
fqdn = "0.3"
futures = "0.3"
Expand Down Expand Up @@ -49,9 +51,13 @@ little-loadshedder = "0.2"
maxminddb = "0.24"
mockall = "0.12"
moka = { version = "0.12", features = ["sync", "future"] }
num-bigint = { version = "0.4.5", features = ["serde"] }
once_cell = "1.19"
prometheus = "0.13"
rand = "0.8"
rasn = "0.15"
rasn-ocsp = "0.15"
rasn-pkix = "0.15"
rcgen = { version = "0.13", features = ["aws_lc_rs"] }
regex = "1.10"
# TODO switch back when Reqwest upgrades to Rustls 0.23
Expand All @@ -70,6 +76,7 @@ reqwest = { git = "https://github.com/blind-oracle/reqwest.git", default_feature
rustls = "0.23"
rustls-acme = "0.10"
rustls-pemfile = "2"
sha1 = "0.10"
serde = "1.0"
serde_json = "1.0"
strum = "0.26"
Expand Down Expand Up @@ -103,5 +110,6 @@ webpki-roots = "0.26"
x509-parser = "0.16"

[dev-dependencies]
hex-literal = "0.4"
criterion = { version = "0.5", features = ["async_tokio"] }
httptest = "0.16"
8 changes: 6 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ pub struct Cert {
/// How frequently to poll providers for certificates
#[clap(long = "cert-poll-interval", default_value = "10s", value_parser = parse_duration)]
pub poll_interval: Duration,

/// Disable OCSP stapling
#[clap(long = "cert-ocsp-stapling-disable")]
pub ocsp_stapling_disable: bool,
}

#[derive(Args)]
Expand Down Expand Up @@ -207,8 +211,8 @@ pub struct Acme {
pub acme_cache_path: Option<PathBuf>,

/// DNS backend to use when using DNS challenge. Currently only "cloudflare" is supported.
#[clap(long = "acme-dns-backend")]
pub acme_dns_backend: Option<acme::dns::DnsBackend>,
#[clap(long = "acme-dns-backend", default_value = "cloudflare")]
pub acme_dns_backend: acme::dns::DnsBackend,

/// Cloudflare API URL
#[clap(
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ async fn main() -> Result<(), Error> {
log::setup_logging(&cli.log).context("unable to setup logging")?;
warn!("Env: {}, Hostname: {}", cli.misc.env, cli.misc.hostname);

//tls::ocsp::req().await;

core::main(&cli).await
}
7 changes: 4 additions & 3 deletions src/tls/acme/dns/cloudflare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl Cloudflare {
impl DnsManager for Cloudflare {
async fn create(&self, zone: &str, name: &str, record: Record, ttl: u32) -> Result<(), Error> {
// Search zone
let zone_id = self.find_zone(zone).await?;
let zone_id = self.find_zone(zone).await.context("unable to find zone")?;

// Create record
let content = match record {
Expand All @@ -126,12 +126,13 @@ impl DnsManager for Cloudflare {

async fn delete(&self, zone: &str, name: &str) -> Result<(), Error> {
// Search zone
let zone_id = self.find_zone(zone).await?;
let zone_id = self.find_zone(zone).await.context("unable to find zone")?;

// Find records
let resp = self
.find_record(&zone_id, format!("{}.{}", name, zone))
.await?;
.await
.context("unable to find records")?;

// Delete all matching records
for record in resp.result {
Expand Down
46 changes: 26 additions & 20 deletions src/tls/acme/dns/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod cloudflare;

use anyhow::Error;
use anyhow::{Context, Error};
use arc_swap::ArcSwapOption;
use async_trait::async_trait;
use backoff::ExponentialBackoffBuilder;
Expand All @@ -14,7 +14,7 @@ use rustls::{
use std::{str::FromStr, sync::Arc, time::Duration};
use strum_macros::{Display, EnumString};
use tokio_util::sync::CancellationToken;
use tracing::{error, warn};
use tracing::{debug, error, warn};

use super::{Acme, TokenManager, Validity};
use crate::{
Expand Down Expand Up @@ -113,31 +113,33 @@ impl AcmeDns {
}

// Checks if certificate is still valid & reissues if needed
async fn refresh(&self) {
match self.acme.is_valid().await {
Err(e) => warn!("ACME-DNS: Unable to check validity: {e}"),
async fn refresh(&self) -> Result<(), Error> {
let validity = self
.acme
.is_valid()
.await
.context("unable to check validity")?;

Ok(Validity::Valid) => {
warn!("ACME-DNS: Certificate is still valid");
match validity {
Validity::Valid => {
debug!("ACME-DNS: Certificate is still valid");

if self.cert.load_full().is_none() {
if let Err(e) = self.reload().await {
error!("ACME-DNS: Unable to load certificate: {e}");
}
self.reload().await.context("unable to load certificate")?;
}
}

Ok(v) => {
warn!("ACME-DNS: Certificate needs to be renewed ({v})");
if let Err(e) = self.acme.issue().await {
error!("ACME-DNS: Unable to issue a certificate: {e}");
}

if let Err(e) = self.reload().await {
error!("ACME-DNS: Unable to load certificate: {e}");
}
_ => {
warn!("ACME-DNS: Certificate needs to be renewed ({validity})");
self.acme
.issue()
.await
.context("unable to issue a certificate")?;
self.reload().await.context("unable to load certificate")?;
}
}

Ok(())
}
}

Expand Down Expand Up @@ -170,7 +172,11 @@ impl Run for AcmeDns {
return Ok(());
}

_ = interval.tick() => self.refresh().await,
_ = interval.tick() => {
if let Err(e) = self.refresh().await {
error!("ACME-DNS: unable to refresh: {e:#}");
}
},
}
}
}
Expand Down
85 changes: 45 additions & 40 deletions src/tls/acme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ use anyhow::{anyhow, Context, Error};
use async_trait::async_trait;
use derive_new::new;
use instant_acme::{
Account, AccountCredentials, Authorization, AuthorizationStatus, ChallengeType, Identifier,
LetsEncrypt, NewAccount, NewOrder, Order, OrderStatus,
Account, AccountCredentials, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt,
NewAccount, NewOrder, Order, OrderStatus,
};
use itertools::Itertools;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use strum_macros::{Display, EnumString};
use tokio::fs;
use tracing::info;
use tracing::{debug, info};
use x509_parser::prelude::*;

use crate::tls::cert::{extract_san_from_der, extract_validity_from_der};
use crate::tls::cert::extract_sans;

const FILE_CERT: &str = "cert.pem";
const FILE_KEY: &str = "cert.key";

#[derive(Clone, Display, EnumString, PartialEq, Eq)]
#[strum(serialize_all = "snake_case")]
Expand Down Expand Up @@ -185,7 +188,7 @@ impl Acme {
Ok(order)
}

async fn process_authorizations(&self, order: &mut Order) -> Result<Vec<Authorization>, Error> {
async fn process_authorizations(&self, order: &mut Order) -> Result<(), Error> {
let authorizations = order
.authorizations()
.await
Expand Down Expand Up @@ -221,11 +224,12 @@ impl Acme {
.await
.context("unable to set challenge token")?;

debug!("ACME: token '{token}' for challenge id '{id}' set");
challenges.push((id, token, &challenge.url));
}

// Give it a bit time to settle
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::time::sleep(Duration::from_secs(30)).await;

// Verify that the tokens are set & mark challenges as ready
for (id, token, url) in challenges {
Expand All @@ -234,34 +238,27 @@ impl Acme {
.await
.context("unable to verify that the token is set")?;

debug!("ACME: token '{token}' for challenge id '{id}' verified, marking ready");

order
.set_challenge_ready(url)
.await
.context("unable to set challenge as ready")?;
}

Ok(authorizations)
Ok(())
}

async fn cleanup(&self, authorizations: Vec<Authorization>) -> Result<(), Error> {
let ids = authorizations
.into_iter()
.map(|x| {
let Identifier::Dns(v) = x.identifier;
v
})
.unique()
.collect::<Vec<_>>();

for id in ids {
self.token_manager.unset(&id).await?;
async fn cleanup(&self) -> Result<(), Error> {
for id in &self.domains {
self.token_manager.unset(id).await?;
}

Ok(())
}

// Poll the order with increasing intervals until it reaches some final state
// backoff crate does not work here nicely because of &mut
// Poll the order with increasing intervals until it reaches some final state.
// backoff crate does not work here nicely because of &mut.
async fn poll_order(&self, order: &mut Order, expect: OrderStatus) -> Result<(), Error> {
let mut delay = Duration::from_millis(500);
let mut retries = 8;
Expand All @@ -273,7 +270,7 @@ impl Acme {
}

if state.status == OrderStatus::Invalid {
return Err(anyhow!("order is in invalid state"));
return Err(anyhow!("order is in Invalid state"));
}
}

Expand All @@ -287,8 +284,8 @@ impl Acme {

pub async fn load(&self) -> Result<Cert, Error> {
Ok(Cert {
cert: tokio::fs::read(self.cache_path.join("cert.pem")).await?,
key: tokio::fs::read(self.cache_path.join("cert.key")).await?,
cert: tokio::fs::read(self.cache_path.join(FILE_CERT)).await?,
key: tokio::fs::read(self.cache_path.join(FILE_KEY)).await?,
})
}

Expand All @@ -303,17 +300,18 @@ impl Acme {
return Ok(Validity::NoCertsFound);
}

let cert = certs[0].as_ref();
let cert = X509Certificate::from_der(certs[0].as_ref())
.context("Unable to parse DER-encoded certificate")?
.1;

// Check if it's time to renew
let validity = extract_validity_from_der(cert)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
if now > validity.not_after.timestamp() as u64 - self.renew_before.as_secs() {
if now > cert.validity().not_after.timestamp() as u64 - self.renew_before.as_secs() {
return Ok(Validity::Expires);
}

// Check if cert's SANs match the domains that we have
let mut sans = extract_san_from_der(cert)?;
let mut sans = extract_sans(&cert)?;
let mut names = self.generate_names();
sans.sort();
names.sort();
Expand All @@ -328,12 +326,25 @@ impl Acme {
}

pub async fn issue(&self) -> Result<(), Error> {
let res = self.issue_inner().await;

// Cleanup the tokens
info!("ACME: Cleaning up");
self.cleanup().await.context("unable to cleanup tokens")?;

res
}

async fn issue_inner(&self) -> Result<(), Error> {
let mut order = self.prepare_order().await?;
info!("ACME: Order for {:?} obtained", self.domains);
info!(
"ACME: Order for {:?} obtained (status: {:?})",
self.domains,
order.state().status
);

// Process authorizations and fulfill their challenges
let authorizations = self
.process_authorizations(&mut order)
self.process_authorizations(&mut order)
.await
.context("unable to process authorizations")?;

Expand Down Expand Up @@ -374,19 +385,13 @@ impl Acme {
.ok_or_else(|| anyhow!("certificate not found"))?;

// Store the resulting cert & key
tokio::fs::write(self.cache_path.join("cert.pem"), cert)
tokio::fs::write(self.cache_path.join(FILE_CERT), cert)
.await
.context("unable to store certificate")?;
tokio::fs::write(self.cache_path.join("cert.key"), key_pair.serialize_pem())
tokio::fs::write(self.cache_path.join(FILE_KEY), key_pair.serialize_pem())
.await
.context("unable to store private key")?;

// Cleanup the tokens
info!("ACME: Cleaning up");
self.cleanup(authorizations)
.await
.context("unable to cleanup tokens")?;

Ok(())
}
}
Loading

0 comments on commit 6067fe9

Please # to comment.