Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::jose::{Body, eab_sign, sign};
use crate::network::acme::http::{get_header, https};
use crate::network::acme::{AcmeError, AcmeResult, Directory};
use aws_lc_rs::rand::SystemRandom;
use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, EcdsaSigningAlgorithm};
use base64::Engine;
use base64::engine::general_purpose::{self, URL_SAFE_NO_PAD};
use registry::schema::structs::AcmeProvider;
use reqwest::Method;
use utils::sanitize_email;
static ALG: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_FIXED_SIGNING;
#[derive(Clone)]
pub struct EabSettings {
pub kid: String,
pub hmac_key: Vec<u8>,
}
#[derive(Debug, serde::Serialize)]
pub struct NewAccountPayload<'x> {
#[serde(rename = "termsOfServiceAgreed")]
tos_agreed: bool,
contact: &'x [String],
#[serde(rename = "externalAccountBinding")]
#[serde(skip_serializing_if = "Option::is_none")]
eab: Option<Body>,
}
pub async fn acme_create_account(
provider: &mut AcmeProvider,
eab: Option<EabSettings>,
) -> AcmeResult<()> {
if provider.contact.is_empty() {
return Err(AcmeError::Invalid(
"At least one contact email is required".to_string(),
));
}
for contact in provider.contact.iter_mut() {
let email = sanitize_email(contact.trim().strip_prefix("mailto:").unwrap_or(contact))
.ok_or_else(|| AcmeError::Invalid(format!("Invalid contact email: {}", contact)))?;
*contact = format!("mailto:{}", email);
}
let directory = Directory::discover(&provider.directory, provider.max_retries as u32).await?;
let account_key = EcdsaKeyPair::generate_pkcs8(ALG, &SystemRandom::new()).unwrap();
let key_pair = EcdsaKeyPair::from_pkcs8(ALG, account_key.as_ref())
.map_err(|err| AcmeError::Crypto(format!("Failed to create ECDSA key pair: {}", err)))?;
let eab = if let Some(eab) = &eab {
eab_sign(&key_pair, &eab.kid, &eab.hmac_key, &directory.new_account)?.into()
} else {
None
};
let payload = serde_json::to_string(&NewAccountPayload {
tos_agreed: true,
contact: provider.contact.as_slice(),
eab,
})
.unwrap_or_default();
let body = sign(
&key_pair,
None,
directory.nonce(provider.max_retries as u32).await?,
&directory.new_account,
&payload,
)?;
provider.account_uri = get_header(
&https(
&directory.new_account,
Method::POST,
Some(body),
provider.max_retries as u32,
)
.await?,
"Location",
)?;
provider.account_key = URL_SAFE_NO_PAD.encode(account_key.as_ref());
Ok(())
}
impl EabSettings {
pub fn new(kid: impl Into<String>, hmac_key: impl AsRef<[u8]>) -> AcmeResult<Self> {
let key = general_purpose::URL_SAFE_NO_PAD
.decode(hmac_key.as_ref())
.map_err(|err| AcmeError::Invalid(format!("Failed to decode EAB HMAC key: {}", err)))?;
Ok(Self {
kid: kid.into(),
hmac_key: key,
})
}
}
+271
View File
@@ -0,0 +1,271 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use super::jose::{
key_authorization, key_authorization_sha256, key_authorization_sha256_base64, sign,
};
use crate::network::acme::http::{get_header, https, parse_alternate_links, parse_retry_after};
use crate::network::acme::{
AcmeError, AcmeResult, Auth, AuthStatus, Challenge, ChallengeType, Directory, Identifier,
Order, SerializedCert,
};
use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, EcdsaSigningAlgorithm};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rcgen::{CustomExtension, KeyPair, PKCS_ECDSA_P256_SHA256};
use registry::schema::structs::AcmeProvider;
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde_json::json;
use std::time::Duration;
use store::Serialize;
use store::write::Archiver;
pub const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1";
#[derive(Debug)]
pub struct AcmeRequestBuilder {
pub key_pair: EcdsaKeyPair,
pub directory: Directory,
pub kid: String,
pub challenge: ChallengeType,
pub max_retries: u32,
pub preferred_chain: Option<String>,
}
pub struct AcmeResponse<L, B> {
pub location: L,
pub body: B,
pub retry_after: Option<Duration>,
pub alternates: Vec<String>,
}
static ALG: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_FIXED_SIGNING;
impl AcmeRequestBuilder {
pub async fn new(provider: AcmeProvider) -> AcmeResult<Self> {
let directory =
Directory::discover(&provider.directory, provider.max_retries as u32).await?;
let key_pair = EcdsaKeyPair::from_pkcs8(
ALG,
&URL_SAFE_NO_PAD
.decode(&provider.account_key)
.map_err(|err| {
AcmeError::Crypto(format!("Failed to decode account key: {}", err))
})?,
)
.map_err(|err| AcmeError::Crypto(format!("Failed to create ECDSA key pair: {}", err)))?;
Ok(Self {
key_pair,
directory,
kid: provider.account_uri,
challenge: provider.challenge_type.into(),
max_retries: provider.max_retries as u32,
preferred_chain: provider.preferred_chain,
})
}
async fn request(
&self,
url: impl AsRef<str>,
payload: &str,
) -> AcmeResult<AcmeResponse<Option<String>, String>> {
let body = sign(
&self.key_pair,
Some(&self.kid),
self.directory.nonce(self.max_retries).await?,
url.as_ref(),
payload,
)?;
let response = https(url.as_ref(), Method::POST, Some(body), self.max_retries).await?;
Ok(AcmeResponse {
location: get_header(&response, "Location").ok(),
retry_after: parse_retry_after(&response),
alternates: parse_alternate_links(&response),
body: response.text().await?,
})
}
pub async fn new_order(&self, domains: Vec<String>) -> AcmeResult<AcmeResponse<String, Order>> {
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
let payload = json!({
"identifiers": domains,
})
.to_string();
let response = self.request(&self.directory.new_order, &payload).await?;
Ok(AcmeResponse {
location: response.location.ok_or(AcmeError::Invalid(format!(
"Missing Location header in new order response from {}",
self.directory.new_order
)))?,
body: serde_json::from_str(&response.body).map_err(AcmeError::Json)?,
retry_after: response.retry_after,
alternates: response.alternates,
})
}
pub async fn auth(
&self,
url: impl AsRef<str>,
) -> AcmeResult<AcmeResponse<Option<String>, Auth>> {
AcmeResponse::parse(self.request(url, "").await?)
}
pub async fn challenge(&self, url: impl AsRef<str>) -> AcmeResult<()> {
self.request(&url, "{}").await.map(|_| ())
}
pub async fn order(
&self,
url: impl AsRef<str>,
) -> AcmeResult<AcmeResponse<Option<String>, Order>> {
AcmeResponse::parse(self.request(&url, "").await?)
}
pub async fn finalize(
&self,
url: impl AsRef<str>,
csr: Vec<u8>,
) -> AcmeResult<AcmeResponse<Option<String>, Order>> {
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr));
AcmeResponse::parse(self.request(&url, &payload).await?)
}
pub async fn certificate(
&self,
url: impl AsRef<str>,
) -> AcmeResult<AcmeResponse<Option<String>, String>> {
self.request(&url, "").await
}
pub fn http_proof(&self, challenge: &Challenge) -> AcmeResult<Vec<u8>> {
let challenge_token = challenge.token.as_deref().ok_or_else(|| {
AcmeError::Invalid("Missing http-01 challenge token in response".to_string())
})?;
key_authorization(&self.key_pair, challenge_token).map(|key| key.into_bytes())
}
pub fn dns_proof(&self, challenge: &Challenge) -> AcmeResult<String> {
let challenge_token = challenge.token.as_deref().ok_or_else(|| {
AcmeError::Invalid("Missing dns-01 challenge token in response".to_string())
})?;
key_authorization_sha256_base64(&self.key_pair, challenge_token)
}
pub fn tls_alpn_key(&self, challenge: &Challenge, domain: String) -> AcmeResult<Vec<u8>> {
let challenge_token = challenge.token.as_deref().ok_or_else(|| {
AcmeError::Invalid("Missing tls-alpn-01 challenge token in response".to_string())
})?;
let mut params = rcgen::CertificateParams::new(vec![domain]).map_err(|err| {
AcmeError::Crypto(format!("Failed to create certificate params: {}", err))
})?;
let key_auth = key_authorization_sha256(&self.key_pair, challenge_token)?;
params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)
.map_err(|err| AcmeError::Crypto(format!("Failed to generate key pair: {}", err)))?;
let cert = params.self_signed(&key_pair).map_err(|err| {
AcmeError::Crypto(format!(
"Failed to generate TLS-ALPN-01 certificate: {}",
err
))
})?;
Archiver::new(SerializedCert {
certificate: cert.der().to_vec(),
private_key: key_pair.serialize_der(),
})
.untrusted()
.serialize()
.map_err(|_| AcmeError::Crypto("Failed to serialize certificate".to_string()))
}
}
impl Directory {
pub async fn discover(url: impl AsRef<str>, max_retries: u32) -> AcmeResult<Self> {
serde_json::from_str(
&https(url, Method::GET, None, max_retries)
.await?
.text()
.await?,
)
.map_err(Into::into)
}
pub async fn nonce(&self, max_retries: u32) -> AcmeResult<String> {
get_header(
&https(&self.new_nonce.as_str(), Method::HEAD, None, max_retries).await?,
"replay-nonce",
)
}
}
impl<L, T: DeserializeOwned> AcmeResponse<L, T> {
pub fn parse(input: AcmeResponse<L, String>) -> AcmeResult<AcmeResponse<L, T>> {
serde_json::from_str(&input.body)
.map_err(|err| {
AcmeError::Invalid(format!(
"ACME response parsing error: {}, body: {}",
err, input.body
))
})
.map(|body| AcmeResponse {
location: input.location,
body,
retry_after: input.retry_after,
alternates: input.alternates,
})
}
}
impl<L, T> AcmeResponse<L, T> {
pub fn assert_reasonable_retry_after(self, max_retries: u32) -> AcmeResult<Self> {
if let Some(retry_after) = self.retry_after
&& retry_after > Duration::from_secs(10 * 60)
{
trc::event!(
Acme(trc::AcmeEvent::RenewBackoff),
Elapsed = retry_after,
Reason = "ACME server requested an excessively long Retry-After",
);
return Err(AcmeError::Backoff {
max_retries,
wait: retry_after.into(),
});
}
Ok(self)
}
}
impl ChallengeType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Http01 => "http-01",
Self::Dns01 => "dns-01",
Self::TlsAlpn01 => "tls-alpn-01",
Self::DnsPersist01 => "dns-persist-01",
Self::Unknown => "unknown",
}
}
}
impl AuthStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Valid => "valid",
Self::Invalid => "invalid",
Self::Revoked => "revoked",
Self::Expired => "expired",
Self::Deactivated => "deactivated",
}
}
}
+200
View File
@@ -0,0 +1,200 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::network::acme::{AcmeError, AcmeResult};
use chrono::{DateTime, Utc};
use hyper::{
Method, StatusCode,
header::{CONTENT_TYPE, USER_AGENT},
};
use reqwest::Response;
use std::time::Duration;
#[allow(unused_mut)]
pub(crate) async fn https(
url: impl AsRef<str>,
method: Method,
body: Option<String>,
max_retries: u32,
) -> AcmeResult<Response> {
let url = url.as_ref();
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut allow_invalid_certs = false;
#[cfg(any(feature = "dev_mode", feature = "test_mode"))]
{
allow_invalid_certs =
url.starts_with("https://localhost") || url.starts_with("https://127.0.0.1");
}
let mut request = utils::http::http1_client_builder(allow_invalid_certs)
.timeout(Duration::from_secs(30))
.build()?
.request(method, url)
.header(USER_AGENT, crate::USER_AGENT);
if let Some(body) = body {
request = request
.header(CONTENT_TYPE, "application/jose+json")
.body(body);
}
let response = request.send().await?;
if response.status().is_success() {
Ok(response)
} else if matches!(
response.status(),
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
) {
let wait = parse_retry_after(&response);
trc::event!(
Acme(trc::AcmeEvent::RenewBackoff),
Url = url.to_string(),
Code = response.status().as_u16(),
Elapsed = wait.unwrap_or_default(),
);
Err(AcmeError::Backoff { wait, max_retries })
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
Err(AcmeError::HttpStatus(format!(
"Unexpected status {}: {}",
status, text
)))
}
}
pub(crate) fn get_header(response: &Response, header: &'static str) -> AcmeResult<String> {
match response.headers().get_all(header).iter().next_back() {
Some(value) => Ok(value
.to_str()
.map_err(|err| {
AcmeError::Invalid(format!("Failed to read header {}: {}", header, err))
})?
.to_string()),
None => Err(AcmeError::Invalid(format!("Missing header: {}", header))),
}
}
pub(crate) fn parse_alternate_links(response: &Response) -> Vec<String> {
alternate_links(
response
.headers()
.get_all("Link")
.iter()
.filter_map(|value| value.to_str().ok()),
)
}
fn alternate_links<'a>(values: impl Iterator<Item = &'a str>) -> Vec<String> {
let mut urls = Vec::new();
for value in values {
for link in value.split(',') {
let mut url = None;
let mut is_alternate = false;
for (index, part) in link.split(';').enumerate() {
let part = part.trim();
if index == 0 {
url = part
.strip_prefix('<')
.and_then(|part| part.strip_suffix('>'));
} else if let Some(rel) = part.strip_prefix("rel=") {
is_alternate = rel.trim_matches('"') == "alternate";
}
}
if is_alternate && let Some(url) = url {
urls.push(url.to_string());
}
}
}
urls
}
pub(crate) fn parse_retry_after(response: &Response) -> Option<Duration> {
let value = response.headers().get("Retry-After")?.to_str().ok()?;
if let Ok(secs) = value.parse::<u64>() {
Some(Duration::from_secs(secs + 1))
} else if let Ok(dt) = DateTime::parse_from_rfc2822(value) {
Utc::now()
.signed_duration_since(dt.with_timezone(&Utc))
.to_std()
.map(|dur| dur + Duration::from_secs(1))
.ok()
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::alternate_links;
#[test]
fn parses_single_alternate_link() {
let links =
alternate_links([r#"<https://acme.example/cert/1/1>;rel="alternate""#].into_iter());
assert_eq!(links, vec!["https://acme.example/cert/1/1".to_string()]);
}
#[test]
fn parses_multiple_alternates_in_one_header() {
let links = alternate_links(
[r#"<https://acme.example/cert/1/1>;rel="alternate", <https://acme.example/cert/1/2>;rel="alternate""#]
.into_iter(),
);
assert_eq!(
links,
vec![
"https://acme.example/cert/1/1".to_string(),
"https://acme.example/cert/1/2".to_string(),
]
);
}
#[test]
fn parses_alternates_across_multiple_headers() {
let links = alternate_links(
[
r#"<https://acme.example/cert/1/1>;rel="alternate""#,
r#"<https://acme.example/cert/1/2>;rel="alternate""#,
]
.into_iter(),
);
assert_eq!(
links,
vec![
"https://acme.example/cert/1/1".to_string(),
"https://acme.example/cert/1/2".to_string(),
]
);
}
#[test]
fn ignores_non_alternate_relations() {
let links = alternate_links(
[r#"<https://acme.example/index>;rel="index", <https://acme.example/cert/1/1>;rel="alternate""#]
.into_iter(),
);
assert_eq!(links, vec!["https://acme.example/cert/1/1".to_string()]);
}
#[test]
fn tolerates_unquoted_rel_and_extra_whitespace() {
let links =
alternate_links([r#" <https://acme.example/cert/1/1> ; rel=alternate "#].into_iter());
assert_eq!(links, vec!["https://acme.example/cert/1/1".to_string()]);
}
#[test]
fn returns_empty_when_no_alternates() {
let links = alternate_links([r#"<https://acme.example/dir>;rel="index""#].into_iter());
assert!(links.is_empty());
}
}
+173
View File
@@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use crate::network::acme::{AcmeError, AcmeResult};
use aws_lc_rs::digest::{Digest, SHA256, digest};
use aws_lc_rs::hmac;
use aws_lc_rs::rand::SystemRandom;
use aws_lc_rs::signature::{EcdsaKeyPair, KeyPair};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use serde::Serialize;
pub(crate) fn sign(
key: &EcdsaKeyPair,
kid: Option<&str>,
nonce: String,
url: &str,
payload: &str,
) -> AcmeResult<String> {
let jwk = match kid {
None => Some(Jwk::new(key)),
Some(_) => None,
};
let protected = Protected::encode("ES256", jwk, kid, nonce.into(), url)?;
let payload = URL_SAFE_NO_PAD.encode(payload);
let combined = format!("{}.{}", protected, payload);
let signature = key
.sign(&SystemRandom::new(), combined.as_bytes())
.map_err(|err| AcmeError::Crypto(format!("Failed to sign payload: {}", err)))?;
serde_json::to_string(&Body {
protected,
payload,
signature: URL_SAFE_NO_PAD.encode(signature.as_ref()),
})
.map_err(Into::into)
}
pub(crate) fn eab_sign(
key: &EcdsaKeyPair,
kid: &str,
hmac_key: &[u8],
url: &str,
) -> AcmeResult<Body> {
let protected = Protected::encode("HS256", None, kid.into(), None, url)?;
let payload = Jwk::new(key).base64()?;
let combined = format!("{}.{}", protected, payload);
let key = hmac::Key::new(hmac::HMAC_SHA256, hmac_key);
let tag = hmac::sign(&key, combined.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(tag.as_ref());
Ok(Body {
protected,
payload,
signature,
})
}
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> AcmeResult<String> {
Ok(format!(
"{}.{}",
token,
Jwk::new(key).thumb_sha256_base64()?
))
}
pub(crate) fn key_authorization_sha256(key: &EcdsaKeyPair, token: &str) -> AcmeResult<Digest> {
key_authorization(key, token).map(|s| digest(&SHA256, s.as_bytes()))
}
pub(crate) fn key_authorization_sha256_base64(
key: &EcdsaKeyPair,
token: &str,
) -> AcmeResult<String> {
key_authorization_sha256(key, token).map(|s| URL_SAFE_NO_PAD.encode(s.as_ref()))
}
#[derive(Debug, Serialize)]
pub(crate) struct Body {
protected: String,
payload: String,
signature: String,
}
#[derive(Serialize)]
struct Protected<'a> {
alg: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
jwk: Option<Jwk>,
#[serde(skip_serializing_if = "Option::is_none")]
kid: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>,
url: &'a str,
}
impl<'a> Protected<'a> {
fn encode(
alg: &'static str,
jwk: Option<Jwk>,
kid: Option<&'a str>,
nonce: Option<String>,
url: &'a str,
) -> AcmeResult<String> {
serde_json::to_vec(&Protected {
alg,
jwk,
kid,
nonce,
url,
})
.map_err(Into::into)
.map(|v| URL_SAFE_NO_PAD.encode(v.as_slice()))
}
}
#[derive(Serialize)]
struct Jwk {
alg: &'static str,
crv: &'static str,
kty: &'static str,
#[serde(rename = "use")]
u: &'static str,
x: String,
y: String,
}
impl Jwk {
pub(crate) fn new(key: &EcdsaKeyPair) -> Self {
let (x, y) = key.public_key().as_ref()[1..].split_at(32);
Self {
alg: "ES256",
crv: "P-256",
kty: "EC",
u: "sig",
x: URL_SAFE_NO_PAD.encode(x),
y: URL_SAFE_NO_PAD.encode(y),
}
}
pub(crate) fn base64(&self) -> AcmeResult<String> {
serde_json::to_vec(self)
.map_err(Into::into)
.map(|v| URL_SAFE_NO_PAD.encode(v.as_slice()))
}
pub(crate) fn thumb_sha256_base64(&self) -> AcmeResult<String> {
Ok(URL_SAFE_NO_PAD.encode(digest(
&SHA256,
&serde_json::to_vec(&JwkThumb {
crv: self.crv,
kty: self.kty,
x: &self.x,
y: &self.y,
})
.map_err(AcmeError::Json)?,
)))
}
}
#[derive(Serialize)]
struct JwkThumb<'a> {
crv: &'a str,
kty: &'a str,
x: &'a str,
y: &'a str,
}
+295
View File
@@ -0,0 +1,295 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod account;
pub mod directory;
pub mod http;
pub mod jose;
pub mod order;
pub mod renew;
pub mod resolver;
use crate::network::dns::update::DnsUpdater;
use chrono::{DateTime, Utc};
use registry::schema::enums::AcmeChallengeType;
use rustls::sign::CertifiedKey;
use serde::Deserialize;
use std::{
fmt::{Debug, Display},
sync::Arc,
time::Duration,
};
use store::registry::write::RegistryWriteResult;
pub type AcmeResult<T> = Result<T, AcmeError>;
pub enum AcmeError {
Http(reqwest::Error),
HttpStatus(String),
Json(serde_json::Error),
Crypto(String),
Invalid(String),
NotDue(String),
Dns(String),
AuthInvalid(String),
OrderInvalid(String),
ChallengeNotSupported {
requested: ChallengeType,
supported: Vec<Challenge>,
},
Internal(trc::Error),
Registry(RegistryWriteResult),
OrderTimeout {
max_retries: u32,
},
AuthTimeout {
max_retries: u32,
},
Backoff {
max_retries: u32,
wait: Option<Duration>,
},
}
#[derive(
rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, serde::Serialize, Deserialize,
)]
pub struct SerializedCert {
pub certificate: Vec<u8>,
pub private_key: Vec<u8>,
}
pub struct PemCert {
pub certificate: String,
pub private_key: String,
}
pub struct ParsedCert {
pub sans: Vec<String>,
pub issuer: String,
pub valid_not_before: DateTime<Utc>,
pub valid_not_after: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
pub new_nonce: String,
pub new_account: String,
pub new_order: String,
}
#[derive(Debug, Deserialize, Eq, PartialEq, Clone, Copy)]
pub enum ChallengeType {
#[serde(rename = "http-01")]
Http01,
#[serde(rename = "dns-01")]
Dns01,
#[serde(rename = "dns-persist-01")]
DnsPersist01,
#[serde(rename = "tls-alpn-01")]
TlsAlpn01,
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
#[serde(flatten)]
pub status: OrderStatus,
pub authorizations: Vec<String>,
pub finalize: String,
pub error: Option<Problem>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum OrderStatus {
Pending,
Ready,
Valid { certificate: String },
Invalid,
Processing,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Auth {
pub status: AuthStatus,
pub identifier: Identifier,
pub challenges: Vec<Challenge>,
pub wildcard: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AuthStatus {
Pending,
Valid,
Invalid,
Revoked,
Expired,
Deactivated,
}
pub struct AcmeDnsParameters {
pub updater: DnsUpdater,
pub origin: Option<String>,
}
#[derive(Clone, Debug, serde::Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Identifier {
Dns(String),
}
#[derive(Debug, Deserialize, Clone)]
pub struct Challenge {
#[serde(rename = "type")]
pub typ: ChallengeType,
pub url: String,
pub token: Option<String>,
pub error: Option<Problem>,
}
#[derive(Clone, Debug, serde::Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Problem {
#[serde(rename = "type")]
pub typ: Option<String>,
pub detail: Option<String>,
}
pub struct StaticResolver {
pub key: Option<Arc<CertifiedKey>>,
}
impl Debug for StaticResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticResolver").finish()
}
}
impl From<reqwest::Error> for AcmeError {
fn from(err: reqwest::Error) -> Self {
AcmeError::Http(err)
}
}
impl From<serde_json::Error> for AcmeError {
fn from(err: serde_json::Error) -> Self {
AcmeError::Json(err)
}
}
impl From<trc::Error> for AcmeError {
fn from(err: trc::Error) -> Self {
AcmeError::Internal(err)
}
}
impl From<AcmeChallengeType> for ChallengeType {
fn from(value: AcmeChallengeType) -> Self {
match value {
AcmeChallengeType::Http01 => ChallengeType::Http01,
AcmeChallengeType::Dns01 => ChallengeType::Dns01,
AcmeChallengeType::TlsAlpn01 => ChallengeType::TlsAlpn01,
AcmeChallengeType::DnsPersist01 => ChallengeType::DnsPersist01,
}
}
}
impl Display for AuthStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthStatus::Pending => write!(f, "pending"),
AuthStatus::Valid => write!(f, "valid"),
AuthStatus::Invalid => write!(f, "invalid"),
AuthStatus::Revoked => write!(f, "revoked"),
AuthStatus::Expired => write!(f, "expired"),
AuthStatus::Deactivated => write!(f, "deactivated"),
}
}
}
impl Auth {
pub fn to_error(&self) -> String {
let mut errors = format!("Status: {}", self.status);
for challenge in &self.challenges {
if let Some(error) = &challenge.error {
errors.push_str(&format!(
"; Challenge type: {}, error: {}",
challenge.typ.as_str(),
error
));
}
}
errors
}
}
impl Identifier {
pub fn hostname(&self) -> &str {
match self {
Identifier::Dns(hostname) => hostname,
}
}
}
impl Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(typ) = &self.typ {
if let Some(detail) = &self.detail {
write!(f, "{}: {}", typ, detail)
} else {
write!(f, "{}", typ)
}
} else if let Some(detail) = &self.detail {
write!(f, "{}", detail)
} else {
write!(f, "Unknown error")
}
}
}
impl Display for AcmeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AcmeError::Http(err) => write!(f, "HTTP error: {}", err),
AcmeError::HttpStatus(status) => write!(f, "HTTP error: {}", status),
AcmeError::Json(err) => write!(f, "JSON error: {}", err),
AcmeError::Dns(err) => write!(f, "DNS error: {}", err),
AcmeError::Crypto(err) => write!(f, "Cryptographic error: {}", err),
AcmeError::Invalid(err) => write!(f, "Invalid request: {}", err),
AcmeError::NotDue(err) => write!(f, "{}", err),
AcmeError::AuthInvalid(status) => write!(f, "Authentication failed: {:?}", status),
AcmeError::OrderTimeout { .. } => write!(f, "Order processing timed out"),
AcmeError::OrderInvalid(reason) => write!(f, "Order is invalid: {}", reason),
AcmeError::AuthTimeout { .. } => write!(f, "Authentication timed out"),
AcmeError::ChallengeNotSupported {
requested,
supported,
} => {
write!(
f,
"Challenge type {:?} not supported. Supported types: {:?}",
requested, supported
)
}
AcmeError::Internal(err) => write!(f, "Internal error: {}", err),
AcmeError::Registry(err) => write!(f, "Registry error: {:?}", err),
AcmeError::Backoff { wait, .. } => {
if let Some(time) = wait {
write!(f, "Rate limited. Retry after {} seconds", time.as_secs())
} else {
write!(f, "Rate limited. Retry after some time")
}
}
}
}
}
+567
View File
@@ -0,0 +1,567 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use crate::network::acme::directory::AcmeRequestBuilder;
use crate::network::acme::{
AcmeDnsParameters, AcmeError, AcmeResult, AuthStatus, ChallengeType, Identifier, OrderStatus,
ParsedCert, PemCert,
};
use crate::{KV_ACME, Server};
use chrono::{TimeZone, Utc};
use dns_update::DnsRecord;
use futures::future::try_join_all;
use rcgen::{CertificateParams, DistinguishedName, KeyPair, PKCS_ECDSA_P256_SHA256};
use std::collections::BTreeSet;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::Duration;
use store::dispatch::lookup::KeyValue;
use trc::AcmeEvent;
use x509_parser::parse_x509_certificate;
use x509_parser::prelude::{GeneralName, ParsedExtension};
const HOSTNAMES: &[&str] = &["mta-sts", "ua-auto-config", "autoconfig", "autodiscover"];
impl AcmeRequestBuilder {
pub fn build_domains(
&self,
server: &Server,
domain: &str,
hostnames: &[String],
) -> Vec<String> {
if hostnames.is_empty() {
if matches!(
self.challenge,
ChallengeType::Dns01 | ChallengeType::DnsPersist01
) {
vec![format!("*.{domain}"), domain.to_string()]
} else {
let server_name = server.core.network.server_name.as_str();
let domain_suffix = format!(".{domain}");
let matches_zone = |name: &str| name == domain || name.ends_with(&domain_suffix);
// Add technical domains
let mut domains = HOSTNAMES
.iter()
.map(|hostname| format!("{hostname}.{domain}"))
.collect::<BTreeSet<_>>();
// Add server name if it matches the domain (including the apex itself)
if matches_zone(server_name) {
domains.insert(server_name.to_string());
}
// Add mail exchangers
for exchanger in &server.core.network.info.mxs {
if let Some(exchanger) = &exchanger.hostname
&& matches_zone(exchanger)
{
domains.insert(exchanger.to_string());
}
}
// Add service hosts
for (_, service) in &server.core.network.info.services {
if let Some(service) = &service.hostname
&& matches_zone(service)
{
domains.insert(service.to_string());
}
}
domains.into_iter().collect()
}
} else {
hostnames
.iter()
.map(|h| {
if h.contains('.') {
h.clone()
} else {
format!("{h}.{domain}")
}
})
.collect()
}
}
pub async fn renew(
&self,
server: &Server,
domains: Vec<String>,
reuse_key_pem: Option<String>,
dns_parameters: Option<AcmeDnsParameters>,
) -> AcmeResult<PemCert> {
let mut params = CertificateParams::new(domains.clone()).map_err(|err| {
AcmeError::Crypto(format!("Failed to create certificate params: {}", err))
})?;
params.distinguished_name = DistinguishedName::new();
let key_pair = match reuse_key_pem {
Some(pem) => KeyPair::from_pem(&pem).map_err(|err| {
AcmeError::Crypto(format!("Failed to load private key for reuse: {}", err))
})?,
None => KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).map_err(|err| {
AcmeError::Crypto(format!("Failed to generate key pair: {}", err))
})?,
};
let response = self.new_order(domains.clone()).await?;
let order_url = response.location;
let mut order = response.body;
let mut retry_after = None;
trc::event!(
Acme(AcmeEvent::OrderStart),
Url = self.directory.new_order.to_string(),
Details = order_url.to_string(),
Hostname = domains.as_slice(),
Type = self.challenge.as_str(),
);
loop {
match order.status {
OrderStatus::Pending => {
if matches!(self.challenge, ChallengeType::Dns01) {
for url in &order.authorizations {
self.authorize(server, url, dns_parameters.as_ref()).await?;
}
} else {
let auth_futures = order
.authorizations
.iter()
.map(|url| self.authorize(server, url, dns_parameters.as_ref()));
try_join_all(auth_futures).await?;
}
trc::event!(
Acme(AcmeEvent::AuthCompleted),
Url = self.directory.new_order.to_string(),
Hostname = domains.as_slice(),
);
let response = self.order(&order_url).await?;
order = response.body;
retry_after = response.retry_after;
}
OrderStatus::Processing => {
for i in 0u64..10 {
trc::event!(
Acme(AcmeEvent::OrderProcessing),
Url = self.directory.new_order.to_string(),
Hostname = domains.as_slice(),
Total = i,
);
tokio::time::sleep(
retry_after.unwrap_or_else(|| Duration::from_secs(1u64 << i)),
)
.await;
let response = self
.order(&order_url)
.await?
.assert_reasonable_retry_after(self.max_retries)?;
order = response.body;
retry_after = response.retry_after;
if order.status != OrderStatus::Processing {
break;
}
}
if order.status == OrderStatus::Processing {
return Err(AcmeError::OrderTimeout {
max_retries: self.max_retries,
});
}
}
OrderStatus::Ready => {
trc::event!(
Acme(AcmeEvent::OrderReady),
Url = self.directory.new_order.to_string(),
Hostname = domains.as_slice(),
);
let csr = params.serialize_request(&key_pair).map_err(|err| {
AcmeError::Crypto(format!("Failed to serialize CSR: {}", err))
})?;
let csr = csr.der().to_vec();
order = self.finalize(order.finalize, csr).await?.body;
}
OrderStatus::Valid { certificate } => {
trc::event!(
Acme(AcmeEvent::OrderValid),
Url = self.directory.new_order.to_string(),
Hostname = domains.as_slice(),
);
let certificate = self.select_certificate(&domains, certificate).await?;
return Ok(PemCert {
certificate,
private_key: key_pair.serialize_pem(),
});
}
OrderStatus::Invalid => {
let reason = if let Some(reason) = order.error {
reason.to_string()
} else {
"Unknown reason".to_string()
};
trc::event!(
Acme(AcmeEvent::OrderInvalid),
Url = self.directory.new_order.to_string(),
Details = order_url.to_string(),
Hostname = domains.as_slice(),
Reason = reason.clone(),
);
return Err(AcmeError::OrderInvalid(reason));
}
}
}
}
async fn authorize(
&self,
server: &Server,
url: &String,
dns_parameters: Option<&AcmeDnsParameters>,
) -> AcmeResult<()> {
let response = self
.auth(url)
.await?
.assert_reasonable_retry_after(self.max_retries)?;
let mut retry_after = response.retry_after;
let auth = response.body;
let (domain, challenge_url) = match auth.status {
AuthStatus::Pending => {
let Identifier::Dns(domain) = auth.identifier;
trc::event!(
Acme(AcmeEvent::AuthStart),
Hostname = domain.to_string(),
Type = self.challenge.as_str(),
Url = self.directory.new_order.to_string(),
);
let challenge = auth
.challenges
.iter()
.find(|c| c.typ == self.challenge)
.ok_or(AcmeError::ChallengeNotSupported {
requested: self.challenge,
supported: auth.challenges.clone(),
})?;
match &self.challenge {
ChallengeType::TlsAlpn01 => {
server
.in_memory_store()
.key_set(
KeyValue::with_prefix(
KV_ACME,
&domain,
self.tls_alpn_key(challenge, domain.clone())?,
)
.expires(3600),
)
.await?;
}
ChallengeType::Http01 => {
server
.in_memory_store()
.key_set(
KeyValue::with_prefix(
KV_ACME,
challenge.token.as_deref().ok_or_else(|| {
AcmeError::Invalid(
"Missing http-01 challenge token in response"
.to_string(),
)
})?,
self.http_proof(challenge)?,
)
.expires(3600),
)
.await?;
}
ChallengeType::Dns01 => {
let dns_parameters = dns_parameters.unwrap();
let domain = domain.strip_prefix("*.").unwrap_or(&domain);
let zone = dns_parameters
.origin
.as_deref()
.or_else(|| psl::domain_str(domain))
.unwrap_or(domain);
let proof = self.dns_proof(challenge)?;
let challenge_name = format!("_acme-challenge.{}", domain);
dns_parameters
.updater
.set_rrset(
zone,
&challenge_name,
dns_update::DnsRecordType::TXT,
vec![DnsRecord::TXT(proof.clone())],
)
.await
.map_err(AcmeError::Dns)?;
dns_parameters
.updater
.wait_for_txt_propagation(&challenge_name, zone, &proof)
.await;
}
ChallengeType::DnsPersist01 => {}
ChallengeType::Unknown => unreachable!(),
}
self.challenge(&challenge.url).await?;
(domain, challenge.url.clone())
}
AuthStatus::Valid => return Ok(()),
_ => {
trc::event!(
Acme(AcmeEvent::AuthError),
Hostname = auth.identifier.hostname().to_string(),
Type = self.challenge.as_str(),
Url = self.directory.new_order.to_string(),
Details = url.to_string(),
Reason = auth.to_error(),
);
return Err(AcmeError::AuthInvalid(auth.to_error()));
}
};
for i in 0u64..5 {
tokio::time::sleep(retry_after.unwrap_or_else(|| Duration::from_secs(1u64 << i))).await;
let response = self
.auth(url)
.await?
.assert_reasonable_retry_after(self.max_retries)?;
retry_after = response.retry_after;
match response.body.status {
AuthStatus::Pending => {
trc::event!(
Acme(AcmeEvent::AuthPending),
Hostname = domain.to_string(),
Url = self.directory.new_order.to_string(),
Total = i,
);
self.challenge(&challenge_url).await?
}
AuthStatus::Valid => {
trc::event!(
Acme(AcmeEvent::AuthValid),
Hostname = domain.to_string(),
Url = self.directory.new_order.to_string(),
);
return Ok(());
}
_ => {
trc::event!(
Acme(AcmeEvent::AuthError),
Hostname = domain.to_string(),
Type = self.challenge.as_str(),
Url = self.directory.new_order.to_string(),
Details = url.to_string(),
Reason = response.body.to_error(),
);
return Err(AcmeError::AuthInvalid(response.body.to_error()));
}
}
}
trc::event!(
Acme(AcmeEvent::AuthTooManyAttempts),
Hostname = domain.to_string(),
Type = self.challenge.as_str(),
Url = self.directory.new_order.to_string(),
Details = url.to_string(),
Total = 5u64,
);
Err(AcmeError::AuthTimeout {
max_retries: self.max_retries,
})
}
async fn select_certificate(&self, domains: &[String], url: String) -> AcmeResult<String> {
let response = self.certificate(url).await?;
let Some(preferred) = self.preferred_chain.as_deref() else {
return Ok(response.body);
};
if chain_matches(&response.body, preferred) {
return Ok(response.body);
}
for alternate in &response.alternates {
match self.certificate(alternate).await {
Ok(alternate) if chain_matches(&alternate.body, preferred) => {
return Ok(alternate.body);
}
Ok(_) => {}
Err(err) => {
trc::event!(
Acme(AcmeEvent::ProcessCert),
Url = alternate.to_string(),
Hostname = domains,
Reason = err.to_string(),
);
}
}
}
trc::event!(
Acme(AcmeEvent::ProcessCert),
Hostname = domains,
Reason = format!(
"Preferred certificate chain '{preferred}' not offered by the CA; using the default chain",
),
);
Ok(response.body)
}
}
fn chain_matches(pem_chain: &str, preferred: &str) -> bool {
let Ok(blocks) = pem::parse_many(pem_chain) else {
return false;
};
let Some(top) = blocks.last() else {
return false;
};
let Ok((_, cert)) = parse_x509_certificate(top.contents()) else {
return false;
};
cert.issuer()
.iter_common_name()
.filter_map(|cn| cn.as_str().ok())
.any(|cn| cn == preferred)
}
impl ParsedCert {
pub fn parse(certificate: impl AsRef<[u8]>) -> AcmeResult<ParsedCert> {
let der = pem::parse_many(certificate)
.map_err(|err| AcmeError::Crypto(format!("Failed to parse PEM: {}", err)))?
.into_iter()
.next()
.ok_or_else(|| AcmeError::Crypto("No certificates found in PEM".to_string()))?;
Self::parse_der(der.contents())
}
pub fn parse_der(der: &[u8]) -> AcmeResult<ParsedCert> {
parse_x509_certificate(der)
.map_err(|err| AcmeError::Crypto(format!("Failed to parse X.509 certificate: {}", err)))
.and_then(|(_, cert)| {
// Add CNs and SANs to the list of names
let mut names: BTreeSet<String> = BTreeSet::new();
for name in cert.subject().iter_common_name() {
if let Ok(name) = name.as_str() {
names.insert(name.into());
}
}
for ext in cert.extensions() {
if let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() {
for name in &san.general_names {
let name = match name {
GeneralName::DNSName(name) => (*name).into(),
GeneralName::IPAddress(ip) => match ip.len() {
4 => Ipv4Addr::from(<[u8; 4]>::try_from(*ip).unwrap())
.to_string(),
16 => Ipv6Addr::from(<[u8; 16]>::try_from(*ip).unwrap())
.to_string(),
_ => continue,
},
_ => {
continue;
}
};
names.insert(name);
}
}
}
Ok(ParsedCert {
sans: names.into_iter().collect(),
issuer: cert.tbs_certificate.issuer().to_string(),
valid_not_before: Utc
.timestamp_opt(cert.tbs_certificate.validity().not_before.timestamp(), 0)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_before time is out of range".to_string(),
)
})?,
valid_not_after: Utc
.timestamp_opt(cert.tbs_certificate.validity().not_after.timestamp(), 0)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_after time is out of range".to_string(),
)
})?,
})
})
}
}
#[cfg(test)]
mod tests {
use super::chain_matches;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_ECDSA_P256_SHA256};
fn self_signed_pem(common_name: &str) -> String {
let mut params = CertificateParams::new(vec!["host.example".to_string()]).unwrap();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, common_name);
params.distinguished_name = dn;
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
params.self_signed(&key_pair).unwrap().pem()
}
#[test]
fn matches_top_certificate_issuer() {
let chain = self_signed_pem("ISRG Root X1");
assert!(chain_matches(&chain, "ISRG Root X1"));
}
#[test]
fn match_is_case_sensitive() {
let chain = self_signed_pem("ISRG Root X1");
assert!(!chain_matches(&chain, "isrg root x1"));
}
#[test]
fn match_is_exact_not_substring() {
let chain = self_signed_pem("ISRG Root X10");
assert!(!chain_matches(&chain, "ISRG Root X1"));
}
#[test]
fn does_not_match_unrelated_issuer() {
let chain = self_signed_pem("ISRG Root X2");
assert!(!chain_matches(&chain, "ISRG Root X1"));
}
#[test]
fn uses_topmost_certificate_not_leaf() {
let leaf = self_signed_pem("Leaf Issuer");
let top = self_signed_pem("ISRG Root X1");
let chain = format!("{leaf}{top}");
assert!(chain_matches(&chain, "ISRG Root X1"));
assert!(!chain_matches(&chain, "Leaf Issuer"));
}
#[test]
fn rejects_unparseable_chain() {
assert!(!chain_matches("not a pem", "ISRG Root X1"));
}
}
+311
View File
@@ -0,0 +1,311 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Server,
ipc::{BroadcastEvent, RegistryChange},
network::acme::{
AcmeDnsParameters, AcmeError, AcmeResult, ParsedCert, directory::AcmeRequestBuilder,
},
};
use registry::{
schema::{
enums::{AcmeChallengeType, AcmeRenewBefore, DnsRecordType},
prelude::{ObjectType, Property},
structs::{
AcmeProvider, Certificate, CertificateManagement, DnsManagement, Domain, PublicText,
PublicTextValue, SecretText, SecretTextValue, SystemSettings, Task, TaskDnsManagement,
TaskDomainManagement, TaskStatus,
},
},
types::{datetime::UTCDateTime, id::ObjectId, map::Map},
};
use store::{
registry::{
RegistryQuery,
write::{RegistryWrite, RegistryWriteResult},
},
write::now,
};
use types::id::Id;
impl Server {
pub async fn acme_renew(&self, domain_id: Id) -> AcmeResult<Vec<Task>> {
let Some(domain) = self.registry().object::<Domain>(domain_id).await? else {
return Err(AcmeError::Invalid(format!(
"Domain with ID {} not found",
domain_id
)));
};
let cert = match domain.certificate_management {
CertificateManagement::Manual => {
return Err(AcmeError::Invalid(
"ACME not configured for domain".to_string(),
));
}
CertificateManagement::Automatic(props) => props,
};
let Some(acme_provider) = self
.registry()
.object::<AcmeProvider>(cert.acme_provider_id)
.await?
else {
return Err(AcmeError::Invalid(format!(
"ACME provider with ID {} not found",
cert.acme_provider_id
)));
};
let challenge_type = acme_provider.challenge_type;
let renew_before = acme_provider.renew_before;
let reuse_key = acme_provider.reuse_key;
let request = AcmeRequestBuilder::new(acme_provider).await?;
let domains = request.build_domains(
self,
&domain.name,
&cert.subject_alternative_names.into_inner(),
);
if let Some(renew_at) = self
.acme_certificate_renewal_due(&domains, renew_before, now())
.await?
{
return Err(AcmeError::NotDue(format!(
"Certificate for domain {} is still valid; renewal is not due until {}",
domain.name,
UTCDateTime::from_timestamp(renew_at as i64)
)));
}
let dns_parameters = match &domain.dns_management {
DnsManagement::Automatic(props) if challenge_type == AcmeChallengeType::Dns01 => {
match self.build_dns_updater(props.dns_server_id).await? {
Ok(updater) => Some(AcmeDnsParameters {
updater,
origin: props.origin.clone(),
}),
Err(err) => {
return Err(AcmeError::Invalid(format!(
"Failed to build DNS updater: {}",
err
)));
}
}
}
_ => None,
};
if challenge_type == AcmeChallengeType::Dns01 && dns_parameters.is_none() {
return Err(AcmeError::Invalid(
"ACME provider requires DNS challenge but a DNS provider was not configured"
.to_string(),
));
}
let reuse_key_pem = if reuse_key {
match self.acme_certificate_by_domains(&domains).await? {
Some(certificate) => certificate
.private_key
.secret()
.await
.map(std::borrow::Cow::into_owned)
.map_err(|err| {
AcmeError::Crypto(format!("Failed to load certificate private key: {err}"))
})?
.into(),
None => None,
}
} else {
None
};
let pem_cert = request
.renew(self, domains, reuse_key_pem, dns_parameters)
.await?;
let parsed_cert = ParsedCert::parse(&pem_cert.certificate)?;
let mut new_sans = parsed_cert.sans.clone();
new_sans.sort();
let certificate = Certificate {
private_key: SecretText::Text(SecretTextValue {
secret: pem_cert.private_key,
}),
certificate: PublicText::Text(PublicTextValue {
value: pem_cert.certificate,
}),
issuer: parsed_cert.issuer,
not_valid_after: UTCDateTime::from_timestamp(parsed_cert.valid_not_after.timestamp()),
not_valid_before: UTCDateTime::from_timestamp(parsed_cert.valid_not_before.timestamp()),
subject_alternative_names: Map::new(parsed_cert.sans),
};
let now = now();
let expires_in = (parsed_cert.valid_not_after.timestamp() as u64).saturating_sub(now);
if expires_in < 3600 {
return Err(AcmeError::Invalid(format!(
"Certificate expires in {} seconds, expected at least 3600 seconds",
expires_in
)));
}
match self
.registry()
.write(RegistryWrite::insert(&certificate.into()))
.await?
{
RegistryWriteResult::Success(id) => {
// Repoint the default certificate to the renewed object when it
// tracks the same SAN set, so its id does not go stale
if let Some(old) = self
.registry()
.get(ObjectType::SystemSettings.singleton())
.await?
{
let mut settings = SystemSettings::from(old.clone());
if let Some(default_id) = settings.default_certificate_id
&& let Some(default_cert) =
self.registry().object::<Certificate>(default_id).await?
{
let mut default_sans =
default_cert.subject_alternative_names.clone().into_inner();
default_sans.sort();
if default_sans == new_sans {
settings.default_certificate_id = Some(id);
if let Err(err) = self
.registry()
.write(RegistryWrite::update(
Id::singleton(),
&settings.into(),
&old,
))
.await
{
trc::error!(
err.details(
"Failed to update default certificate after ACME renewal."
)
.caused_by(trc::location!())
);
}
}
}
}
// Reload registry
let change = RegistryChange::Insert(ObjectId::new(ObjectType::Certificate, id));
Box::pin(self.reload_registry(change)).await?;
self.cluster_broadcast(BroadcastEvent::RegistryChange(change))
.await;
let mut tasks = Vec::new();
let renew_at = Self::acme_renewal_due_at(
parsed_cert.valid_not_before.timestamp(),
parsed_cert.valid_not_after.timestamp(),
renew_before,
);
trc::event!(
Acme(trc::AcmeEvent::OrderCompleted),
Domain = domain.name.clone(),
Hostname = new_sans.as_slice(),
Id = id.to_string(),
ValidFrom =
trc::Value::Timestamp(parsed_cert.valid_not_before.timestamp() as u64),
ValidTo = trc::Value::Timestamp(parsed_cert.valid_not_after.timestamp() as u64),
NextRetry = trc::Value::Timestamp(renew_at as u64),
);
tasks.push(Task::AcmeRenewal(TaskDomainManagement {
domain_id,
status: TaskStatus::at(renew_at),
}));
// Update TLSA records
if let DnsManagement::Automatic(props) = &domain.dns_management
&& props.publish_records.contains(&DnsRecordType::Tlsa)
{
tasks.push(Task::DnsManagement(TaskDnsManagement {
domain_id,
on_success_renew_certificate: false,
status: TaskStatus::now(),
update_records: Map::new(vec![DnsRecordType::Tlsa]),
}));
}
Ok(tasks)
}
err => Err(AcmeError::Registry(err)),
}
}
async fn acme_certificate_by_domains(
&self,
domains: &[String],
) -> AcmeResult<Option<Certificate>> {
let mut wanted = domains.iter().collect::<Vec<_>>();
wanted.sort();
let Some(reference) = wanted.first() else {
return Ok(None);
};
let candidate_ids = self
.registry()
.query::<Vec<Id>>(
RegistryQuery::new(ObjectType::Certificate)
.text(Property::SubjectAlternativeNames, reference.as_str()),
)
.await?;
for id in candidate_ids {
let Some(certificate) = self.registry().object::<Certificate>(id).await? else {
continue;
};
let mut sans = certificate
.subject_alternative_names
.iter()
.collect::<Vec<_>>();
sans.sort();
if sans == wanted {
return Ok(Some(certificate));
}
}
Ok(None)
}
async fn acme_certificate_renewal_due(
&self,
domains: &[String],
renew_before: AcmeRenewBefore,
now: u64,
) -> AcmeResult<Option<u64>> {
let now = now as i64;
let Some(certificate) = self.acme_certificate_by_domains(domains).await? else {
return Ok(None);
};
let not_valid_after = certificate.not_valid_after.timestamp();
if not_valid_after <= now {
return Ok(None);
}
let not_valid_before = certificate.not_valid_before.timestamp();
let renew_at = Self::acme_renewal_due_at(not_valid_before, not_valid_after, renew_before);
Ok(if now < renew_at {
Some(renew_at as u64)
} else {
None
})
}
fn acme_renewal_due_at(
not_valid_before: i64,
not_valid_after: i64,
renew_before: AcmeRenewBefore,
) -> i64 {
let total = not_valid_after.saturating_sub(not_valid_before);
let (numerator, denominator) = match renew_before {
AcmeRenewBefore::R12 => (1, 2),
AcmeRenewBefore::R23 => (2, 3),
AcmeRenewBefore::R34 => (3, 4),
AcmeRenewBefore::R45 => (4, 5),
};
not_valid_before + total * numerator / denominator
}
}
+111
View File
@@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
KV_ACME, Server,
network::acme::{SerializedCert, StaticResolver, directory::ACME_TLS_ALPN_NAME},
};
use rustls::{
ServerConfig,
crypto::aws_lc_rs::sign::any_ecdsa_type,
server::{ClientHello, ResolvesServerCert},
sign::CertifiedKey,
};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::sync::Arc;
use store::{
dispatch::lookup::KeyValue,
write::{AlignedBytes, Archive},
};
use trc::AcmeEvent;
impl Server {
pub(crate) async fn build_acme_certificate(&self, domain: &str) -> Option<Arc<CertifiedKey>> {
match self
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(KeyValue::<()>::build_key(KV_ACME, domain))
.await
{
Ok(Some(cert_)) => match cert_.unarchive::<SerializedCert>() {
Ok(cert) => {
match any_ecdsa_type(&PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
cert.private_key.as_ref(),
))) {
Ok(key) => Some(Arc::new(CertifiedKey::new(
vec![CertificateDer::from(cert.certificate.to_vec())],
key,
))),
Err(err) => {
trc::event!(
Acme(AcmeEvent::Error),
Domain = domain.to_string(),
Reason = err.to_string(),
Details = "Failed to parse private key"
);
None
}
}
}
Err(err) => {
trc::event!(
Acme(AcmeEvent::Error),
Domain = domain.to_string(),
CausedBy = err,
Details = "Failed to unarchive certificate"
);
None
}
},
Err(err) => {
trc::event!(
Acme(AcmeEvent::Error),
Domain = domain.to_string(),
CausedBy = err
);
None
}
Ok(None) => {
trc::event!(Acme(AcmeEvent::TokenNotFound), Domain = domain.to_string());
None
}
}
}
#[inline(always)]
pub fn has_acme_tls_providers(&self) -> bool {
self.core.network.has_acme_tls_challenge
}
#[inline(always)]
pub fn has_acme_http_providers(&self) -> bool {
self.core.network.has_acme_http_challenge
}
}
impl ResolvesServerCert for StaticResolver {
fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
self.key.clone()
}
}
pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> {
let mut challenge = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(StaticResolver { key }));
challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
Arc::new(challenge)
}
pub trait IsTlsAlpnChallenge {
fn is_tls_alpn_challenge(&self) -> bool;
}
impl IsTlsAlpnChallenge for ClientHello<'_> {
fn is_tls_alpn_challenge(&self) -> bool {
self.alpn().into_iter().flatten().eq([ACME_TLS_ALPN_NAME])
}
}