ACME TLS implementation using TLS-ALPN-01 - closes #160

This commit is contained in:
mdecimus
2024-01-05 18:44:22 +01:00
parent 172c8afae0
commit ffba9b5a61
43 changed files with 2285 additions and 464 deletions

View File

@@ -0,0 +1,94 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{io::ErrorKind, path::PathBuf};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ring::digest::{Context, SHA512};
use super::{AcmeError, AcmeManager};
impl AcmeManager {
pub(crate) async fn load_cert(&self) -> Result<Option<Vec<u8>>, AcmeError> {
self.read_if_exists("cert", self.domains.as_slice())
.await
.map_err(AcmeError::CertCacheLoad)
}
pub(crate) async fn store_cert(&self, cert: &[u8]) -> Result<(), AcmeError> {
self.write("cert", self.domains.as_slice(), cert)
.await
.map_err(AcmeError::CertCacheStore)
}
pub(crate) async fn load_account(&self) -> Result<Option<Vec<u8>>, AcmeError> {
self.read_if_exists("key", self.contact.as_slice())
.await
.map_err(AcmeError::AccountCacheLoad)
}
pub(crate) async fn store_account(&self, account: &[u8]) -> Result<(), AcmeError> {
self.write("key", self.contact.as_slice(), account)
.await
.map_err(AcmeError::AccountCacheStore)
}
async fn read_if_exists(
&self,
class: &str,
items: &[String],
) -> Result<Option<Vec<u8>>, std::io::Error> {
match tokio::fs::read(self.build_filename(class, items)).await {
Ok(content) => Ok(Some(content)),
Err(err) => match err.kind() {
ErrorKind::NotFound => Ok(None),
_ => Err(err),
},
}
}
async fn write(
&self,
class: &str,
items: &[String],
contents: impl AsRef<[u8]>,
) -> Result<(), std::io::Error> {
tokio::fs::create_dir_all(&self.cache_path).await?;
tokio::fs::write(self.build_filename(class, items), contents.as_ref()).await
}
fn build_filename(&self, class: &str, items: &[String]) -> PathBuf {
let mut ctx = Context::new(&SHA512);
for el in items {
ctx.update(el.as_ref());
ctx.update(&[0])
}
ctx.update(self.directory_url.as_bytes());
self.cache_path.join(format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(ctx.finish()),
class
))
}
}

View File

