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
+1
View File
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. This projec
## [0.5.2] - 2024-01-xx
## Added
- ACME support for automatic TLS certificate generation and renewal.
### Changed
Generated
+118 -80
View File
@@ -173,9 +173,15 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.77"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]]
name = "argon2"
@@ -270,7 +276,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -292,18 +298,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
name = "async-trait"
version = "0.1.75"
version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -485,7 +491,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.43",
"syn 2.0.47",
"which",
]
@@ -506,7 +512,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -648,7 +654,7 @@ dependencies = [
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
"syn_derive",
]
@@ -863,9 +869,9 @@ dependencies = [
[[package]]
name = "clang-sys"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
dependencies = [
"glob",
"libc",
@@ -903,7 +909,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1188,7 +1194,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1236,7 +1242,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1258,7 +1264,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core 0.20.3",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1355,9 +1361,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
@@ -1496,7 +1502,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1698,7 +1704,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1838,9 +1844,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form-data"
version = "0.5.1"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8068ad05855697798ecc31d03463899fce290d338ea31644bd6b4ca6d215883"
checksum = "cfaaa4f36921bef661ea6e0ee5453db203a86263c6984be5616eee4e72be68e7"
dependencies = [
"bytes",
"http 1.0.0",
@@ -1899,7 +1905,7 @@ checksum = "83c8d52fe8b46ab822b4decdcc0d6d85aeedfc98f0d52ba2bd4aec4a97807516"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
"try_map",
]
@@ -1937,7 +1943,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e"
dependencies = [
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1949,7 +1955,7 @@ dependencies = [
"frunk_core",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -1961,7 +1967,7 @@ dependencies = [
"frunk_core",
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -2026,7 +2032,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -2481,9 +2487,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.58"
version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -2945,12 +2951,12 @@ checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
[[package]]
name = "libloading"
version = "0.7.4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
dependencies = [
"cfg-if",
"winapi",
"windows-sys 0.48.0",
]
[[package]]
@@ -3314,7 +3320,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
"termcolor",
"thiserror",
]
@@ -3591,7 +3597,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -3902,7 +3908,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -3940,7 +3946,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -4055,12 +4061,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
dependencies = [
"proc-macro2",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -4141,9 +4147,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.71"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708"
dependencies = [
"unicode-ident",
]
@@ -4233,9 +4239,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -4363,6 +4369,18 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rcgen"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d918c80c5a4c7560db726763020bd16db179e4d5b828078842274a443addb5d"
dependencies = [
"pem",
"ring 0.17.7",
"time",
"yasna",
]
[[package]]
name = "redis"
version = "0.24.0"
@@ -4781,7 +4799,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.20",
"semver 1.0.21",
]
[[package]]
@@ -4839,7 +4857,7 @@ dependencies = [
"log",
"ring 0.17.7",
"rustls-pki-types",
"rustls-webpki 0.102.0",
"rustls-webpki 0.102.1",
"subtle",
"zeroize",
]
@@ -4903,9 +4921,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.102.0"
version = "0.102.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89"
checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b"
dependencies = [
"ring 0.17.7",
"rustls-pki-types",
@@ -5039,9 +5057,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.20"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
[[package]]
name = "semver-parser"
@@ -5105,38 +5123,38 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.13"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb1879ea93538b78549031e2d54da3e901fd7e75f2e4dc758d760937b123d10"
checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
name = "serde_json"
version = "1.0.108"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257"
dependencies = [
"itoa",
"ryu",
@@ -5199,7 +5217,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -5408,9 +5426,9 @@ dependencies = [
[[package]]
name = "smtp-proto"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f7278d054ed6844babe3d760d03ccae24e26207c3c27b68d8385a03d3324619"
checksum = "20b37ae016fedcac2174dd0e3029870154830a4d3fb10f533317f2604e72b343"
[[package]]
name = "snafu"
@@ -5514,6 +5532,7 @@ dependencies = [
"openssl",
"pwhash",
"rand",
"rcgen",
"reqwest",
"rpassword",
"tar",
@@ -5630,9 +5649,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.43"
version = "2.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53"
checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb"
dependencies = [
"proc-macro2",
"quote",
@@ -5648,7 +5667,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -5791,22 +5810,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.52"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.52"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -5919,7 +5938,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -6129,7 +6148,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -6388,20 +6407,29 @@ name = "utils"
version = "0.5.1"
dependencies = [
"ahash 0.8.7",
"arc-swap",
"base64 0.21.5",
"chrono",
"dashmap",
"futures",
"mail-auth",
"mail-send",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"parking_lot",
"pem",
"privdrop",
"rand",
"rcgen",
"reqwest",
"ring 0.17.7",
"rustls 0.22.1",
"rustls-pemfile 2.0.0",
"rustls-pki-types",
"serde",
"serde_json",
"smtp-proto",
"tokio",
"tokio-rustls 0.25.0",
@@ -6411,6 +6439,7 @@ dependencies = [
"tracing-opentelemetry",
"tracing-subscriber",
"webpki-roots 0.26.0",
"x509-parser",
]
[[package]]
@@ -6482,7 +6511,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
"wasm-bindgen-shared",
]
@@ -6516,7 +6545,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6674,11 +6703,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.51.1"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.48.5",
"windows-targets 0.52.0",
]
[[package]]
@@ -6881,9 +6910,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.31"
version = "0.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c"
checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6"
dependencies = [
"memchr",
]
@@ -6958,6 +6987,15 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]]
name = "zerocopy"
version = "0.7.32"
@@ -6975,7 +7013,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
@@ -6995,7 +7033,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.47",
]
[[package]]
+1 -1
View File
@@ -69,7 +69,7 @@ async fn main() -> std::io::Result<()> {
Commands::Export(command) => {
command.exec(client).await;
}
Commands::Database(command) => command.exec(client).await,
Commands::Server(command) => command.exec(client).await,
Commands::Account(command) => command.exec(client).await,
Commands::Domain(command) => command.exec(client).await,
Commands::List(command) => command.exec(client).await,
+5 -3
View File
@@ -71,7 +71,7 @@ pub enum Commands {
/// Manage JMAP database
#[clap(subcommand)]
Database(DatabaseCommands),
Server(ServerCommands),
/// Manage SMTP message queue
#[clap(subcommand)]
@@ -399,9 +399,11 @@ pub enum ExportCommands {
}
#[derive(Subcommand)]
pub enum DatabaseCommands {
pub enum ServerCommands {
/// Perform database maintenance
Maintenance {},
DatabaseMaintenance {},
/// Reload TLS certificates
ReloadCertificates {},
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+9 -3
View File
@@ -24,17 +24,23 @@
use reqwest::Method;
use serde_json::Value;
use super::cli::{Client, DatabaseCommands};
use super::cli::{Client, ServerCommands};
impl DatabaseCommands {
impl ServerCommands {
pub async fn exec(self, client: Client) {
match self {
DatabaseCommands::Maintenance {} => {
ServerCommands::DatabaseMaintenance {} => {
client
.http_request::<Value, String>(Method::GET, "/admin/store/maintenance", None)
.await;
eprintln!("Success.");
}
ServerCommands::ReloadCertificates {} => {
client
.http_request::<Value, String>(Method::GET, "/admin/certificates/reload", None)
.await;
eprintln!("Success.");
}
}
}
}
+1 -1
View File
@@ -142,7 +142,7 @@ impl Session<TcpStream> {
}
pub async fn handle_conn(mut self) {
if self.handle_conn_().await && self.instance.tls_acceptor.is_some() {
if self.handle_conn_().await && self.instance.acceptor.is_tls() {
if let Ok(session) = self.into_tls().await {
session.handle_conn().await;
}
+1
View File
@@ -21,6 +21,7 @@ pwhash = "1.0.0"
rand = "0.8.5"
clap = { version = "4.1.6", features = ["derive"] }
zip-extract = "0.1.2"
rcgen = "0.12"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
libc = "0.2.147"
+54 -43
View File
@@ -31,9 +31,10 @@ use std::{
use base64::{engine::general_purpose, Engine};
use clap::{Parser, ValueEnum};
use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select};
use dialoguer::{console::Term, theme::ColorfulTheme, Confirm, Input, Select};
use openssl::rsa::Rsa;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rcgen::generate_simple_self_signed;
const CONFIG_URL: &str = "https://get.stalw.art/resources/config.zip";
@@ -468,35 +469,34 @@ fn main() -> std::io::Result<()> {
.trim()
.to_lowercase();
// Obtain TLS certificate path
let (cert_path, pk_path) = if !args.docker {
#[cfg(not(target_env = "msvc"))]
let cert_base_path = format!("/etc/letsencrypt/live/{}/", hostname);
#[cfg(target_env = "msvc")]
let cert_base_path = format!("C:\\Program Files\\Letsencrypt\\live\\{}\\", hostname);
// Obtain TLS configuration
let is_acme = Confirm::new()
.with_prompt(&format!("Do you want the TLS certificates for {hostname} to be obtained automatically from Let's Encrypt using ACME?"))
.interact()
.unwrap();
let (cert_path, pk_path) = {
let base_path = base_path.join("etc").join("certs").join(&hostname);
// Create directories
fs::create_dir_all(&base_path)?;
let cert_path = base_path.join("fullchain.pem");
let pk_path = base_path.join("privkey.pem");
// Build self-signed cert
let cert = generate_simple_self_signed(vec![hostname.to_string()]).unwrap_or_else(|err| {
panic!("Failed to generate self-signed certificate for {hostname}: {err}",)
});
std::fs::write(
&cert_path,
cert.serialize_pem()
.unwrap_or_else(|err| panic!("Failed to write certificate for {hostname}: {err}",)),
)?;
std::fs::write(&pk_path, cert.serialize_private_key_pem())?;
(
input(
&format!("Where is the TLS certificate for '{hostname}' located?"),
&format!("{cert_base_path}fullchain.pem"),
file_exists,
)?,
input(
&format!("Where is the TLS private key for '{hostname}' located?"),
&format!("{cert_base_path}privkey.pem"),
file_exists,
)?,
)
} else {
// Create directories
fs::create_dir_all(base_path.join("etc").join("certs").join(&hostname))?;
(
format!(
"{}/etc/certs/{}/fullchain.pem",
base_path.display(),
hostname
),
format!("{}/etc/certs/{}/privkey.pem", base_path.display(), hostname),
cert_path.to_str().unwrap().to_string(),
pk_path.to_str().unwrap().to_string(),
)
};
@@ -526,13 +526,24 @@ fn main() -> std::io::Result<()> {
("__HOST__", &hostname),
],
);
sed(
cfg_path.join("common").join("tls.toml"),
&[("__CERT_PATH__", &cert_path), ("__PK_PATH__", &pk_path)],
);
if is_acme {
sed(
cfg_path.join("common").join("tls.toml"),
&[
("certificate = \"default\"", "#certificate = \"default\""),
("#acme =", "acme ="),
("__CERT_PATH__", &cert_path),
("__PK_PATH__", &pk_path),
],
);
} else {
sed(
cfg_path.join("common").join("tls.toml"),
&[("__CERT_PATH__", &cert_path), ("__PK_PATH__", &pk_path)],
);
}
// Write service file
if !args.docker {
// Change permissions
#[cfg(not(target_env = "msvc"))]
@@ -633,13 +644,22 @@ fn main() -> std::io::Result<()> {
}
}
if is_acme {
eprintln!(
"\n🛡️ Ensure that port 443 (HTTPS) on {hostname} is open and accessible from the internet to successfully obtain your ACME TLS certificate.",
);
} else {
eprintln!(
"\n🛡️ Self-signed certificates have been generated under {}, you'll need to replace them with your own certificates to enable TLS.", base_path.join("etc").join("certs").join(&hostname).display()
);
}
if is_internal {
eprintln!(
"\n🔑 The administrator account is 'admin' and the password can be found in the log files at {}/logs.", base_path.display()
);
}
eprintln!("\n🎉 Installation completed!\n\n✅ {dkim_instructions}\n");
eprintln!("\n✅ {dkim_instructions}\n🎉 Installation completed!\n");
Ok(())
}
@@ -749,15 +769,6 @@ fn dir_create_if_missing(path: &String) -> Result<(), String> {
}
}
fn file_exists(path: &String) -> Result<(), String> {
let path = Path::new(path);
if path.is_file() {
Ok(())
} else {
Err(format!("File {} does not exist", path.display()))
}
}
#[allow(clippy::ptr_arg)]
fn not_empty(value: &String) -> Result<(), String> {
if value.trim().is_empty() {
+12 -1
View File
@@ -30,7 +30,7 @@ use hyper::{body::Bytes, Method, StatusCode};
use jmap_proto::error::request::RequestError;
use serde_json::json;
use crate::JMAP;
use crate::{services::housekeeper, JMAP};
use super::{http::ToHttpResponse, HttpRequest, JsonResponse};
@@ -299,6 +299,17 @@ impl JMAP {
.into_http_response(),
}
}
("certificates", Some("reload"), &Method::GET) => {
let _ = self
.housekeeper_tx
.send(housekeeper::Event::ReloadCertificates)
.await;
JsonResponse::new(json!({
"data": [],
}))
.into_http_response()
}
(path_1 @ ("queue" | "report"), Some(path_2), &Method::GET) => {
self.smtp
.handle_manage_request(req.uri(), req.method(), path_1, path_2)
+35 -30
View File
@@ -42,7 +42,7 @@ use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use utils::listener::{ServerInstance, SessionData, SessionManager};
use utils::listener::{ServerInstance, SessionData, SessionManager, TcpAcceptorResult};
use crate::{
auth::{oauth::OAuthMetadata, AccessToken},
@@ -291,40 +291,45 @@ pub async fn parse_jmap_request(
}
impl SessionManager for JmapSessionManager {
fn spawn(&self, session: SessionData<TcpStream>) {
fn spawn(&self, mut session: SessionData<TcpStream>) {
let jmap = self.inner.clone();
tokio::spawn(async move {
if let Some(tls_acceptor) = &session.instance.tls_acceptor {
let span = session.span;
match tls_acceptor.accept(session.stream).await {
Ok(stream) => {
handle_request(
jmap,
SessionData {
stream,
local_ip: session.local_ip,
remote_ip: session.remote_ip,
remote_port: session.remote_port,
span,
in_flight: session.in_flight,
instance: session.instance,
},
)
.await;
}
Err(err) => {
tracing::debug!(
parent: &span,
context = "tls",
event = "error",
"Failed to accept TLS connection: {}",
err
);
match session.instance.acceptor.accept(session.stream).await {
TcpAcceptorResult::Tls(accept) => {
let span = session.span;
match accept.await {
Ok(stream) => {
handle_request(
jmap,
SessionData {
stream,
local_ip: session.local_ip,
remote_ip: session.remote_ip,
remote_port: session.remote_port,
span,
in_flight: session.in_flight,
instance: session.instance,
},
)
.await;
}
Err(err) => {
tracing::debug!(
parent: &span,
context = "tls",
event = "error",
"Failed to accept TLS connection: {}",
err
);
}
}
}
} else {
handle_request(jmap, session).await;
TcpAcceptorResult::Plain(stream) => {
session.stream = stream;
handle_request(jmap, session).await;
}
TcpAcceptorResult::Close => (),
}
});
}
+3 -1
View File
@@ -60,6 +60,7 @@ use tokio::sync::mpsc;
use utils::{
config::Rate,
ipc::DeliveryEvent,
listener::tls::Certificate,
map::ttl_dashmap::{TtlDashMap, TtlMap},
snowflake::SnowflakeIdGenerator,
UnwrapFailure,
@@ -186,6 +187,7 @@ impl JMAP {
config: &utils::config::Config,
stores: &Stores,
directories: &Directories,
certificates: Vec<Arc<Certificate>>,
delivery_rx: mpsc::Receiver<DeliveryEvent>,
smtp: Arc<SMTP>,
) -> Result<Arc<Self>, String> {
@@ -420,7 +422,7 @@ impl JMAP {
spawn_state_manager(jmap_server.clone(), config, state_rx);
// Spawn housekeeper
spawn_housekeeper(jmap_server.clone(), config, housekeeper_rx);
spawn_housekeeper(jmap_server.clone(), config, certificates, housekeeper_rx);
Ok(jmap_server)
}
+37 -2
View File
@@ -26,6 +26,7 @@ use std::sync::Arc;
use tokio::sync::mpsc;
use utils::{
config::{cron::SimpleCron, Config},
listener::tls::Certificate,
map::ttl_dashmap::TtlMap,
UnwrapFailure,
};
@@ -36,6 +37,7 @@ use super::IPC_CHANNEL_BUFFER;
pub enum Event {
PurgeSessions,
ReloadCertificates,
IndexStart,
IndexDone,
#[cfg(feature = "test_mode")]
@@ -43,7 +45,12 @@ pub enum Event {
Exit,
}
pub fn spawn_housekeeper(core: Arc<JMAP>, settings: &Config, mut rx: mpsc::Receiver<Event>) {
pub fn spawn_housekeeper(
core: Arc<JMAP>,
settings: &Config,
certificates: Vec<Arc<Certificate>>,
mut rx: mpsc::Receiver<Event>,
) {
let purge_cache = settings
.property_or_static::<SimpleCron>("jmap.session.purge.frequency", "15 * *")
.failed("Initialize housekeeper");
@@ -69,6 +76,32 @@ pub fn spawn_housekeeper(core: Arc<JMAP>, settings: &Config, mut rx: mpsc::Recei
Event::PurgeSessions => {
do_purge = true;
}
Event::ReloadCertificates => {
let certificates = certificates.clone();
tokio::spawn(async move {
for cert in certificates {
match cert.reload().await {
Ok(_) => {
tracing::info!(
context = "tls",
event = "reload",
path = cert.path[0].to_string_lossy().as_ref(),
"Reloaded certificate."
);
}
Err(err) => {
tracing::error!(
context = "tls",
event = "error",
path = cert.path[0].to_string_lossy().as_ref(),
error = ?err,
"Failed to reload certificate."
);
}
}
}
});
}
Event::IndexStart => {
if !index_busy {
index_busy = true;
@@ -104,7 +137,9 @@ pub fn spawn_housekeeper(core: Arc<JMAP>, settings: &Config, mut rx: mpsc::Recei
tracing::debug!("Housekeeper task exiting.");
return;
}
Err(_) => (),
Err(_) => {
do_purge = true;
}
}
if do_purge {
+11 -4
View File
@@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> {
.failed("Failed to enable tracing");
// Bind ports and drop privileges
let servers = config.parse_servers().failed("Invalid configuration");
let mut servers = config.parse_servers().failed("Invalid configuration");
servers.bind(&config);
// Parse stores and directories
@@ -80,9 +80,16 @@ async fn main() -> std::io::Result<()> {
let smtp = SMTP::init(&config, &servers, &stores, &directory, delivery_tx)
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(&config, &stores, &directory, delivery_rx, smtp.clone())
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(
&config,
&stores,
&directory,
std::mem::take(&mut servers.certificates),
delivery_rx,
smtp.clone(),
)
.await
.failed("Invalid configuration file");
let imap = IMAP::init(&config)
.await
.failed("Invalid configuration file");
+1 -1
View File
@@ -164,7 +164,7 @@ impl Session<TcpStream> {
}
pub async fn handle_conn(mut self) {
if self.handle_conn_().await && self.instance.tls_acceptor.is_some() {
if self.handle_conn_().await && self.instance.acceptor.is_tls() {
if let Ok(session) = self.into_tls().await {
session.handle_conn().await;
}
+7 -5
View File
@@ -41,7 +41,7 @@ use tokio::{
sync::oneshot,
};
use utils::listener::{limiter::InFlight, SessionManager};
use utils::listener::{limiter::InFlight, SessionManager, TcpAcceptorResult};
use crate::{
queue::{self, instant_to_timestamp, InstantFromTimestamp, QueueId, Status},
@@ -160,8 +160,8 @@ impl SessionManager for SmtpAdminSessionManager {
fn spawn(&self, session: utils::listener::SessionData<tokio::net::TcpStream>) {
let core = self.inner.clone();
tokio::spawn(async move {
if let Some(tls_acceptor) = &session.instance.tls_acceptor {
match tls_acceptor.accept(session.stream).await {
match session.instance.acceptor.accept(session.stream).await {
TcpAcceptorResult::Tls(accept) => match accept.await {
Ok(stream) => {
handle_request(stream, core, session.remote_ip, session.in_flight).await;
}
@@ -174,9 +174,11 @@ impl SessionManager for SmtpAdminSessionManager {
err
);
}
},
TcpAcceptorResult::Plain(stream) => {
handle_request(stream, core, session.remote_ip, session.in_flight).await;
}
} else {
handle_request(session.stream, core, session.remote_ip, session.in_flight).await;
TcpAcceptorResult::Close => (),
}
});
}
+2 -2
View File
@@ -49,7 +49,7 @@ use tokio_rustls::TlsConnector;
use tracing::Span;
use utils::{
ipc::DeliveryEvent,
listener::{limiter::InFlight, ServerInstance},
listener::{limiter::InFlight, ServerInstance, TcpAcceptor},
};
use crate::{
@@ -481,7 +481,7 @@ static ref SIEVE: Arc<ServerInstance> = Arc::new(utils::listener::ServerInstance
protocol: utils::config::ServerProtocol::Lmtp,
hostname: "localhost".to_string(),
data: "localhost".to_string(),
tls_acceptor: None,
acceptor: TcpAcceptor::Plain,
is_tls_implicit: true,
limiter: utils::listener::limiter::ConcurrencyLimiter::new(0),
shutdown_rx: tokio::sync::watch::channel(false).1,
+1 -1
View File
@@ -96,7 +96,7 @@ impl Session<TcpStream> {
}
pub async fn handle_conn(mut self) {
if self.handle_conn_().await && self.instance.tls_acceptor.is_some() {
if self.handle_conn_().await && self.instance.acceptor.is_tls() {
if let Ok(session) = self.into_tls().await {
session.handle_conn().await;
}
+1 -1
View File
@@ -40,7 +40,7 @@ pub struct FsStore {
impl FsStore {
pub async fn open(config: &Config, prefix: impl AsKey) -> crate::Result<Self> {
let prefix = prefix.as_key();
let path = config.property_require::<PathBuf>((&prefix, "path"))?;
let path = PathBuf::from(config.value_require((&prefix, "path"))?);
if !path.exists() {
fs::create_dir_all(&path).await.map_err(|e| {
crate::Error::InternalError(format!(
+10
View File
@@ -27,6 +27,16 @@ ahash = { version = "0.8" }
chrono = "0.4"
rand = "0.8.5"
webpki-roots = { version = "0.26"}
ring = { version = "0.17" }
base64 = "0.21"
serde_json = "1.0"
rcgen = "0.12"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]}
x509-parser = "0.15.0"
pem = "3.0"
parking_lot = "0.12"
arc-swap = "1.6.0"
futures = "0.3"
[target.'cfg(unix)'.dependencies]
privdrop = "0.5.3"
+94
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
))
}
}
+364
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)
}
}
+140
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)
}
}
+206
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()
}
}
+300
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))
}
}
+81
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])
}
}
-99
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\".",
)),
}
}
}
+202 -143
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,
})
}
+12 -4
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)]
+190
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",
)
}
+1
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;
+34 -16
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(())
}
+47 -4
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"),
}
}
}
+200
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()
}
}
Binary file not shown.
+9
View File
@@ -7,6 +7,7 @@ enable = true
implicit = false
timeout = "1m"
certificate = "default"
#acme = "letsencrypt"
#sni = [{subject = "", certificate = ""}]
#protocols = ["TLSv1.2", "TLSv1.3"]
#ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256",
@@ -16,6 +17,14 @@ certificate = "default"
# "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"]
ignore-client-order = true
[acme."letsencrypt"]
directory = "https://acme-v02.api.letsencrypt.org/directory"
#directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
contact = ["postmaster@%{DEFAULT_DOMAIN}%"]
cache = "%{BASE_PATH}%/etc/acme"
port = 443
renew-before = "30d"
[certificate."default"]
cert = "file://__CERT_PATH__"
private-key = "file://__PK_PATH__"
+5 -2
View File
@@ -3,6 +3,9 @@
#############################################
[server.listener."jmap"]
bind = ["[::]:8080"]
url = "https://%{HOST}%:8080"
protocol = "jmap"
bind = ["[::]:443"]
url = "https://%{HOST}%"
#bind = ["[::]:8080"]
#url = "https://%{HOST}%:8080"
+28
View File
@@ -0,0 +1,28 @@
FROM golang:1.18-alpine as builder
ENV CGO_ENABLED=0
WORKDIR /pebble-src
RUN apk update && apk add --no-cache git
RUN git clone https://github.com/letsencrypt/pebble/ /pebble-src
RUN go build -o /go/bin/pebble ./cmd/pebble
## main
FROM alpine:3.15.4
COPY --from=builder /go/bin/pebble /usr/bin/pebble
COPY --from=builder /pebble-src/test/ /test/
CMD [ "/usr/bin/pebble" ]
EXPOSE 14000
EXPOSE 15000
# Build:
# docker build -f Docker.pebble -t pebble
# Run:
# docker run -d -p 14000:14000 -p 15000:15000 pebble
# docker run -d --name pebble -p 14000:14000 -p 15000:15000 pebble pebble -config /test/config/pebble-config.json -strict
@@ -0,0 +1,28 @@
# curl --request POST --data '{"ip":"192.168.5.2"}' http://localhost:8055/set-default-ipv4
version: '3'
services:
pebble:
image: letsencrypt/pebble:latest
command: pebble -config /test/config/pebble-config.json -strict -dnsserver 10.30.50.3:8053
ports:
- 14000:14000 # HTTPS ACME API
- 15000:15000 # HTTPS Management API
networks:
acmenet:
ipv4_address: 10.30.50.2
challtestsrv:
image: letsencrypt/pebble-challtestsrv:latest
command: pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 10.30.50.3
ports:
- 8055:8055 # HTTP Management API
networks:
acmenet:
ipv4_address: 10.30.50.3
networks:
acmenet:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.30.50.0/24
+11 -4
View File
@@ -276,7 +276,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest {
.replace("{TMP}", &temp_dir.path.display().to_string()),
)
.unwrap();
let servers = config.parse_servers().unwrap();
let mut servers = config.parse_servers().unwrap();
let stores = config.parse_stores().await.failed("Invalid configuration");
let directory = config
.parse_directory(&stores, store_id.into())
@@ -289,9 +289,16 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest {
let smtp = SMTP::init(&config, &servers, &stores, &directory, delivery_tx)
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(&config, &stores, &directory, delivery_rx, smtp.clone())
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(
&config,
&stores,
&directory,
std::mem::take(&mut servers.certificates),
delivery_rx,
smtp.clone(),
)
.await
.failed("Invalid configuration file");
let imap: Arc<IMAP> = IMAP::init(&config)
.await
.failed("Invalid configuration file");
+11 -4
View File
@@ -390,7 +390,7 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest {
.replace("{TMP}", &temp_dir.path.display().to_string()),
)
.unwrap();
let servers = config.parse_servers().unwrap();
let mut servers = config.parse_servers().unwrap();
let stores = config.parse_stores().await.failed("Invalid configuration");
let directory = config
.parse_directory(&stores, store_id.into())
@@ -403,9 +403,16 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest {
let smtp = SMTP::init(&config, &servers, &stores, &directory, delivery_tx)
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(&config, &stores, &directory, delivery_rx, smtp.clone())
.await
.failed("Invalid configuration file");
let jmap = JMAP::init(
&config,
&stores,
&directory,
std::mem::take(&mut servers.certificates),
delivery_rx,
smtp.clone(),
)
.await
.failed("Invalid configuration file");
let (shutdown_tx, _) = servers.spawn(|server, shutdown_rx| {
match &server.protocol {
ServerProtocol::Smtp | ServerProtocol::Lmtp => {
+3 -3
View File
@@ -296,11 +296,11 @@ impl utils::listener::SessionManager for SessionManager {
TokioIo::new(
session
.instance
.tls_acceptor
.as_ref()
.unwrap()
.acceptor
.accept(session.stream)
.await
.unwrap_tls()
.await
.unwrap(),
),
service_fn(|mut req: hyper::Request<body::Incoming>| {
+7 -4
View File
@@ -37,7 +37,10 @@ use store::{
};
use tokio::net::TcpSocket;
use utils::config::{Config, DynValue, KeyLookup, Listener, Rate, Server, ServerProtocol};
use utils::{
config::{Config, DynValue, KeyLookup, Listener, Rate, Server, ServerProtocol},
listener::TcpAcceptor,
};
use ahash::AHashMap;
@@ -449,7 +452,7 @@ fn parse_servers() {
linger: None,
nodelay: true,
}],
tls: None,
acceptor: TcpAcceptor::Plain,
tls_implicit: false,
max_connections: 8192,
},
@@ -477,7 +480,7 @@ fn parse_servers() {
nodelay: true,
},
],
tls: None,
acceptor: TcpAcceptor::Plain,
tls_implicit: true,
max_connections: 1024,
},
@@ -495,7 +498,7 @@ fn parse_servers() {
linger: None,
nodelay: true,
}],
tls: None,
acceptor: TcpAcceptor::Plain,
tls_implicit: true,
max_connections: 8192,
},
+2 -2
View File
@@ -34,7 +34,7 @@ use smtp::{
};
use utils::{
config::ServerProtocol,
listener::{limiter::ConcurrencyLimiter, ServerInstance},
listener::{limiter::ConcurrencyLimiter, ServerInstance, TcpAcceptor},
};
use super::TestConfig;
@@ -368,7 +368,7 @@ impl TestServerInstance for ServerInstance {
hostname: "mx.example.org".to_string(),
protocol: ServerProtocol::Smtp,
data: "220 mx.example.org at your service.\r\n".to_string(),
tls_acceptor: None,
acceptor: TcpAcceptor::Plain,
is_tls_implicit: false,
limiter: ConcurrencyLimiter::new(100),
shutdown_rx,