ACME TLS implementation using TLS-ALPN-01 - closes #160
This commit is contained in:
94
crates/utils/src/acme/cache.rs
Normal file
94
crates/utils/src/acme/cache.rs
Normal 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
|
||||
))
|
||||
}
|
||||
}
|
||||
364
crates/utils/src/acme/directory.rs
Normal file
364
crates/utils/src/acme/directory.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
140
crates/utils/src/acme/jose.rs
Normal file
140
crates/utils/src/acme/jose.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
206
crates/utils/src/acme/mod.rs
Normal file
206
crates/utils/src/acme/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
300
crates/utils/src/acme/order.rs
Normal file
300
crates/utils/src/acme/order.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
81
crates/utils/src/acme/resolver.rs
Normal file
81
crates/utils/src/acme/resolver.rs
Normal 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])
|
||||
}
|
||||
}
|
||||
@@ -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\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
190
crates/utils/src/config/tls.rs
Normal file
190
crates/utils/src/config/tls.rs
Normal 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",
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
200
crates/utils/src/listener/tls.rs
Normal file
200
crates/utils/src/listener/tls.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user