@@ -0,0 +1,364 @@
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use std::time::Duration;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use rcgen::{Certificate, CustomExtension, PKCS_ECDSA_P256_SHA256};
use reqwest::header::{ToStrError, CONTENT_TYPE};
use reqwest::{Method, Response, StatusCode};
use ring::error::{KeyRejected, Unspecified};
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING};
use rustls::crypto::ring::sign::any_ecdsa_type;
use rustls::sign::CertifiedKey;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::jose::{key_authorization_sha256, sign, JoseError};
pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str =
"https://acme-staging-v02.api.letsencrypt.org/directory";
pub const LETS_ENCRYPT_PRODUCTION_DIRECTORY: &str =
"https://acme-v02.api.letsencrypt.org/directory";
pub const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1";
#[derive(Debug)]
pub struct Account {
pub key_pair: EcdsaKeyPair,
pub directory: Directory,
pub kid: String,
}
static ALG: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_FIXED_SIGNING;
impl Account {
pub fn generate_key_pair() -> Vec<u8> {
EcdsaKeyPair::generate_pkcs8(ALG, &SystemRandom::new())
.unwrap()
.as_ref()
.to_vec()
}
pub async fn create<'a, S, I>(directory: Directory, contact: I) -> Result<Self, DirectoryError>
where
S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>,
{
Self::create_with_keypair(directory, contact, &Self::generate_key_pair()).await
}
pub async fn create_with_keypair<'a, S, I>(
directory: Directory,
contact: I,
key_pair: &[u8],
) -> Result<Self, DirectoryError>
where
S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>,
{
let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())?;
let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
let payload = json!({
"termsOfServiceAgreed": true,
"contact": contact,
})
.to_string();
let body = sign(
&key_pair,
None,
directory.nonce().await?,
&directory.new_account,
&payload,
)?;
let response = https(&directory.new_account, Method::POST, Some(body)).await?;
let kid = get_header(&response, "Location")?;
Ok(Account {
key_pair,
kid,
directory,
})
}
async fn request(
&self,
url: impl AsRef<str>,
payload: &str,
) -> Result<(Option<String>, String), DirectoryError> {
let body = sign(
&self.key_pair,
Some(&self.kid),
self.directory.nonce().await?,
url.as_ref(),
payload,
)?;
let response = https(url.as_ref(), Method::POST, Some(body)).await?;
let location = get_header(&response, "Location").ok();
let body = response.text().await?;
Ok((location, body))
}
pub async fn new_order(&self, domains: Vec<String>) -> Result<(String, Order), DirectoryError> {
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
let response = self.request(&self.directory.new_order, &payload).await?;
let url = response
.0
.ok_or(DirectoryError::MissingHeader("Location"))?;
let order = serde_json::from_str(&response.1)?;
Ok((url, order))
}
pub async fn auth(&self, url: impl AsRef<str>) -> Result<Auth, DirectoryError> {
let response = self.request(url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into)
}
pub async fn challenge(&self, url: impl AsRef<str>) -> Result<(), DirectoryError> {
self.request(&url, "{}").await.map(|_| ())
}
pub async fn order(&self, url: impl AsRef<str>) -> Result<Order, DirectoryError> {
let response = self.request(&url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into)
}
pub async fn finalize(
&self,
url: impl AsRef<str>,
csr: Vec<u8>,
) -> Result<Order, DirectoryError> {
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr));
let response = self.request(&url, &payload).await?;
serde_json::from_str(&response.1).map_err(Into::into)
}
pub async fn certificate(&self, url: impl AsRef<str>) -> Result<String, DirectoryError> {
Ok(self.request(&url, "").await?.1)
}
pub fn tls_alpn_01<'a>(
&self,
challenges: &'a [Challenge],
domain: String,
) -> Result<(&'a Challenge, CertifiedKey), DirectoryError> {
let challenge = challenges
.iter()
.find(|c| c.typ == ChallengeType::TlsAlpn01);
let challenge = match challenge {
Some(challenge) => challenge,
None => return Err(DirectoryError::NoTlsAlpn01Challenge),
};
let mut params = rcgen::CertificateParams::new(vec![domain]);
let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
params.alg = &PKCS_ECDSA_P256_SHA256;
params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
let cert = Certificate::from_params(params)?;
let pk = any_ecdsa_type(&PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
cert.serialize_private_key_der(),
)))
.unwrap();
let certified_key =
CertifiedKey::new(vec![CertificateDer::from(cert.serialize_der()?)], pk);
Ok((challenge, certified_key))
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
pub new_nonce: String,
pub new_account: String,
pub new_order: String,
}
impl Directory {
pub async fn discover(url: impl AsRef<str>) -> Result<Self, DirectoryError> {
Ok(serde_json::from_str(
&https(url, Method::GET, None).await?.text().await?,
)?)
}
pub async fn nonce(&self) -> Result<String, DirectoryError> {
get_header(
&https(&self.new_nonce.as_str(), Method::HEAD, None).await?,
"replay-nonce",
)
}
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
pub enum ChallengeType {
#[serde(rename = "http-01")]
Http01,
#[serde(rename = "dns-01")]
Dns01,
#[serde(rename = "tls-alpn-01")]
TlsAlpn01,
}
#[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>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AuthStatus {
Pending,
Valid,
Invalid,
Revoked,
Expired,
Deactivated,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Identifier {
Dns(String),
}
#[derive(Debug, Deserialize)]
pub struct Challenge {
#[serde(rename = "type")]
pub typ: ChallengeType,
pub url: String,
pub token: String,
pub error: Option<Problem>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Problem {
#[serde(rename = "type")]
pub typ: Option<String>,
pub detail: Option<String>,
}
#[derive(Debug)]
pub enum DirectoryError {
Io(std::io::Error),
Rcgen(rcgen::Error),
Jose(JoseError),
Json(serde_json::Error),
HttpRequest(reqwest::Error),
HttpRequestCode { code: StatusCode, reason: String },
HttpResponseNonStringHeader(ToStrError),
KeyRejected(KeyRejected),
Crypto(Unspecified),
MissingHeader(&'static str),
NoTlsAlpn01Challenge,
}
async fn https(
url: impl AsRef<str>,
method: Method,
body: Option<String>,
) -> Result<Response, DirectoryError> {
let url = url.as_ref();
let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(30));
#[cfg(debug_assertions)]
{
builder = builder.danger_accept_invalid_certs(
url.starts_with("https://localhost") || url.starts_with("https://127.0.0.1"),
);
}
let mut request = builder.build()?.request(method, url);
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 {
Err(DirectoryError::HttpRequestCode {
code: response.status(),
reason: response.text().await?,
})
}
}
fn get_header(response: &Response, header: &'static str) -> Result<String, DirectoryError> {
match response.headers().get_all(header).iter().last() {
Some(value) => Ok(value.to_str()?.to_string()),
None => Err(DirectoryError::MissingHeader(header)),
}
}
impl From<std::io::Error> for DirectoryError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
impl From<rcgen::Error> for DirectoryError {
fn from(err: rcgen::Error) -> Self {
Self::Rcgen(err)
}
}
impl From<JoseError> for DirectoryError {
fn from(err: JoseError) -> Self {
Self::Jose(err)
}
}
impl From<serde_json::Error> for DirectoryError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
impl From<reqwest::Error> for DirectoryError {
fn from(err: reqwest::Error) -> Self {
Self::HttpRequest(err)
}
}
impl From<KeyRejected> for DirectoryError {
fn from(err: KeyRejected) -> Self {
Self::KeyRejected(err)
}
}
impl From<Unspecified> for DirectoryError {
fn from(err: Unspecified) -> Self {
Self::Crypto(err)
}
}
impl From<ToStrError> for DirectoryError {
fn from(err: ToStrError) -> Self {
Self::HttpResponseNonStringHeader(err)
}
}

View File

@@ -0,0 +1,140 @@
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use ring::digest::{digest, Digest, SHA256};
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair};
use serde::Serialize;
pub(crate) fn sign(
key: &EcdsaKeyPair,
kid: Option<&str>,
nonce: String,
url: &str,
payload: &str,
) -> Result<String, JoseError> {
let jwk = match kid {
None => Some(Jwk::new(key)),
Some(_) => None,
};
let protected = Protected::base64(jwk, kid, nonce, url)?;
let payload = URL_SAFE_NO_PAD.encode(payload);
let combined = format!("{}.{}", &protected, &payload);
let signature = key.sign(&SystemRandom::new(), combined.as_bytes())?;
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
let body = Body {
protected,
payload,
signature,
};
Ok(serde_json::to_string(&body)?)
}
pub(crate) fn key_authorization_sha256(
key: &EcdsaKeyPair,
token: &str,
) -> Result<Digest, JoseError> {
let jwk = Jwk::new(key);
let key_authorization = format!("{}.{}", token, jwk.thumb_sha256_base64()?);
Ok(digest(&SHA256, key_authorization.as_bytes()))
}
#[derive(Serialize)]
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>,
nonce: String,
url: &'a str,
}
impl<'a> Protected<'a> {
fn base64(
jwk: Option<Jwk>,
kid: Option<&'a str>,
nonce: String,
url: &'a str,
) -> Result<String, JoseError> {
let protected = Self {
alg: "ES256",
jwk,
kid,
nonce,
url,
};
let protected = serde_json::to_vec(&protected)?;
Ok(URL_SAFE_NO_PAD.encode(protected))
}
}
#[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 thumb_sha256_base64(&self) -> Result<String, JoseError> {
let jwk_thumb = JwkThumb {
crv: self.crv,
kty: self.kty,
x: &self.x,
y: &self.y,
};
let json = serde_json::to_vec(&jwk_thumb)?;
let hash = digest(&SHA256, &json);
Ok(URL_SAFE_NO_PAD.encode(hash))
}
}
#[derive(Serialize)]
struct JwkThumb<'a> {
crv: &'a str,
kty: &'a str,
x: &'a str,
y: &'a str,
}
#[derive(Debug)]
pub enum JoseError {
Json(serde_json::Error),
Crypto(ring::error::Unspecified),
}
impl From<serde_json::Error> for JoseError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
impl From<ring::error::Unspecified> for JoseError {
fn from(err: ring::error::Unspecified) -> Self {
Self::Crypto(err)
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
pub mod cache;
pub mod directory;
pub mod jose;
pub mod order;
pub mod resolver;
use std::{
fmt::Debug,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use ahash::AHashMap;
use arc_swap::ArcSwap;
use parking_lot::Mutex;
use rustls::sign::CertifiedKey;
use tokio::sync::watch;
use crate::config::tls::build_self_signed_cert;
use self::{
directory::Account,
order::{CertParseError, OrderError},
};
pub struct AcmeManager {
pub(crate) directory_url: String,
pub(crate) domains: Vec<String>,
contact: Vec<String>,
renew_before: chrono::Duration,
cache_path: PathBuf,
account_key: ArcSwap<Vec<u8>>,
auth_keys: Mutex<AHashMap<String, Arc<CertifiedKey>>>,
order_in_progress: AtomicBool,
cert: ArcSwap<CertifiedKey>,
}
#[derive(Debug)]
pub enum AcmeError {
CertCacheLoad(std::io::Error),
AccountCacheLoad(std::io::Error),
CertCacheStore(std::io::Error),
AccountCacheStore(std::io::Error),
CachedCertParse(CertParseError),
Order(OrderError),
NewCertParse(CertParseError),
}
impl AcmeManager {
pub fn new(
directory_url: String,
domains: Vec<String>,
contact: Vec<String>,
renew_before: Duration,
cache_path: PathBuf,
) -> crate::config::Result<Self> {
Ok(AcmeManager {
directory_url,
contact: contact
.into_iter()
.map(|c| {
if !c.starts_with("mailto:") {
format!("mailto:{}", c)
} else {
c
}
})
.collect(),
renew_before: chrono::Duration::from_std(renew_before).unwrap(),
cache_path,
account_key: ArcSwap::from_pointee(Vec::new()),
auth_keys: Mutex::new(AHashMap::new()),
order_in_progress: false.into(),
cert: ArcSwap::from_pointee(build_self_signed_cert(&domains)?),
domains,
})
}
pub async fn init(&self) -> Result<Duration, AcmeError> {
// Load account key from cache or generate a new one
if let Some(account_key) = self.load_account().await? {
self.account_key.store(Arc::new(account_key));
} else {
let account_key = Account::generate_key_pair();
self.store_account(&account_key).await?;
self.account_key.store(Arc::new(account_key));
}
// Load certificate from cache or request a new one
Ok(if let Some(pem) = self.load_cert().await? {
self.process_cert(pem, true).await?
} else {
Duration::from_millis(1000)
})
}
pub fn has_order_in_progress(&self) -> bool {
self.order_in_progress.load(Ordering::Relaxed)
}
}
pub trait SpawnAcme {
fn spawn(self, shutdown_rx: watch::Receiver<bool>);
}
impl SpawnAcme for Arc<AcmeManager> {
fn spawn(self, mut shutdown_rx: watch::Receiver<bool>) {
tokio::spawn(async move {
let acme = self;
let mut renew_at = match acme.init().await {
Ok(renew_at) => renew_at,
Err(err) => {
tracing::error!(
context = "acme",
event = "error",
error = ?err,
"Failed to initialize ACME certificate manager.");
return;
}
};
loop {
tokio::select! {
_ = tokio::time::sleep(renew_at) => {
tracing::info!(
context = "acme",
event = "order",
domains = ?acme.domains,
"Ordering certificates.");
match acme.renew().await {
Ok(renew_at_) => {
renew_at = renew_at_;
tracing::info!(
context = "acme",
event = "success",
domains = ?acme.domains,
next_renewal = ?renew_at,
"Certificates renewed.");
},
Err(err) => {
tracing::error!(
context = "acme",
event = "error",
error = ?err,
"Failed to renew certificates.");
renew_at = Duration::from_secs(3600);
},
}
},
_ = shutdown_rx.changed() => {
tracing::debug!(
context = "acme",
event = "shutdown",
domains = ?acme.domains,
"ACME certificate manager shutting down.");
break;
}
};
}
});
}
}
impl Debug for AcmeManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcmeManager")
.field("directory_url", &self.directory_url)
.field("domains", &self.domains)
.field("contact", &self.contact)
.field("cache_path", &self.cache_path)
.field("account_key", &self.account_key)
.finish()
}
}

View File

@@ -0,0 +1,300 @@
// Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
use chrono::{DateTime, TimeZone, Utc};
use futures::future::try_join_all;
use rcgen::{CertificateParams, DistinguishedName, PKCS_ECDSA_P256_SHA256};
use rustls::crypto::ring::sign::any_ecdsa_type;
use rustls::sign::CertifiedKey;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::fmt::Debug;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use x509_parser::parse_x509_certificate;
use crate::acme::directory::Identifier;
use super::directory::{Account, Auth, AuthStatus, Directory, DirectoryError, Order, OrderStatus};
use super::jose::JoseError;
use super::{AcmeError, AcmeManager};
#[derive(Debug)]
pub enum OrderError {
Acme(DirectoryError),
Rcgen(rcgen::Error),
BadOrder(Order),
BadAuth(Auth),
TooManyAttemptsAuth(String),
ProcessingTimeout(Order),
}
#[derive(Debug)]
pub enum CertParseError {
X509(x509_parser::nom::Err<x509_parser::error::X509Error>),
Pem(pem::PemError),
TooFewPem(usize),
InvalidPrivateKey,
}
impl AcmeManager {
pub(crate) async fn process_cert(
&self,
pem: Vec<u8>,
cached: bool,
) -> Result<Duration, AcmeError> {
let (cert, validity) = match (parse_cert(&pem), cached) {
(Ok(r), _) => r,
(Err(err), cached) => {
return match cached {
true => Err(AcmeError::CachedCertParse(err)),
false => Err(AcmeError::NewCertParse(err)),
}
}
};
self.set_cert(Arc::new(cert));
let renew_at = (validity[1] - self.renew_before - Utc::now())
.max(chrono::Duration::zero())
.to_std()
.unwrap_or_default();
let renewal_date = validity[1] - self.renew_before;
tracing::info!(
context = "acme",
event = "process-cert",
valid_not_before = %validity[0],
valid_not_after = %validity[1],
renewal_date = ?renewal_date,
domains = ?self.domains,
"Loaded certificate for domains {:?}", self.domains);
if !cached {
self.store_cert(&pem).await?;
}
Ok(renew_at)
}
pub async fn renew(&self) -> Result<Duration, AcmeError> {
let mut backoff = 0;
self.order_in_progress.store(true, Ordering::Relaxed);
loop {
match self.order().await {
Ok(pem) => return self.process_cert(pem, false).await,
Err(err) if backoff < 16 => {
tracing::debug!(
context = "acme",
event = "renew-backoff",
domains = ?self.domains,
attempt = backoff,
reason = ?err,
"Failed to renew certificate, backing off for {} seconds",
1 << backoff);
backoff = (backoff + 1).min(16);
tokio::time::sleep(Duration::from_secs(1 << backoff)).await;
}
Err(err) => return Err(AcmeError::Order(err)),
}
}
}
async fn order(&self) -> Result<Vec<u8>, OrderError> {
let directory = Directory::discover(&self.directory_url).await?;
let account = Account::create_with_keypair(
directory,
&self.contact,
self.account_key.load().as_slice(),
)
.await?;
let mut params = CertificateParams::new(self.domains.clone());
params.distinguished_name = DistinguishedName::new();
params.alg = &PKCS_ECDSA_P256_SHA256;
let cert = rcgen::Certificate::from_params(params)?;
let (order_url, mut order) = account.new_order(self.domains.clone()).await?;
loop {
match order.status {
OrderStatus::Pending => {
let auth_futures = order
.authorizations
.iter()
.map(|url| self.authorize(&account, url));
try_join_all(auth_futures).await?;
tracing::info!(
context = "acme",
event = "auth-complete",
domains = ?self.domains.as_slice(),
"Completed all authorizations"
);
order = account.order(&order_url).await?;
}
OrderStatus::Processing => {
for i in 0u64..10 {
tracing::info!(
context = "acme",
event = "processing",
domains = ?self.domains.as_slice(),
attempt = i,
"Processing order"
);
tokio::time::sleep(Duration::from_secs(1u64 << i)).await;
order = account.order(&order_url).await?;
if order.status != OrderStatus::Processing {
break;
}
}
if order.status == OrderStatus::Processing {
return Err(OrderError::ProcessingTimeout(order));
}
}
OrderStatus::Ready => {
tracing::info!(
context = "acme",
event = "csr-send",
domains = ?self.domains.as_slice(),
"Sending CSR"
);
let csr = cert.serialize_request_der()?;
order = account.finalize(order.finalize, csr).await?
}
OrderStatus::Valid { certificate } => {
tracing::info!(
context = "acme",
event = "download",
domains = ?self.domains.as_slice(),
"Downloading certificate"
);
let pem = [
&cert.serialize_private_key_pem(),
"\n",
&account.certificate(certificate).await?,
]
.concat();
return Ok(pem.into_bytes());
}
OrderStatus::Invalid => {
tracing::warn!(
context = "acme",
event = "error",
reason = "invalid-order",
domains = ?self.domains.as_slice(),
"Invalid order"
);
return Err(OrderError::BadOrder(order));
}
}
}
}
async fn authorize(&self, account: &Account, url: &String) -> Result<(), OrderError> {
let auth = account.auth(url).await?;
let (domain, challenge_url) = match auth.status {
AuthStatus::Pending => {
let Identifier::Dns(domain) = auth.identifier;
tracing::info!(
context = "acme",
event = "challenge",
domain = domain,
"Requesting challenge for domain {domain}"
);
let (challenge, auth_key) =
account.tls_alpn_01(&auth.challenges, domain.clone())?;
self.set_auth_key(domain.clone(), Arc::new(auth_key));
account.challenge(&challenge.url).await?;
(domain, challenge.url.clone())
}
AuthStatus::Valid => return Ok(()),
_ => return Err(OrderError::BadAuth(auth)),
};
for i in 0u64..5 {
tokio::time::sleep(Duration::from_secs(1u64 << i)).await;
let auth = account.auth(url).await?;
match auth.status {
AuthStatus::Pending => {
tracing::info!(
context = "acme",
event = "auth-pending",
domain = domain,
attempt = i,
"Authorization for domain {domain} is still pending",
);
account.challenge(&challenge_url).await?
}
AuthStatus::Valid => return Ok(()),
_ => return Err(OrderError::BadAuth(auth)),
}
}
Err(OrderError::TooManyAttemptsAuth(domain))
}
}
fn parse_cert(pem: &[u8]) -> Result<(CertifiedKey, [DateTime<Utc>; 2]), CertParseError> {
let mut pems = pem::parse_many(pem)?;
if pems.len() < 2 {
return Err(CertParseError::TooFewPem(pems.len()));
}
let pk = match any_ecdsa_type(&PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
pems.remove(0).contents(),
))) {
Ok(pk) => pk,
Err(_) => return Err(CertParseError::InvalidPrivateKey),
};
let cert_chain: Vec<CertificateDer> = pems
.into_iter()
.map(|p| CertificateDer::from(p.into_contents()))
.collect();
let validity = match parse_x509_certificate(&cert_chain[0]) {
Ok((_, cert)) => {
let validity = cert.validity();
[validity.not_before, validity.not_after].map(|t| {
Utc.timestamp_opt(t.timestamp(), 0)
.earliest()
.unwrap_or_default()
})
}
Err(err) => return Err(CertParseError::X509(err)),
};
let cert = CertifiedKey::new(cert_chain, pk);
Ok((cert, validity))
}
impl From<DirectoryError> for OrderError {
fn from(err: DirectoryError) -> Self {
Self::Acme(err)
}
}
impl From<rcgen::Error> for OrderError {
fn from(err: rcgen::Error) -> Self {
Self::Rcgen(err)
}
}
impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for CertParseError {
fn from(err: x509_parser::nom::Err<x509_parser::error::X509Error>) -> Self {
Self::X509(err)
}
}
impl From<pem::PemError> for CertParseError {
fn from(err: pem::PemError) -> Self {
Self::Pem(err)
}
}
impl From<JoseError> for OrderError {
fn from(err: JoseError) -> Self {
Self::Acme(DirectoryError::Jose(err))
}
}
impl From<JoseError> for AcmeError {
fn from(err: JoseError) -> Self {
Self::Order(OrderError::from(err))
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::sync::{atomic::Ordering, Arc};
use rustls::{
server::{ClientHello, ResolvesServerCert},
sign::CertifiedKey,
};
use super::{directory::ACME_TLS_ALPN_NAME, AcmeManager};
impl AcmeManager {
pub(crate) fn set_cert(&self, cert: Arc<CertifiedKey>) {
self.cert.store(cert);
self.order_in_progress.store(false, Ordering::Relaxed);
self.auth_keys.lock().clear();
}
pub(crate) fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
self.auth_keys.lock().insert(domain, cert);
}
}
impl ResolvesServerCert for AcmeManager {
fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
if self.has_order_in_progress() && client_hello.is_tls_alpn_challenge() {
match client_hello.server_name() {
None => {
tracing::debug!(
context = "acme",
event = "error",
reason = "missing-sni",
"client did not supply SNI"
);
None
}
Some(domain) => {
tracing::trace!(
context = "acme",
event = "auth-key",
domain = %domain,
"Found client supplied SNI");
self.auth_keys.lock().get(domain).cloned()
}
}
} else {
self.cert.load().clone().into()
}
}
}
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])
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{io::Cursor, sync::Arc};
use rustls::{
server::{ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni},
sign::CertifiedKey,
version::{TLS12, TLS13},
SupportedProtocolVersion,
};
use rustls_pemfile::{certs, read_one, Item};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use super::Config;
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
#[derive(Debug)]
pub struct CertificateResolver {
pub resolver: Option<ResolvesServerCertUsingSni>,
pub default_cert: Option<Arc<CertifiedKey>>,
}
impl ResolvesServerCert for CertificateResolver {
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
self.resolver
.as_ref()
.and_then(|r| r.resolve(hello))
.or_else(|| self.default_cert.clone())
}
}
impl Config {
pub fn rustls_certificate(&self, cert_id: &str) -> super::Result<Vec<CertificateDer<'static>>> {
let certs = certs(&mut Cursor::new(self.file_contents((
"certificate",
cert_id,
"cert",
))?))
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
format!("Failed to read certificates in \"certificate.{cert_id}.cert\": {err}")
})?;
if !certs.is_empty() {
Ok(certs)
} else {
Err(format!(
"No certificates found in \"certificate.{cert_id}.cert\"."
))
}
}
pub fn rustls_private_key(&self, cert_id: &str) -> super::Result<PrivateKeyDer<'static>> {
match read_one(&mut Cursor::new(self.file_contents((
"certificate",
cert_id,
"private-key",
))?))
.map_err(|err| {
format!("Failed to read private keys in \"certificate.{cert_id}.private-key\": {err}",)
})?
.into_iter()
.next()
{
Some(Item::Pkcs8Key(key)) => Ok(PrivateKeyDer::Pkcs8(key)),
Some(Item::Pkcs1Key(key)) => Ok(PrivateKeyDer::Pkcs1(key)),
Some(Item::Sec1Key(key)) => Ok(PrivateKeyDer::Sec1(key)),
Some(_) => Err(format!(
"Unsupported private keys found in \"certificate.{cert_id}.private-key\".",
)),
None => Err(format!(
"No private keys found in \"certificate.{cert_id}.private-key\".",
)),
}
}
}

