mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-10-31 00:12:18 +00:00
ACME TLS implementation using TLS-ALPN-01 - closes #160
This commit is contained in:
@@ -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
@@ -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]]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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 => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
|
||||
use rustls::{
|
||||
server::{ClientHello, ResolvesServerCert},
|
||||
sign::CertifiedKey,
|
||||
};
|
||||
|
||||
use super::{directory::ACME_TLS_ALPN_NAME, AcmeManager};
|
||||
|
||||
impl AcmeManager {
|
||||
pub(crate) fn set_cert(&self, cert: Arc<CertifiedKey>) {
|
||||
self.cert.store(cert);
|
||||
self.order_in_progress.store(false, Ordering::Relaxed);
|
||||
self.auth_keys.lock().clear();
|
||||
}
|
||||
pub(crate) fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
|
||||
self.auth_keys.lock().insert(domain, cert);
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvesServerCert for AcmeManager {
|
||||
fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
|
||||
if self.has_order_in_progress() && client_hello.is_tls_alpn_challenge() {
|
||||
match client_hello.server_name() {
|
||||
None => {
|
||||
tracing::debug!(
|
||||
context = "acme",
|
||||
event = "error",
|
||||
reason = "missing-sni",
|
||||
"client did not supply SNI"
|
||||
);
|
||||
None
|
||||
}
|
||||
Some(domain) => {
|
||||
tracing::trace!(
|
||||
context = "acme",
|
||||
event = "auth-key",
|
||||
domain = %domain,
|
||||
"Found client supplied SNI");
|
||||
|
||||
self.auth_keys.lock().get(domain).cloned()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.cert.load().clone().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IsTlsAlpnChallenge {
|
||||
fn is_tls_alpn_challenge(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsTlsAlpnChallenge for ClientHello<'_> {
|
||||
fn is_tls_alpn_challenge(&self) -> bool {
|
||||
self.alpn().into_iter().flatten().eq([ACME_TLS_ALPN_NAME])
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
use rustls::{
|
||||
server::{ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni},
|
||||
sign::CertifiedKey,
|
||||
version::{TLS12, TLS13},
|
||||
SupportedProtocolVersion,
|
||||
};
|
||||
use rustls_pemfile::{certs, read_one, Item};
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
|
||||
|
||||
use super::Config;
|
||||
|
||||
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
|
||||
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CertificateResolver {
|
||||
pub resolver: Option<ResolvesServerCertUsingSni>,
|
||||
pub default_cert: Option<Arc<CertifiedKey>>,
|
||||
}
|
||||
|
||||
impl ResolvesServerCert for CertificateResolver {
|
||||
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
||||
self.resolver
|
||||
.as_ref()
|
||||
.and_then(|r| r.resolve(hello))
|
||||
.or_else(|| self.default_cert.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn rustls_certificate(&self, cert_id: &str) -> super::Result<Vec<CertificateDer<'static>>> {
|
||||
let certs = certs(&mut Cursor::new(self.file_contents((
|
||||
"certificate",
|
||||
cert_id,
|
||||
"cert",
|
||||
))?))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| {
|
||||
format!("Failed to read certificates in \"certificate.{cert_id}.cert\": {err}")
|
||||
})?;
|
||||
|
||||
if !certs.is_empty() {
|
||||
Ok(certs)
|
||||
} else {
|
||||
Err(format!(
|
||||
"No certificates found in \"certificate.{cert_id}.cert\"."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rustls_private_key(&self, cert_id: &str) -> super::Result<PrivateKeyDer<'static>> {
|
||||
match read_one(&mut Cursor::new(self.file_contents((
|
||||
"certificate",
|
||||
cert_id,
|
||||
"private-key",
|
||||
))?))
|
||||
.map_err(|err| {
|
||||
format!("Failed to read private keys in \"certificate.{cert_id}.private-key\": {err}",)
|
||||
})?
|
||||
.into_iter()
|
||||
.next()
|
||||
{
|
||||
Some(Item::Pkcs8Key(key)) => Ok(PrivateKeyDer::Pkcs8(key)),
|
||||
Some(Item::Pkcs1Key(key)) => Ok(PrivateKeyDer::Pkcs1(key)),
|
||||
Some(Item::Sec1Key(key)) => Ok(PrivateKeyDer::Sec1(key)),
|
||||
Some(_) => Err(format!(
|
||||
"Unsupported private keys found in \"certificate.{cert_id}.private-key\".",
|
||||
)),
|
||||
None => Err(format!(
|
||||
"No private keys found in \"certificate.{cert_id}.private-key\".",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+202
-143
@@ -23,6 +23,7 @@
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use rustls::{
|
||||
crypto::ring::{
|
||||
cipher_suite::{
|
||||
@@ -32,173 +33,87 @@ use rustls::{
|
||||
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
default_provider,
|
||||
sign::any_supported_type,
|
||||
},
|
||||
server::ResolvesServerCertUsingSni,
|
||||
sign::CertifiedKey,
|
||||
server::ResolvesServerCert,
|
||||
ServerConfig, SupportedCipherSuite, ALL_VERSIONS,
|
||||
};
|
||||
use tokio::net::TcpSocket;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use crate::UnwrapFailure;
|
||||
use crate::{
|
||||
acme::{directory::ACME_TLS_ALPN_NAME, AcmeManager},
|
||||
listener::{
|
||||
tls::{Certificate, CertificateResolver},
|
||||
TcpAcceptor,
|
||||
},
|
||||
UnwrapFailure,
|
||||
};
|
||||
|
||||
use super::{
|
||||
certificate::{CertificateResolver, TLS12_VERSION, TLS13_VERSION},
|
||||
tls::{TLS12_VERSION, TLS13_VERSION},
|
||||
utils::{AsKey, ParseKey, ParseValue},
|
||||
Config, Listener, Server, ServerProtocol, Servers,
|
||||
};
|
||||
|
||||
impl Config {
|
||||
pub fn parse_servers(&self) -> super::Result<Servers> {
|
||||
let mut servers: Vec<Server> = Vec::new();
|
||||
// Parse certificates and ACME managers
|
||||
let certificates = self.parse_certificates()?;
|
||||
let acmes = self.parse_acmes()?;
|
||||
|
||||
// Parse servers
|
||||
let mut servers = Servers::default();
|
||||
for (internal_id, id) in self.sub_keys("server.listener").enumerate() {
|
||||
let mut server = self.parse_server(id)?;
|
||||
if !servers.iter().any(|s| s.id == server.id) {
|
||||
let mut server = self.parse_server(id, &certificates, &acmes)?;
|
||||
if !servers.inner.iter().any(|s| s.id == server.id) {
|
||||
server.internal_id = internal_id as u16;
|
||||
servers.push(server);
|
||||
servers.inner.push(server);
|
||||
} else {
|
||||
return Err(format!("Duplicate listener id {:?}.", server.id));
|
||||
}
|
||||
}
|
||||
|
||||
if !servers.is_empty() {
|
||||
Ok(Servers { inner: servers })
|
||||
// Add certificates with valid paths
|
||||
for (id, cert) in certificates {
|
||||
if cert.path.len() == 2 {
|
||||
servers.certificates.push(cert);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
context = "config",
|
||||
event = "acme",
|
||||
id = id,
|
||||
"Certificate reloading disabled for id {id:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ACME managers with configured domains
|
||||
for (id, acme) in acmes {
|
||||
if !acme.domains.is_empty() {
|
||||
servers.acme_managers.push(acme);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
context = "config",
|
||||
event = "acme",
|
||||
id = id,
|
||||
"ACME certificate manager disabled for id {id:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !servers.inner.is_empty() {
|
||||
Ok(servers)
|
||||
} else {
|
||||
Err("No server directives found in config file.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_server(&self, id: &str) -> super::Result<Server> {
|
||||
// Build TLS config
|
||||
let (tls, tls_implicit) = if self
|
||||
.property_or_default(("server.listener", id, "tls.enable"), "server.tls.enable")?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Parse protocol versions
|
||||
let mut tls_v2 = false;
|
||||
let mut tls_v3 = false;
|
||||
for (key, protocol) in self.values_or_default(
|
||||
("server.listener", id, "tls.protocols"),
|
||||
"server.tls.protocols",
|
||||
) {
|
||||
match protocol {
|
||||
"TLSv1.2" | "0x0303" => tls_v2 = true,
|
||||
"TLSv1.3" | "0x0304" => tls_v3 = true,
|
||||
protocol => {
|
||||
return Err(format!(
|
||||
"Unsupported TLS protocol {protocol:?} found in key {key:?}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cipher suites
|
||||
let mut ciphers: Vec<SupportedCipherSuite> = Vec::new();
|
||||
for (key, protocol) in
|
||||
self.values_or_default(("server.listener", id, "tls.ciphers"), "server.tls.ciphers")
|
||||
{
|
||||
ciphers.push(protocol.parse_key(key)?);
|
||||
}
|
||||
|
||||
// Obtain default certificate
|
||||
let cert_id = self
|
||||
.value_or_default(
|
||||
("server.listener", id, "tls.certificate"),
|
||||
"server.tls.certificate",
|
||||
)
|
||||
.ok_or_else(|| format!("Undefined certificate id for listener {id:?}."))?;
|
||||
let cert = self.rustls_certificate(cert_id)?;
|
||||
let pki = self.rustls_private_key(cert_id)?;
|
||||
|
||||
// Add SNI certificates
|
||||
let mut resolver = ResolvesServerCertUsingSni::new();
|
||||
let mut has_sni = false;
|
||||
for (key, value) in
|
||||
self.values_or_default(("server.listener", id, "tls.sni"), "server.tls.sni")
|
||||
{
|
||||
if let Some(prefix) = key.strip_suffix(".subject") {
|
||||
has_sni = true;
|
||||
resolver
|
||||
.add(
|
||||
value,
|
||||
match self.value((prefix, "certificate")) {
|
||||
Some(sni_cert_id) if sni_cert_id != cert_id => CertifiedKey {
|
||||
cert: self.rustls_certificate(sni_cert_id)?,
|
||||
key: any_supported_type(&self.rustls_private_key(sni_cert_id)?)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Failed to sign SNI certificate for {key:?}: {err}",
|
||||
)
|
||||
})?,
|
||||
ocsp: None,
|
||||
},
|
||||
_ => CertifiedKey {
|
||||
cert: cert.clone(),
|
||||
key:
|
||||
any_supported_type(&pki).map_err(|err| {
|
||||
format!(
|
||||
"Failed to sign SNI certificate for {key:?}: {err}",
|
||||
)
|
||||
})?,
|
||||
ocsp: None,
|
||||
},
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!("Failed to add SNI certificate for {key:?}: {err}")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
// Add default certificate
|
||||
let default_cert = Some(Arc::new(CertifiedKey {
|
||||
cert,
|
||||
key: any_supported_type(&pki)
|
||||
.map_err(|err| format!("Failed to sign certificate id {cert_id:?}: {err}"))?,
|
||||
ocsp: None,
|
||||
}));
|
||||
|
||||
// Build cert provider
|
||||
let mut provider = default_provider();
|
||||
if !ciphers.is_empty() {
|
||||
provider.cipher_suites = ciphers;
|
||||
}
|
||||
|
||||
// Build server config
|
||||
let mut config = ServerConfig::builder_with_provider(provider.into())
|
||||
.with_protocol_versions(if tls_v3 == tls_v2 {
|
||||
ALL_VERSIONS
|
||||
} else if tls_v3 {
|
||||
TLS13_VERSION
|
||||
} else {
|
||||
TLS12_VERSION
|
||||
})
|
||||
.map_err(|err| format!("Failed to build TLS config: {err}"))?
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(CertificateResolver {
|
||||
resolver: if has_sni { resolver.into() } else { None },
|
||||
default_cert,
|
||||
}));
|
||||
|
||||
//config.key_log = Arc::new(KeyLogger::default());
|
||||
config.ignore_client_order = self
|
||||
.property_or_default(
|
||||
("server.listener", id, "tls.ignore-client-order"),
|
||||
"server.tls.ignore-client-order",
|
||||
)?
|
||||
.unwrap_or(true);
|
||||
(
|
||||
config.into(),
|
||||
self.property_or_default(
|
||||
("server.listener", id, "tls.implicit"),
|
||||
"server.tls.implicit",
|
||||
)?
|
||||
.unwrap_or(true),
|
||||
)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
fn parse_server(
|
||||
&self,
|
||||
id: &str,
|
||||
certificates: &AHashMap<String, Arc<Certificate>>,
|
||||
acmes: &AHashMap<String, Arc<AcmeManager>>,
|
||||
) -> super::Result<Server> {
|
||||
// Build listeners
|
||||
let mut listeners = Vec::new();
|
||||
for result in self.properties::<SocketAddr>(("server.listener", id, "bind")) {
|
||||
@@ -267,6 +182,150 @@ impl Config {
|
||||
return Err(format!("No 'bind' directive found for listener id {id:?}"));
|
||||
}
|
||||
|
||||
// Build TLS config
|
||||
let (acceptor, tls_implicit) = if self
|
||||
.property_or_default(("server.listener", id, "tls.enable"), "server.tls.enable")?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Parse protocol versions
|
||||
let mut tls_v2 = false;
|
||||
let mut tls_v3 = false;
|
||||
for (key, protocol) in self.values_or_default(
|
||||
("server.listener", id, "tls.protocols"),
|
||||
"server.tls.protocols",
|
||||
) {
|
||||
match protocol {
|
||||
"TLSv1.2" | "0x0303" => tls_v2 = true,
|
||||
"TLSv1.3" | "0x0304" => tls_v3 = true,
|
||||
protocol => {
|
||||
return Err(format!(
|
||||
"Unsupported TLS protocol {protocol:?} found in key {key:?}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cipher suites
|
||||
let mut ciphers: Vec<SupportedCipherSuite> = Vec::new();
|
||||
for (key, protocol) in
|
||||
self.values_or_default(("server.listener", id, "tls.ciphers"), "server.tls.ciphers")
|
||||
{
|
||||
ciphers.push(protocol.parse_key(key)?);
|
||||
}
|
||||
|
||||
// Build resolver
|
||||
let mut acme_acceptor = None;
|
||||
let resolver: Arc<dyn ResolvesServerCert> = if let Some(acme_id) =
|
||||
self.value_or_default(("server.listener", id, "tls.acme"), "server.tls.acme")
|
||||
{
|
||||
let acme = acmes.get(acme_id).ok_or_else(|| {
|
||||
format!("Undefined ACME id {acme_id:?} for listener {id:?}.",)
|
||||
})?;
|
||||
|
||||
// Check if this port is used to receive ACME challenges
|
||||
let acme_port = self.property_or_static::<u16>(("acme", acme_id, "port"), "443")?;
|
||||
if listeners.iter().any(|l| l.addr.port() == acme_port) {
|
||||
acme_acceptor = Some(acme.clone());
|
||||
}
|
||||
|
||||
acme.clone()
|
||||
} else {
|
||||
let cert_id = self
|
||||
.value_or_default(
|
||||
("server.listener", id, "tls.certificate"),
|
||||
"server.tls.certificate",
|
||||
)
|
||||
.ok_or_else(|| format!("Undefined certificate id for listener {id:?}."))?;
|
||||
let mut resolver = CertificateResolver {
|
||||
sni: Default::default(),
|
||||
cert: certificates
|
||||
.get(cert_id)
|
||||
.ok_or_else(|| {
|
||||
format!("Undefined certificate id {cert_id:?} for listener {id:?}.",)
|
||||
})?
|
||||
.clone(),
|
||||
};
|
||||
|
||||
// Add SNI certificates
|
||||
for (key, value) in
|
||||
self.values_or_default(("server.listener", id, "tls.sni"), "server.tls.sni")
|
||||
{
|
||||
if let Some(prefix) = key.strip_suffix(".subject") {
|
||||
resolver
|
||||
.add(
|
||||
value,
|
||||
match self.value((prefix, "certificate")) {
|
||||
Some(sni_cert_id) if sni_cert_id != cert_id => {
|
||||
certificates.get(sni_cert_id).ok_or_else(|| {
|
||||
format!(
|
||||
"Undefined certificate id {sni_cert_id:?} for SNI {value:?} in listener {id:?}.",
|
||||
)
|
||||
})?.clone()
|
||||
}
|
||||
_ => resolver.cert.clone(),
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!("Failed to add SNI certificate for {key:?}: {err}")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(resolver)
|
||||
};
|
||||
|
||||
// Build cert provider
|
||||
let mut provider = default_provider();
|
||||
if !ciphers.is_empty() {
|
||||
provider.cipher_suites = ciphers;
|
||||
}
|
||||
|
||||
// Build server config
|
||||
let mut config = ServerConfig::builder_with_provider(provider.into())
|
||||
.with_protocol_versions(if tls_v3 == tls_v2 {
|
||||
ALL_VERSIONS
|
||||
} else if tls_v3 {
|
||||
TLS13_VERSION
|
||||
} else {
|
||||
TLS12_VERSION
|
||||
})
|
||||
.map_err(|err| format!("Failed to build TLS config: {err}"))?
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(resolver.clone());
|
||||
config.ignore_client_order = self
|
||||
.property_or_default(
|
||||
("server.listener", id, "tls.ignore-client-order"),
|
||||
"server.tls.ignore-client-order",
|
||||
)?
|
||||
.unwrap_or(true);
|
||||
|
||||
// Build acceptor
|
||||
let acceptor = if let Some(manager) = acme_acceptor {
|
||||
let mut challenge = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(resolver);
|
||||
challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
|
||||
TcpAcceptor::Acme {
|
||||
challenge: Arc::new(challenge),
|
||||
default: Arc::new(config),
|
||||
manager,
|
||||
}
|
||||
} else {
|
||||
TcpAcceptor::Tls(TlsAcceptor::from(Arc::new(config)))
|
||||
};
|
||||
|
||||
(
|
||||
acceptor,
|
||||
self.property_or_default(
|
||||
("server.listener", id, "tls.implicit"),
|
||||
"server.tls.implicit",
|
||||
)?
|
||||
.unwrap_or(true),
|
||||
)
|
||||
} else {
|
||||
(TcpAcceptor::Plain, false)
|
||||
};
|
||||
|
||||
let protocol = self.property_require(("server.listener", id, "protocol"))?;
|
||||
|
||||
Ok(Server {
|
||||
@@ -303,7 +362,7 @@ impl Config {
|
||||
.unwrap_or(8192),
|
||||
protocol,
|
||||
listeners,
|
||||
tls,
|
||||
acceptor,
|
||||
tls_implicit,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
pub mod certificate;
|
||||
pub mod cron;
|
||||
pub mod dynvalue;
|
||||
pub mod listener;
|
||||
pub mod parser;
|
||||
pub mod tls;
|
||||
pub mod utils;
|
||||
|
||||
use std::{
|
||||
@@ -33,14 +33,19 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::Display,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use rustls::ServerConfig;
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
use crate::{failed, UnwrapFailure};
|
||||
use crate::{
|
||||
acme::AcmeManager,
|
||||
failed,
|
||||
listener::{tls::Certificate, TcpAcceptor},
|
||||
UnwrapFailure,
|
||||
};
|
||||
|
||||
use self::utils::ParseValue;
|
||||
|
||||
@@ -57,13 +62,16 @@ pub struct Server {
|
||||
pub data: String,
|
||||
pub protocol: ServerProtocol,
|
||||
pub listeners: Vec<Listener>,
|
||||
pub tls: Option<ServerConfig>,
|
||||
pub acceptor: TcpAcceptor,
|
||||
pub tls_implicit: bool,
|
||||
pub max_connections: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Servers {
|
||||
pub inner: Vec<Server>,
|
||||
pub certificates: Vec<Arc<Certificate>>,
|
||||
pub acme_managers: Vec<Arc<AcmeManager>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{io::Cursor, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use rcgen::generate_simple_self_signed;
|
||||
use rustls::{
|
||||
crypto::ring::sign::any_supported_type,
|
||||
sign::CertifiedKey,
|
||||
version::{TLS12, TLS13},
|
||||
SupportedProtocolVersion,
|
||||
};
|
||||
use rustls_pemfile::{certs, read_one, Item};
|
||||
use rustls_pki_types::PrivateKeyDer;
|
||||
|
||||
use crate::{
|
||||
acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeManager},
|
||||
listener::tls::Certificate,
|
||||
};
|
||||
|
||||
use super::Config;
|
||||
|
||||
pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13];
|
||||
pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12];
|
||||
|
||||
impl Config {
|
||||
pub fn parse_certificates(&self) -> super::Result<AHashMap<String, Arc<Certificate>>> {
|
||||
let mut certs = AHashMap::new();
|
||||
for cert_id in self.sub_keys("certificate") {
|
||||
let key_cert = ("certificate", cert_id, "cert");
|
||||
let key_pk = ("certificate", cert_id, "private-key");
|
||||
|
||||
let mut cert = Certificate {
|
||||
cert: ArcSwap::from(Arc::new(build_certified_key(
|
||||
self.file_contents(key_cert)?,
|
||||
self.file_contents(key_pk)?,
|
||||
&format!("certificate.{cert_id}"),
|
||||
)?)),
|
||||
path: Vec::with_capacity(2),
|
||||
};
|
||||
|
||||
for key in [key_cert, key_pk] {
|
||||
if let Some(path) = self.value(key).and_then(|v| v.strip_prefix("file://")) {
|
||||
cert.path.push(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
|
||||
certs.insert(cert_id.to_string(), Arc::new(cert));
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub fn parse_acmes(&self) -> super::Result<AHashMap<String, Arc<AcmeManager>>> {
|
||||
let mut acmes = AHashMap::new();
|
||||
for acme_id in self.sub_keys("acme") {
|
||||
let directory = self
|
||||
.value(("acme", acme_id, "directory"))
|
||||
.unwrap_or(LETS_ENCRYPT_PRODUCTION_DIRECTORY)
|
||||
.trim()
|
||||
.to_string();
|
||||
let contact = self
|
||||
.values(("acme", acme_id, "contact"))
|
||||
.filter_map(|(_, v)| {
|
||||
let v = v.trim().to_string();
|
||||
if !v.is_empty() {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let cache = PathBuf::from(self.value_require(("acme", acme_id, "cache"))?);
|
||||
if !cache.exists() {
|
||||
std::fs::create_dir_all(&cache).map_err(|err| {
|
||||
format!("Failed to create ACME cache directory {:?}: {}", cache, err)
|
||||
})?;
|
||||
}
|
||||
|
||||
let renew_before: Duration =
|
||||
self.property_or_static(("acme", acme_id, "renew-before"), "30d")?;
|
||||
|
||||
if directory.is_empty() {
|
||||
return Err(format!("Missing directory for acme.{acme_id}."));
|
||||
}
|
||||
|
||||
if contact.is_empty() {
|
||||
return Err(format!("Missing contact for acme.{acme_id}."));
|
||||
}
|
||||
|
||||
// Find which domains are covered by this ACME manager
|
||||
let mut domains = Vec::new();
|
||||
for id in self.sub_keys("server.listener") {
|
||||
match (
|
||||
self.value_or_default(("server.listener", id, "tls.acme"), "server.tls.acme"),
|
||||
self.value_or_default(("server.listener", id, "hostname"), "server.hostname"),
|
||||
) {
|
||||
(Some(listener_acme), Some(hostname)) if listener_acme == acme_id => {
|
||||
let hostname = hostname.trim().to_lowercase();
|
||||
|
||||
if !domains.contains(&hostname) {
|
||||
domains.push(hostname);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
acmes.insert(
|
||||
acme_id.to_string(),
|
||||
Arc::new(AcmeManager::new(
|
||||
directory,
|
||||
domains,
|
||||
contact,
|
||||
renew_before,
|
||||
cache,
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(acmes)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_certified_key(
|
||||
cert: Vec<u8>,
|
||||
pk: Vec<u8>,
|
||||
id: &str,
|
||||
) -> super::Result<CertifiedKey> {
|
||||
let cert = certs(&mut Cursor::new(cert))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| format!("Failed to read certificates in {id:?}: {err}"))?;
|
||||
if cert.is_empty() {
|
||||
return Err(format!("No certificates found in {id:?}."));
|
||||
}
|
||||
let pk = match read_one(&mut Cursor::new(pk))
|
||||
.map_err(|err| format!("Failed to read private keys in {id:?}.: {err}",))?
|
||||
.into_iter()
|
||||
.next()
|
||||
{
|
||||
Some(Item::Pkcs8Key(key)) => PrivateKeyDer::Pkcs8(key),
|
||||
Some(Item::Pkcs1Key(key)) => PrivateKeyDer::Pkcs1(key),
|
||||
Some(Item::Sec1Key(key)) => PrivateKeyDer::Sec1(key),
|
||||
Some(_) => return Err(format!("Unsupported private keys found in {id:?}.",)),
|
||||
None => return Err(format!("No private keys found in {id:?}.",)),
|
||||
};
|
||||
|
||||
Ok(CertifiedKey {
|
||||
cert,
|
||||
key: any_supported_type(&pk)
|
||||
.map_err(|err| format!("Failed to sign certificate for {id:?}: {err}",))?,
|
||||
ocsp: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_self_signed_cert(domains: &[String]) -> super::Result<CertifiedKey> {
|
||||
let cert = generate_simple_self_signed(domains).map_err(|err| {
|
||||
format!(
|
||||
"Failed to generate self-signed certificate for {domains:?}: {err}",
|
||||
domains = domains
|
||||
)
|
||||
})?;
|
||||
build_certified_key(
|
||||
cert.serialize_pem().unwrap().into_bytes(),
|
||||
cert.serialize_private_key_pem().into_bytes(),
|
||||
"self-signed",
|
||||
)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use config::Config;
|
||||
|
||||
pub mod acme;
|
||||
pub mod codec;
|
||||
pub mod config;
|
||||
pub mod ipc;
|
||||
|
||||
@@ -28,17 +28,18 @@ use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::watch,
|
||||
};
|
||||
use tokio_rustls::{server::TlsStream, TlsAcceptor};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::{
|
||||
acme::SpawnAcme,
|
||||
config::{Config, Listener, Server, ServerProtocol, Servers},
|
||||
failed,
|
||||
listener::SessionData,
|
||||
UnwrapFailure,
|
||||
};
|
||||
|
||||
use super::{limiter::ConcurrencyLimiter, ServerInstance, SessionManager};
|
||||
use super::{limiter::ConcurrencyLimiter, ServerInstance, SessionManager, TcpAcceptorResult};
|
||||
|
||||
impl Server {
|
||||
pub fn spawn(self, manager: impl SessionManager, shutdown_rx: watch::Receiver<bool>) {
|
||||
@@ -53,7 +54,7 @@ impl Server {
|
||||
listener_id: self.internal_id,
|
||||
protocol: self.protocol,
|
||||
hostname: self.hostname,
|
||||
tls_acceptor: self.tls.map(|config| TlsAcceptor::from(Arc::new(config))),
|
||||
acceptor: self.acceptor,
|
||||
is_tls_implicit: self.tls_implicit,
|
||||
limiter: ConcurrencyLimiter::new(self.max_connections),
|
||||
shutdown_rx,
|
||||
@@ -223,6 +224,11 @@ impl Servers {
|
||||
spawn(server, shutdown_rx.clone());
|
||||
}
|
||||
|
||||
// Spawn ACME managers
|
||||
for acme_manager in self.acme_managers {
|
||||
acme_manager.spawn(shutdown_rx.clone());
|
||||
}
|
||||
|
||||
(shutdown_tx, shutdown_rx)
|
||||
}
|
||||
}
|
||||
@@ -241,24 +247,36 @@ impl ServerInstance {
|
||||
stream: TcpStream,
|
||||
span: &Span,
|
||||
) -> Result<TlsStream<TcpStream>, ()> {
|
||||
match self.tls_acceptor.as_ref().unwrap().accept(stream).await {
|
||||
Ok(stream) => {
|
||||
tracing::info!(
|
||||
parent: span,
|
||||
context = "tls",
|
||||
event = "handshake",
|
||||
version = ?stream.get_ref().1.protocol_version().unwrap_or(rustls::ProtocolVersion::TLSv1_3),
|
||||
cipher = ?stream.get_ref().1.negotiated_cipher_suite().unwrap_or(TLS13_AES_128_GCM_SHA256),
|
||||
);
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => {
|
||||
match self.acceptor.accept(stream).await {
|
||||
TcpAcceptorResult::Tls(accept) => match accept.await {
|
||||
Ok(stream) => {
|
||||
tracing::info!(
|
||||
parent: span,
|
||||
context = "tls",
|
||||
event = "handshake",
|
||||
version = ?stream.get_ref().1.protocol_version().unwrap_or(rustls::ProtocolVersion::TLSv1_3),
|
||||
cipher = ?stream.get_ref().1.negotiated_cipher_suite().unwrap_or(TLS13_AES_128_GCM_SHA256),
|
||||
);
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
parent: span,
|
||||
context = "tls",
|
||||
event = "error",
|
||||
"Failed to accept TLS connection: {}",
|
||||
err
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
TcpAcceptorResult::Plain(_) | TcpAcceptorResult::Close => {
|
||||
tracing::debug!(
|
||||
parent: span,
|
||||
context = "tls",
|
||||
event = "error",
|
||||
"Failed to accept TLS connection: {}",
|
||||
err
|
||||
"TLS is not configured for this server."
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
|
||||
@@ -23,19 +23,21 @@
|
||||
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
use crate::{acme::AcmeManager, config::ServerProtocol};
|
||||
use rustls::ServerConfig;
|
||||
use std::fmt::Debug;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::TcpStream,
|
||||
sync::watch,
|
||||
};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use crate::config::ServerProtocol;
|
||||
use tokio_rustls::{Accept, TlsAcceptor};
|
||||
|
||||
use self::limiter::{ConcurrencyLimiter, InFlight};
|
||||
|
||||
pub mod limiter;
|
||||
pub mod listen;
|
||||
pub mod tls;
|
||||
|
||||
pub struct ServerInstance {
|
||||
pub id: String,
|
||||
@@ -43,12 +45,34 @@ pub struct ServerInstance {
|
||||
pub protocol: ServerProtocol,
|
||||
pub hostname: String,
|
||||
pub data: String,
|
||||
pub tls_acceptor: Option<TlsAcceptor>,
|
||||
pub acceptor: TcpAcceptor,
|
||||
pub is_tls_implicit: bool,
|
||||
pub limiter: ConcurrencyLimiter,
|
||||
pub shutdown_rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum TcpAcceptor {
|
||||
Tls(TlsAcceptor),
|
||||
Acme {
|
||||
challenge: Arc<ServerConfig>,
|
||||
default: Arc<ServerConfig>,
|
||||
manager: Arc<AcmeManager>,
|
||||
},
|
||||
#[default]
|
||||
Plain,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TcpAcceptorResult<IO>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
Tls(Accept<IO>),
|
||||
Plain(IO),
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct SessionData<T: AsyncRead + AsyncWrite + Unpin + 'static> {
|
||||
pub stream: T,
|
||||
pub local_ip: IpAddr,
|
||||
@@ -63,3 +87,22 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
|
||||
fn spawn(&self, session: SessionData<TcpStream>);
|
||||
fn shutdown(&self);
|
||||
}
|
||||
|
||||
impl Debug for TcpAcceptor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Tls(_) => f.debug_tuple("Tls").finish(),
|
||||
Self::Acme {
|
||||
challenge,
|
||||
default,
|
||||
manager,
|
||||
} => f
|
||||
.debug_struct("Acme")
|
||||
.field("challenge", challenge)
|
||||
.field("default", default)
|
||||
.field("manager", manager)
|
||||
.finish(),
|
||||
Self::Plain => write!(f, "Plain"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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__"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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>| {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user