View File

@@ -23,6 +23,7 @@
use std::{net::SocketAddr, sync::Arc};
use ahash::AHashMap;
use rustls::{
crypto::ring::{
cipher_suite::{
@@ -32,173 +33,87 @@ use rustls::{
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
},
default_provider,
sign::any_supported_type,
},
server::ResolvesServerCertUsingSni,
sign::CertifiedKey,
server::ResolvesServerCert,
ServerConfig, SupportedCipherSuite, ALL_VERSIONS,
};
use tokio::net::TcpSocket;
use tokio_rustls::TlsAcceptor;
use crate::UnwrapFailure;
use crate::{
acme::{directory::ACME_TLS_ALPN_NAME, AcmeManager},
listener::{
tls::{Certificate, CertificateResolver},
TcpAcceptor,
},
UnwrapFailure,
};
use super::{
certificate::{CertificateResolver, TLS12_VERSION, TLS13_VERSION},
tls::{TLS12_VERSION, TLS13_VERSION},
utils::{AsKey, ParseKey, ParseValue},
Config, Listener, Server, ServerProtocol, Servers,
};
impl Config {
pub fn parse_servers(&self) -> super::Result<Servers> {
let mut servers: Vec<Server> = Vec::new();
// Parse certificates and ACME managers
let certificates = self.parse_certificates()?;
let acmes = self.parse_acmes()?;
// Parse servers
let mut servers = Servers::default();
for (internal_id, id) in self.sub_keys("server.listener").enumerate() {
let mut server = self.parse_server(id)?;
if !servers.iter().any(|s| s.id == server.id) {
let mut server = self.parse_server(id, &certificates, &acmes)?;
if !servers.inner.iter().any(|s| s.id == server.id) {
server.internal_id = internal_id as u16;
servers.push(server);
servers.inner.push(server);
} else {
return Err(format!("Duplicate listener id {:?}.", server.id));
}
}
if !servers.is_empty() {
Ok(Servers { inner: servers })
// Add certificates with valid paths
for (id, cert) in certificates {
if cert.path.len() == 2 {
servers.certificates.push(cert);
} else {
tracing::debug!(
context = "config",
event = "acme",
id = id,
"Certificate reloading disabled for id {id:?}",
);
}
}
// Add ACME managers with configured domains
for (id, acme) in acmes {
if !acme.domains.is_empty() {
servers.acme_managers.push(acme);
} else {
tracing::debug!(
context = "config",
event = "acme",
id = id,
"ACME certificate manager disabled for id {id:?}",
);
}
}
if !servers.inner.is_empty() {
Ok(servers)
} else {
Err("No server directives found in config file.".to_string())
}
}
fn parse_server(&self, id: &str) -> super::Result<Server> {
// Build TLS config
let (tls, tls_implicit) = if self
.property_or_default(("server.listener", id, "tls.enable"), "server.tls.enable")?
.unwrap_or(false)
{
// Parse protocol versions
let mut tls_v2 = false;
let mut tls_v3 = false;
for (key, protocol) in self.values_or_default(
("server.listener", id, "tls.protocols"),
"server.tls.protocols",
) {
match protocol {
"TLSv1.2" | "0x0303" => tls_v2 = true,
"TLSv1.3" | "0x0304" => tls_v3 = true,
protocol => {
return Err(format!(
"Unsupported TLS protocol {protocol:?} found in key {key:?}",
))
}
}
}
// Parse cipher suites
let mut ciphers: Vec<SupportedCipherSuite> = Vec::new();
for (key, protocol) in
self.values_or_default(("server.listener", id, "tls.ciphers"), "server.tls.ciphers")
{
ciphers.push(protocol.parse_key(key)?);
}
// Obtain default certificate
let cert_id = self
.value_or_default(
("server.listener", id, "tls.certificate"),
"server.tls.certificate",
)
.ok_or_else(|| format!("Undefined certificate id for listener {id:?}."))?;
let cert = self.rustls_certificate(cert_id)?;
let pki = self.rustls_private_key(cert_id)?;
// Add SNI certificates
let mut resolver = ResolvesServerCertUsingSni::new();
let mut has_sni = false;
for (key, value) in
self.values_or_default(("server.listener", id, "tls.sni"), "server.tls.sni")
{
if let Some(prefix) = key.strip_suffix(".subject") {
has_sni = true;
resolver
.add(
value,
match self.value((prefix, "certificate")) {
Some(sni_cert_id) if sni_cert_id != cert_id => CertifiedKey {
cert: self.rustls_certificate(sni_cert_id)?,
key: any_supported_type(&self.rustls_private_key(sni_cert_id)?)
.map_err(|err| {
format!(
"Failed to sign SNI certificate for {key:?}: {err}",
)
})?,
ocsp: None,
},
_ => CertifiedKey {
cert: cert.clone(),
key:
any_supported_type(&pki).map_err(|err| {
format!(
"Failed to sign SNI certificate for {key:?}: {err}",
)
})?,
ocsp: None,
},
},
)
.map_err(|err| {
format!("Failed to add SNI certificate for {key:?}: {err}")
})?;
}
}
// Add default certificate
let default_cert = Some(Arc::new(CertifiedKey {
cert,
key: any_supported_type(&pki)
.map_err(|err| format!("Failed to sign certificate id {cert_id:?}: {err}"))?,
ocsp: None,
}));
// Build cert provider
let mut provider = default_provider();
if !ciphers.is_empty() {
provider.cipher_suites = ciphers;
}
// Build server config
let mut config = ServerConfig::builder_with_provider(provider.into())
.with_protocol_versions(if tls_v3 == tls_v2 {
ALL_VERSIONS
} else if tls_v3 {
TLS13_VERSION
} else {
TLS12_VERSION
})
.map_err(|err| format!("Failed to build TLS config: {err}"))?
.with_no_client_auth()
.with_cert_resolver(Arc::new(CertificateResolver {
resolver: if has_sni { resolver.into() } else { None },
default_cert,
}));
//config.key_log = Arc::new(KeyLogger::default());
config.ignore_client_order = self
.property_or_default(
("server.listener", id, "tls.ignore-client-order"),
"server.tls.ignore-client-order",
)?
.unwrap_or(true);
(
config.into(),
self.property_or_default(
("server.listener", id, "tls.implicit"),
"server.tls.implicit",
)?
.unwrap_or(true),
)
} else {
(None, false)
};
fn parse_server(
&self,
id: &str,
certificates: &AHashMap<String, Arc<Certificate>>,
acmes: &AHashMap<String, Arc<AcmeManager>>,
) -> super::Result<Server> {
// Build listeners
let mut listeners = Vec::new();
for result in self.properties::<SocketAddr>(("server.listener", id, "bind")) {
@@ -267,6 +182,150 @@ impl Config {
return Err(format!("No 'bind' directive found for listener id {id:?}"));
}
// Build TLS config
let (acceptor, tls_implicit) = if self
.property_or_default(("server.listener", id, "tls.enable"), "server.tls.enable")?
.unwrap_or(false)
{
// Parse protocol versions
let mut tls_v2 = false;
let mut tls_v3 = false;
for (key, protocol) in self.values_or_default(
("server.listener", id, "tls.protocols"),
"server.tls.protocols",
) {
match protocol {
"TLSv1.2" | "0x0303" => tls_v2 = true,
"TLSv1.3" | "0x0304" => tls_v3 = true,
protocol => {
return Err(format!(
"Unsupported TLS protocol {protocol:?} found in key {key:?}",
))
}
}
}
// Parse cipher suites
let mut ciphers: Vec<SupportedCipherSuite> = Vec::new();
for (key, protocol) in
self.values_or_default(("server.listener", id, "tls.ciphers"), "server.tls.ciphers")
{
ciphers.push(protocol.parse_key(key)?);
}
// Build resolver
let mut acme_acceptor = None;
let resolver: Arc<dyn ResolvesServerCert> = if let Some(acme_id) =
self.value_or_default(("server.listener", id, "tls.acme"), "server.tls.acme")
{
let acme = acmes.get(acme_id).ok_or_else(|| {
format!("Undefined ACME id {acme_id:?} for listener {id:?}.",)
})?;
// Check if this port is used to receive ACME challenges
let acme_port = self.property_or_static::<u16>(("acme", acme_id, "port"), "443")?;
if listeners.iter().any(|l| l.addr.port() == acme_port) {
acme_acceptor = Some(acme.clone());
}
acme.clone()
} else {
let cert_id = self
.value_or_default(
("server.listener", id, "tls.certificate"),
"server.tls.certificate",
)
.ok_or_else(|| format!("Undefined certificate id for listener {id:?}."))?;
let mut resolver = CertificateResolver {
sni: Default::default(),
cert: certificates
.get(cert_id)
.ok_or_else(|| {
format!("Undefined certificate id {cert_id:?} for listener {id:?}.",)
})?
.clone(),
};
// Add SNI certificates
for (key, value) in
self.values_or_default(("server.listener", id, "tls.sni"), "server.tls.sni")
{
if let Some(prefix) = key.strip_suffix(".subject") {
resolver
.add(
value,
match self.value((prefix, "certificate")) {
Some(sni_cert_id) if sni_cert_id != cert_id => {
certificates.get(sni_cert_id).ok_or_else(|| {
format!(
"Undefined certificate id {sni_cert_id:?} for SNI {value:?} in listener {id:?}.",
)
})?.clone()
}
_ => resolver.cert.clone(),
},
)
.map_err(|err| {
format!("Failed to add SNI certificate for {key:?}: {err}")
})?;
}
}
Arc::new(resolver)
};
// Build cert provider
let mut provider = default_provider();
if !ciphers.is_empty() {
provider.cipher_suites = ciphers;
}
// Build server config
let mut config = ServerConfig::builder_with_provider(provider.into())
.with_protocol_versions(if tls_v3 == tls_v2 {
ALL_VERSIONS
} else if tls_v3 {
TLS13_VERSION
} else {
TLS12_VERSION
})
.map_err(|err| format!("Failed to build TLS config: {err}"))?
.with_no_client_auth()
.with_cert_resolver(resolver.clone());
config.ignore_client_order = self
.property_or_default(
("server.listener", id, "tls.ignore-client-order"),
"server.tls.ignore-client-order",
)?
.unwrap_or(true);
// Build acceptor
let acceptor = if let Some(manager) = acme_acceptor {
let mut challenge = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(resolver);
challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
TcpAcceptor::Acme {
challenge: Arc::new(challenge),
default: Arc::new(config),
manager,
}
} else {
TcpAcceptor::Tls(TlsAcceptor::from(Arc::new(config)))
};
(
acceptor,
self.property_or_default(
("server.listener", id, "tls.implicit"),
"server.tls.implicit",
)?
.unwrap_or(true),
)
} else {
(TcpAcceptor::Plain, false)
};
let protocol = self.property_require(("server.listener", id, "protocol"))?;
Ok(Server {
@@ -303,7 +362,7 @@ impl Config {
.unwrap_or(8192),
protocol,
listeners,
tls,
acceptor,
tls_implicit,
})
}

View File

@@ -21,11 +21,11 @@
* for more details.
*/
pub mod certificate;
pub mod cron;
pub mod dynvalue;
pub mod listener;
pub mod parser;
pub mod tls;
pub mod utils;
use std::{
@@ -33,14 +33,19 @@ use std::{
collections::BTreeMap,
fmt::Display,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
time::Duration,
};
use ahash::{AHashMap, AHashSet};
use rustls::ServerConfig;
use tokio::net::TcpSocket;
use crate::{failed, UnwrapFailure};
use crate::{
acme::AcmeManager,
failed,
listener::{tls::Certificate, TcpAcceptor},
UnwrapFailure,
};
use self::utils::ParseValue;
@@ -57,13 +62,16 @@ pub struct Server {
pub data: String,
pub protocol: ServerProtocol,
pub listeners: Vec<Listener>,
pub tls: Option<ServerConfig>,
pub acceptor: TcpAcceptor,
pub tls_implicit: bool,
pub max_connections: u64,
}
#[derive(Default)]
pub struct Servers {
pub inner: Vec<Server>,
pub certificates: Vec<Arc<Certificate>>,
pub acme_managers: Vec<Arc<AcmeManager>>,
}
#[derive(Debug)]

View File

@@ -0,0 +1,190 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{io::Cursor, path::PathBuf, sync::Arc, time::Duration};
use ahash::AHashMap;
use arc_swap::ArcSwap;
use rcgen::generate_simple_self_signed;
use rustls::{
crypto::ring::sign::any_supported_type,
sign::CertifiedKey,
version::{TLS12, TLS13},
SupportedProtocolVersion,
};
use rustls_pemfile::{certs, read_one, Item};
use rustls_pki_types::PrivateKeyDer;
use crate::{
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeManager},
listener::tls::Certificate,
};
use super::Config;
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
impl Config {
pub fn parse_certificates(&self) -> super::Result<AHashMap<String, Arc<Certificate>>> {
let mut certs = AHashMap::new();
for cert_id in self.sub_keys("certificate") {
let key_cert = ("certificate", cert_id, "cert");
let key_pk = ("certificate", cert_id, "private-key");
let mut cert = Certificate {
cert: ArcSwap::from(Arc::new(build_certified_key(
self.file_contents(key_cert)?,
self.file_contents(key_pk)?,
&format!("certificate.{cert_id}"),
)?)),
path: Vec::with_capacity(2),
};
for key in [key_cert, key_pk] {
if let Some(path) = self.value(key).and_then(|v| v.strip_prefix("file://")) {
cert.path.push(PathBuf::from(path));
}
}
certs.insert(cert_id.to_string(), Arc::new(cert));
}
Ok(certs)
}
pub fn parse_acmes(&self) -> super::Result<AHashMap<String, Arc<AcmeManager>>> {
let mut acmes = AHashMap::new();
for acme_id in self.sub_keys("acme") {
let directory = self
.value(("acme", acme_id, "directory"))
.unwrap_or(LETS_ENCRYPT_PRODUCTION_DIRECTORY)
.trim()
.to_string();
let contact = self
.values(("acme", acme_id, "contact"))
.filter_map(|(_, v)| {
let v = v.trim().to_string();
if !v.is_empty() {
Some(v)
} else {
None
}
})
.collect::<Vec<_>>();
let cache = PathBuf::from(self.value_require(("acme", acme_id, "cache"))?);
if !cache.exists() {
std::fs::create_dir_all(&cache).map_err(|err| {
format!("Failed to create ACME cache directory {:?}: {}", cache, err)
})?;
}
let renew_before: Duration =
self.property_or_static(("acme", acme_id, "renew-before"), "30d")?;
if directory.is_empty() {
return Err(format!("Missing directory for acme.{acme_id}."));
}
if contact.is_empty() {
return Err(format!("Missing contact for acme.{acme_id}."));
}
// Find which domains are covered by this ACME manager
let mut domains = Vec::new();
for id in self.sub_keys("server.listener") {
match (
self.value_or_default(("server.listener", id, "tls.acme"), "server.tls.acme"),
self.value_or_default(("server.listener", id, "hostname"), "server.hostname"),
) {
(Some(listener_acme), Some(hostname)) if listener_acme == acme_id => {
let hostname = hostname.trim().to_lowercase();
if !domains.contains(&hostname) {
domains.push(hostname);
}
}
_ => (),
}
}
acmes.insert(
acme_id.to_string(),
Arc::new(AcmeManager::new(
directory,
domains,
contact,
renew_before,
cache,
)?),
);
}
Ok(acmes)
}
}
pub(crate) fn build_certified_key(
cert: Vec<u8>,
pk: Vec<u8>,
id: &str,
) -> super::Result<CertifiedKey> {
let cert = certs(&mut Cursor::new(cert))
.collect::<Result<Vec<_>, _>>()
.map_err(|err| format!("Failed to read certificates in {id:?}: {err}"))?;
if cert.is_empty() {
return Err(format!("No certificates found in {id:?}."));
}
let pk = match read_one(&mut Cursor::new(pk))
.map_err(|err| format!("Failed to read private keys in {id:?}.: {err}",))?
.into_iter()
.next()
{
Some(Item::Pkcs8Key(key)) => PrivateKeyDer::Pkcs8(key),
Some(Item::Pkcs1Key(key)) => PrivateKeyDer::Pkcs1(key),
Some(Item::Sec1Key(key)) => PrivateKeyDer::Sec1(key),
Some(_) => return Err(format!("Unsupported private keys found in {id:?}.",)),
None => return Err(format!("No private keys found in {id:?}.",)),
};
Ok(CertifiedKey {
cert,
key: any_supported_type(&pk)
.map_err(|err| format!("Failed to sign certificate for {id:?}: {err}",))?,
ocsp: None,
})
}
pub(crate) fn build_self_signed_cert(domains: &[String]) -> super::Result<CertifiedKey> {
let cert = generate_simple_self_signed(domains).map_err(|err| {
format!(
"Failed to generate self-signed certificate for {domains:?}: {err}",
domains = domains
)
})?;
build_certified_key(
cert.serialize_pem().unwrap().into_bytes(),
cert.serialize_private_key_pem().into_bytes(),
"self-signed",
)
}

View File

@@ -25,6 +25,7 @@ use std::{collections::HashMap, sync::Arc};
use config::Config;
pub mod acme;
pub mod codec;
pub mod config;
pub mod ipc;

View File

@@ -28,17 +28,18 @@ use tokio::{
net::{TcpListener, TcpStream},
sync::watch,
};
use tokio_rustls::{server::TlsStream, TlsAcceptor};
use tokio_rustls::server::TlsStream;
use tracing::Span;
use crate::{
acme::SpawnAcme,
config::{Config, Listener, Server, ServerProtocol, Servers},
failed,
listener::SessionData,
UnwrapFailure,
};
use super::{limiter::ConcurrencyLimiter, ServerInstance, SessionManager};
use super::{limiter::ConcurrencyLimiter, ServerInstance, SessionManager, TcpAcceptorResult};
impl Server {
pub fn spawn(self, manager: impl SessionManager, shutdown_rx: watch::Receiver<bool>) {
@@ -53,7 +54,7 @@ impl Server {
listener_id: self.internal_id,
protocol: self.protocol,
hostname: self.hostname,
tls_acceptor: self.tls.map(|config| TlsAcceptor::from(Arc::new(config))),
acceptor: self.acceptor,
is_tls_implicit: self.tls_implicit,
limiter: ConcurrencyLimiter::new(self.max_connections),
shutdown_rx,
@@ -223,6 +224,11 @@ impl Servers {
spawn(server, shutdown_rx.clone());
}
// Spawn ACME managers
for acme_manager in self.acme_managers {
acme_manager.spawn(shutdown_rx.clone());
}
(shutdown_tx, shutdown_rx)
}
}
@@ -241,24 +247,36 @@ impl ServerInstance {
stream: TcpStream,
span: &Span,
) -> Result<TlsStream<TcpStream>, ()> {
match self.tls_acceptor.as_ref().unwrap().accept(stream).await {
Ok(stream) => {
tracing::info!(
parent: span,
context = "tls",
event = "handshake",
version = ?stream.get_ref().1.protocol_version().unwrap_or(rustls::ProtocolVersion::TLSv1_3),
cipher = ?stream.get_ref().1.negotiated_cipher_suite().unwrap_or(TLS13_AES_128_GCM_SHA256),
);
Ok(stream)
}
Err(err) => {
match self.acceptor.accept(stream).await {
TcpAcceptorResult::Tls(accept) => match accept.await {
Ok(stream) => {
tracing::info!(
parent: span,
context = "tls",
event = "handshake",
version = ?stream.get_ref().1.protocol_version().unwrap_or(rustls::ProtocolVersion::TLSv1_3),
cipher = ?stream.get_ref().1.negotiated_cipher_suite().unwrap_or(TLS13_AES_128_GCM_SHA256),
);
Ok(stream)
}
Err(err) => {
tracing::debug!(
parent: span,
context = "tls",
event = "error",
"Failed to accept TLS connection: {}",
err
);
Err(())
}
},
TcpAcceptorResult::Plain(_) | TcpAcceptorResult::Close => {
tracing::debug!(
parent: span,
context = "tls",
event = "error",
"Failed to accept TLS connection: {}",
err
"TLS is not configured for this server."
);
Err(())
}

View File

@@ -23,19 +23,21 @@
use std::{net::IpAddr, sync::Arc};
use crate::{acme::AcmeManager, config::ServerProtocol};
use rustls::ServerConfig;
use std::fmt::Debug;
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
sync::watch,
};
use tokio_rustls::TlsAcceptor;
use crate::config::ServerProtocol;
use tokio_rustls::{Accept, TlsAcceptor};
use self::limiter::{ConcurrencyLimiter, InFlight};
pub mod limiter;
pub mod listen;
pub mod tls;
pub struct ServerInstance {
pub id: String,
@@ -43,12 +45,34 @@ pub struct ServerInstance {
pub protocol: ServerProtocol,
pub hostname: String,
pub data: String,
pub tls_acceptor: Option<TlsAcceptor>,
pub acceptor: TcpAcceptor,
pub is_tls_implicit: bool,
pub limiter: ConcurrencyLimiter,
pub shutdown_rx: watch::Receiver<bool>,
}
#[derive(Default)]
pub enum TcpAcceptor {
Tls(TlsAcceptor),
Acme {
challenge: Arc<ServerConfig>,
default: Arc<ServerConfig>,
manager: Arc<AcmeManager>,
},
#[default]
Plain,
}
#[allow(clippy::large_enum_variant)]
pub enum TcpAcceptorResult<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
Tls(Accept<IO>),
Plain(IO),
Close,
}
pub struct SessionData<T: AsyncRead + AsyncWrite + Unpin + 'static> {
pub stream: T,
pub local_ip: IpAddr,
@@ -63,3 +87,22 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
fn spawn(&self, session: SessionData<TcpStream>);
fn shutdown(&self);
}
impl Debug for TcpAcceptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tls(_) => f.debug_tuple("Tls").finish(),
Self::Acme {
challenge,
default,
manager,
} => f
.debug_struct("Acme")
.field("challenge", challenge)
.field("default", default)
.field("manager", manager)
.finish(),
Self::Plain => write!(f, "Plain"),
}
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{
fmt::{self, Formatter},
path::PathBuf,
sync::Arc,
};
use ahash::AHashMap;
use arc_swap::ArcSwap;
use rustls::{
client::verify_server_name,
server::{ClientHello, ParsedCertificate, ResolvesServerCert},
sign::CertifiedKey,
version::{TLS12, TLS13},
Error, SupportedProtocolVersion,
};
use rustls_pki_types::{DnsName, ServerName};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tokio_rustls::{Accept, LazyConfigAcceptor, TlsAcceptor};
use crate::{acme::resolver::IsTlsAlpnChallenge, config::tls::build_certified_key};
use super::{TcpAcceptor, TcpAcceptorResult};
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
pub struct CertificateResolver {
pub sni: AHashMap<String, Arc<Certificate>>,
pub cert: Arc<Certificate>,
}
pub struct Certificate {
pub cert: ArcSwap<CertifiedKey>,
pub path: Vec<PathBuf>,
}
impl CertificateResolver {
pub fn add(&mut self, name: &str, ck: Arc<Certificate>) -> Result<(), Error> {
let server_name = {
let checked_name = DnsName::try_from(name)
.map_err(|_| Error::General("Bad DNS name".into()))
.map(|name| name.to_lowercase_owned())?;
ServerName::DnsName(checked_name)
};
ck.cert
.load()
.end_entity_cert()
.and_then(ParsedCertificate::try_from)
.and_then(|cert| verify_server_name(&cert, &server_name))?;
if let ServerName::DnsName(name) = server_name {
self.sni.insert(name.as_ref().to_string(), ck);
}
Ok(())
}
}
impl ResolvesServerCert for CertificateResolver {
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
if !self.sni.is_empty() {
if let Some(cert) = hello.server_name().and_then(|name| self.sni.get(name)) {
return cert.cert.load().clone().into();
}
}
self.cert.cert.load().clone().into()
}
}
impl TcpAcceptor {
pub async fn accept<IO>(&self, stream: IO) -> TcpAcceptorResult<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
match self {
TcpAcceptor::Tls(acceptor) => TcpAcceptorResult::Tls(acceptor.accept(stream)),
TcpAcceptor::Acme {
challenge,
default,
manager,
} => {
if manager.has_order_in_progress() {
match LazyConfigAcceptor::new(Default::default(), stream).await {
Ok(start_handshake) => {
if start_handshake.client_hello().is_tls_alpn_challenge() {
match start_handshake.into_stream(challenge.clone()).await {
Ok(mut tls) => {
tracing::debug!(
context = "acme",
event = "validation",
"Received TLS-ALPN-01 validation request."
);
let _ = tls.shutdown().await;
}
Err(err) => {
tracing::info!(
context = "acme",
event = "error",
error = ?err,
"TLS-ALPN-01 validation request failed."
);
}
}
} else {
return TcpAcceptorResult::Tls(
start_handshake.into_stream(default.clone()),
);
}
}
Err(err) => {
tracing::debug!(
context = "listener",
event = "error",
error = ?err,
"TLS handshake failed."
);
}
}
TcpAcceptorResult::Close
} else {
TcpAcceptorResult::Tls(TlsAcceptor::from(default.clone()).accept(stream))
}
}
TcpAcceptor::Plain => TcpAcceptorResult::Plain(stream),
}
}
pub fn is_tls(&self) -> bool {
matches!(self, TcpAcceptor::Tls(_) | TcpAcceptor::Acme { .. })
}
}
impl<IO> TcpAcceptorResult<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
pub fn unwrap_tls(self) -> Accept<IO> {
match self {
TcpAcceptorResult::Tls(accept) => accept,
_ => panic!("unwrap_tls called on non-TLS acceptor"),
}
}
}
impl Certificate {
pub async fn reload(&self) -> crate::config::Result<()> {
let cert = build_certified_key(
tokio::fs::read(&self.path[0]).await.map_err(|err| {
format!(
"Failed to read certificate from path {id:?}: {err}",
id = self.path[0]
)
})?,
tokio::fs::read(&self.path[1]).await.map_err(|err| {
format!(
"Failed to read private key from path {id:?}: {err}",
id = self.path[1]
)
})?,
"certificate",
)?;
self.cert.store(Arc::new(cert));
Ok(())
}
}
impl std::fmt::Debug for CertificateResolver {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("CertificateResolver")
.field("sni", &self.sni.keys())
.field("cert", &self.cert.path)
.finish()
}
}