DKIM record management API

This commit is contained in:
mdecimus 2024-04-04 11:51:47 +02:00
parent 93a2f691ea
commit 3a5ca70365
16 changed files with 564 additions and 141 deletions

145
Cargo.lock generated
View file

@ -268,7 +268,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"synstructure 0.13.1",
]
@ -291,14 +291,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
name = "async-compression"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
checksum = "86a9249d1447a85f95810c620abea82e001fe58a31713fcce614caf52499f905"
dependencies = [
"flate2",
"futures-core",
@ -315,7 +315,7 @@ checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -337,7 +337,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -348,7 +348,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -536,7 +536,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.55",
"syn 2.0.58",
"which",
]
@ -679,7 +679,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"syn_derive",
]
@ -922,7 +922,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.0",
"strsim 0.11.1",
]
[[package]]
@ -934,7 +934,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1267,7 +1267,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1315,7 +1315,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1337,7 +1337,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core 0.20.8",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1403,9 +1403,9 @@ dependencies = [
[[package]]
name = "decancer"
version = "3.1.1"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6083786d3fc4f0978c6bdd60334338b2b23a512ce897deafa76be8aa917d30"
checksum = "544abbf2df1b730422bf18a9d9ac020aaf4bea0467b52829d29f4379e27c8383"
dependencies = [
"lazy_static",
"paste",
@ -1414,9 +1414,9 @@ dependencies = [
[[package]]
name = "der"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
@ -1588,7 +1588,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1790,7 +1790,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -1991,7 +1991,7 @@ checksum = "f8db6653cbc621a3810d95d55bd342be3e71181d6df21a4eb29ef986202d3f9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"try_map",
]
@ -2030,7 +2030,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e"
dependencies = [
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -2042,7 +2042,7 @@ dependencies = [
"frunk_core",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -2054,7 +2054,7 @@ dependencies = [
"frunk_core",
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -2119,7 +2119,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -3113,13 +3113,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libredox"
version = "0.0.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.5.0",
"libc",
"redox_syscall",
]
[[package]]
@ -3218,15 +3217,15 @@ dependencies = [
[[package]]
name = "lz4_flex"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15"
checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
[[package]]
name = "mail-auth"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ec1e6a7ed37e42babe87f67b865c9484e271538a3a9c4664e522836cc3e73a"
checksum = "4e9759ecef5c0d048464fee80947ca5ef25faff98add10ea8787a6e195b8dc5f"
dependencies = [
"ahash 0.8.11",
"flate2",
@ -3236,7 +3235,9 @@ dependencies = [
"mail-parser",
"parking_lot",
"quick-xml 0.31.0",
"rand",
"ring 0.17.8",
"rsa",
"rustls-pemfile 2.1.1",
"serde",
"serde_json",
@ -3358,7 +3359,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -3465,7 +3466,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"termcolor",
"thiserror",
]
@ -3509,9 +3510,9 @@ dependencies = [
[[package]]
name = "mysql_common"
version = "0.32.1"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a60cb978c0a1d654edcc1460f8d6092dacf21346ed6017d81fb76a23ef5a8de"
checksum = "0ccdc1fe2bb3ef97e07ba4397327ed45509a1e2e499e2f8265243879cbc7313c"
dependencies = [
"base64 0.21.7",
"bigdecimal",
@ -3756,7 +3757,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -3776,9 +3777,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.101"
version = "0.9.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
dependencies = [
"cc",
"libc",
@ -3804,9 +3805,9 @@ dependencies = [
[[package]]
name = "opentelemetry-http"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbfa5308166ca861434f0b0913569579b8e587430a3d6bcd7fd671921ec145a"
checksum = "7690dc77bf776713848c4faa6501157469017eaf332baccd4eb1cea928743d94"
dependencies = [
"async-trait",
"bytes",
@ -4063,7 +4064,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -4101,14 +4102,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@ -4221,7 +4222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7"
dependencies = [
"proc-macro2",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -4319,7 +4320,7 @@ dependencies = [
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -4591,9 +4592,9 @@ dependencies = [
[[package]]
name = "redox_users"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom",
"libredox",
@ -5237,9 +5238,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.9.2"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@ -5250,9 +5251,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.9.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys",
"libc",
@ -5359,7 +5360,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -5429,7 +5430,7 @@ checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -5830,9 +5831,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subprocess"
@ -5863,9 +5864,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.55"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
@ -5881,7 +5882,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -5910,7 +5911,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -6050,7 +6051,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -6130,9 +6131,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@ -6165,7 +6166,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -6364,7 +6365,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -6740,7 +6741,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"wasm-bindgen-shared",
]
@ -6774,7 +6775,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -7149,9 +7150,9 @@ dependencies = [
[[package]]
name = "xml-rs"
version = "0.8.19"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
[[package]]
name = "xxhash-rust"
@ -7185,7 +7186,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]
@ -7205,7 +7206,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.58",
]
[[package]]

View file

@ -248,11 +248,16 @@ impl ConfigManager {
Ok(results)
}
pub async fn set(&self, keys: impl IntoIterator<Item = ConfigKey>) -> store::Result<()> {
pub async fn set<I, T>(&self, keys: I) -> store::Result<()>
where
I: IntoIterator<Item = T>,
T: Into<ConfigKey>,
{
let mut batch = BatchBuilder::new();
let mut local_batch = Vec::new();
for key in keys {
let key = key.into();
if self.cfg_local_patterns.is_local_key(&key.key) {
local_batch.push(key);
} else {

View file

@ -313,7 +313,7 @@ impl Scripting {
IfBlock::new::<()>(
"sieve.trusted.sign",
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
)
}),
scripts,
@ -349,7 +349,7 @@ impl Default for Scripting {
sign: IfBlock::new::<()>(
"sieve.trusted.sign",
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
),
scripts: AHashMap::new(),
bayes_cache: BayesTokenCache::new(

View file

@ -91,7 +91,7 @@ impl Default for MailAuthConfig {
"auth.dkim.sign",
[(
"is_local_domain('*', sender_domain)",
"['rsa_' + sender_domain, 'ed_' + sender_domain]",
"['rsa-' + sender_domain, 'ed25519-' + sender_domain]",
)],
"false",
),
@ -101,7 +101,7 @@ impl Default for MailAuthConfig {
seal: IfBlock::new::<()>(
"auth.arc.seal",
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
),
},
spf: SpfAuthConfig {
@ -226,50 +226,8 @@ fn build_signature(config: &mut Config, id: &str) -> Option<(DkimSigner, ArcSeal
(DkimSigner::RsaSha256(signer), ArcSealer::RsaSha256(sealer)).into()
}
Algorithm::Ed25519Sha256 => {
let mut public_key = vec![];
let mut private_key = vec![];
for (key, key_bytes) in [
(("signature", id, "public-key"), &mut public_key),
(("signature", id, "private-key"), &mut private_key),
] {
let mut contents = config.value_require(key)?.as_bytes().iter().copied();
let mut base64 = vec![];
'outer: while let Some(ch) = contents.next() {
if !ch.is_ascii_whitespace() {
if ch == b'-' {
for ch in contents.by_ref() {
if ch == b'\n' {
break;
}
}
} else {
base64.push(ch);
}
for ch in contents.by_ref() {
if ch == b'-' {
break 'outer;
} else if !ch.is_ascii_whitespace() {
base64.push(ch);
}
}
}
}
*key_bytes = base64_decode(&base64)
.ok_or_else(|| {
config.new_build_error(
("signature", id),
format!("Failed to base64 decode key for {}.", key.as_key(),),
)
})
.ok()?;
}
let private_key = parse_pem(config, ("signature", id, "private-key"))?;
let key = Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key)
.or_else(|_| Ed25519Key::from_seed_and_public_key(&private_key, &public_key))
.map_err(|err| {
config.new_build_error(
("signature", id),
@ -278,7 +236,6 @@ fn build_signature(config: &mut Config, id: &str) -> Option<(DkimSigner, ArcSeal
})
.ok()?;
let key_clone = Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key)
.or_else(|_| Ed25519Key::from_seed_and_public_key(&private_key, &public_key))
.map_err(|err| {
config.new_build_error(
("signature", id),
@ -304,6 +261,44 @@ fn build_signature(config: &mut Config, id: &str) -> Option<(DkimSigner, ArcSeal
}
}
fn parse_pem(config: &mut Config, key: impl AsKey) -> Option<Vec<u8>> {
if let Some(der) = simple_pem_parse(config.value_require(key.clone())?) {
Some(der)
} else {
config.new_build_error(key, "Failed to base64 decode key.");
None
}
}
pub fn simple_pem_parse(contents: &str) -> Option<Vec<u8>> {
let mut contents = contents.as_bytes().iter().copied();
let mut base64 = vec![];
'outer: while let Some(ch) = contents.next() {
if !ch.is_ascii_whitespace() {
if ch == b'-' {
for ch in contents.by_ref() {
if ch == b'\n' {
break;
}
}
} else {
base64.push(ch);
}
for ch in contents.by_ref() {
if ch == b'-' {
break 'outer;
} else if !ch.is_ascii_whitespace() {
base64.push(ch);
}
}
}
}
base64_decode(&base64)
}
fn parse_signature<T: SigningKey, U: SigningKey<Hasher = Sha256>>(
config: &mut Config,
id: &str,

View file

@ -181,7 +181,7 @@ impl Default for QueueConfig {
sign: IfBlock::new::<()>(
"report.dsn.sign",
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
),
},
timeout: QueueOutboundTimeout {

View file

@ -125,7 +125,7 @@ impl Report {
sign: IfBlock::new::<()>(
format!("report.{id}.sign"),
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
),
send: IfBlock::new::<()>(format!("report.{id}.send"), [], "[1, 1d]"),
};
@ -174,7 +174,7 @@ impl AggregateReport {
sign: IfBlock::new::<()>(
format!("report.{id}.aggregate.sign"),
[],
"['rsa_' + key_get('default', 'domain'), 'ed_' + key_get('default', 'domain')]",
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
),
max_size: IfBlock::new::<()>(format!("report.{id}.aggregate.max-size"), [], "26214400"),
};

View file

@ -86,7 +86,7 @@ impl CharUtils for char {
pub fn fn_cure_text<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
decancer::cure(v[0].to_string().as_ref(), decancer::Options::default())
.map(|s| s.into_str())
.map(String::from)
.unwrap_or_default()
.into()
}

View file

@ -587,11 +587,11 @@ mod test {
// Test spec
let mut text = String::new();
for i in 0..100 {
text += &format!("Line{i} test test test\n");
text += format!("Line{i} test test test\n").as_str();
}
let mut expected = String::new();
for i in [20, 21, 22, 60, 61, 62] {
expected += &format!("Line{i}testtesttest");
expected += format!("Line{i}testtesttest").as_str();
}
assert_eq!(
String::from_utf8(pyzor_digest(Vec::new(), text.lines(), &psl)).unwrap(),

View file

@ -16,7 +16,7 @@ smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5"] }
mail-auth = { version = "0.3" }
mail-auth = { version = "0.3", features = ["generate"] }
sieve-rs = { version = "0.5" }
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"

View file

@ -0,0 +1,266 @@
/*
* 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::str::FromStr;
use common::config::smtp::auth::simple_pem_parse;
use hyper::Method;
use jmap_proto::error::request::RequestError;
use mail_auth::{
common::crypto::{Ed25519Key, RsaKey, Sha256},
dkim::generate::DkimKeyPair,
};
use mail_builder::encoders::base64::base64_encode;
use mail_parser::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::json;
use store::write::now;
use crate::{
api::{
http::ToHttpResponse, management::ManagementApiError, HttpRequest, HttpResponse,
JsonResponse,
},
JMAP,
};
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum Algorithm {
Rsa,
Ed25519,
}
#[derive(Debug, Serialize, Deserialize)]
struct DkimSignature {
id: Option<String>,
algorithm: Algorithm,
domain: String,
selector: Option<String>,
}
impl JMAP {
pub async fn handle_manage_dkim(
&self,
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
match *req.method() {
Method::GET => self.handle_get_public_key(path).await,
Method::POST => self.handle_create_signature(body).await,
_ => RequestError::not_found().into_http_response(),
}
}
async fn handle_get_public_key(&self, path: Vec<&str>) -> HttpResponse {
let signature_id = match path.get(1) {
Some(signature_id) => *signature_id,
None => {
return RequestError::not_found().into_http_response();
}
};
let (pk, algo) = match (
self.core
.storage
.config
.get(&format!("signature.{signature_id}.private-key"))
.await,
self.core
.storage
.config
.get(&format!("signature.{signature_id}.algorithm"))
.await
.map(|algo| algo.and_then(|algo| algo.parse::<Algorithm>().ok())),
) {
(Ok(Some(pk)), Ok(Some(algorithm))) => (pk, algorithm),
(Err(err), _) | (_, Err(err)) => return err.into_http_response(),
_ => return RequestError::not_found().into_http_response(),
};
match obtain_dkim_public_key(algo, &pk) {
Ok(data) => JsonResponse::new(json!({
"data": data,
}))
.into_http_response(),
Err(err) => ManagementApiError::Other {
details: err.into(),
}
.into_http_response(),
}
}
async fn handle_create_signature(&self, body: Option<Vec<u8>>) -> HttpResponse {
let request =
match serde_json::from_slice::<DkimSignature>(body.as_deref().unwrap_or_default()) {
Ok(request) => request,
Err(err) => return err.into_http_response(),
};
let algo_str = match request.algorithm {
Algorithm::Rsa => "rsa",
Algorithm::Ed25519 => "ed25519",
};
let id = request
.id
.unwrap_or_else(|| format!("{algo_str}-{}", request.domain));
let selector = request.selector.unwrap_or_else(|| {
let dt = DateTime::from_timestamp(now() as i64);
format!(
"{:04}{:02}{}",
dt.year,
dt.month,
if Algorithm::Rsa == request.algorithm {
"r"
} else {
"e"
}
)
});
// Make sure the signature does not exist already
match self
.core
.storage
.config
.get(&format!("signature.{id}.private-key"))
.await
{
Ok(None) => (),
Ok(Some(value)) => {
return ManagementApiError::FieldAlreadyExists {
field: format!("signature.{id}.private-key").into(),
value: value.into(),
}
.into_http_response();
}
Err(err) => return err.into_http_response(),
}
// Create signature
match self
.create_dkim_key(request.algorithm, id, request.domain, selector)
.await
{
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
}
async fn create_dkim_key(
&self,
algo: Algorithm,
id: impl AsRef<str>,
domain: impl Into<String>,
selector: impl Into<String>,
) -> store::Result<()> {
let id = id.as_ref();
let (algorithm, pk_type) = match algo {
Algorithm::Rsa => ("rsa-sha256", "RSA PRIVATE KEY"),
Algorithm::Ed25519 => ("ed25519-sha256", "PRIVATE KEY"),
};
let mut pk = format!("-----BEGIN {pk_type}-----\n").into_bytes();
let mut lf_count = 65;
for ch in base64_encode(
match algo {
Algorithm::Rsa => DkimKeyPair::generate_rsa(2048),
Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(),
}
.map_err(|err| store::Error::InternalError(err.to_string()))?
.private_key(),
)
.unwrap_or_default()
{
pk.push(ch);
lf_count -= 1;
if lf_count == 0 {
pk.push(b'\n');
lf_count = 65;
}
}
if lf_count != 65 {
pk.push(b'\n');
}
pk.extend_from_slice(format!("-----END {pk_type}-----\n").as_bytes());
self.core
.storage
.config
.set([
(
format!("signature.{id}.private-key"),
String::from_utf8(pk).unwrap(),
),
(format!("signature.{id}.domain"), domain.into()),
(format!("signature.{id}.selector"), selector.into()),
(format!("signature.{id}.algorithm"), algorithm.to_string()),
(
format!("signature.{id}.canonicalization"),
"relaxed/relaxed".to_string(),
),
(
format!("signature.{id}.headers"),
"['From', 'To', 'Date', 'Subject', 'Message-ID']".to_string(),
),
(format!("signature.{id}.report"), "false".to_string()),
])
.await
}
}
pub fn obtain_dkim_public_key(algo: Algorithm, pk: &str) -> Result<String, &'static str> {
match simple_pem_parse(pk) {
Some(der) => match algo {
Algorithm::Rsa => match RsaKey::<Sha256>::from_der(&der) {
Ok(pk) => Ok(String::from_utf8(
base64_encode(&pk.public_key()).unwrap_or_default(),
)
.unwrap_or_default()),
Err(_) => Err("Failed to read RSA DER"),
},
Algorithm::Ed25519 => match Ed25519Key::from_pkcs8_der(&der) {
Ok(pk) => Ok(String::from_utf8(
base64_encode(&pk.public_key()).unwrap_or_default(),
)
.unwrap_or_default()),
Err(_) => Err("Failed to read ED25519 PKCS#8 DER"),
},
},
None => Err("Failed to decode private key"),
}
}
impl FromStr for Algorithm {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('-').map(|(algo, _)| algo) {
Some("rsa") => Ok(Algorithm::Rsa),
Some("ed25519") => Ok(Algorithm::Ed25519),
_ => Err(()),
}
}
}

View file

@ -25,14 +25,28 @@ use directory::backend::internal::manage::ManageDirectory;
use http_body_util::combinators::BoxBody;
use hyper::{body::Bytes, Method};
use jmap_proto::error::request::RequestError;
use serde::{Deserialize, Serialize};
use serde_json::json;
use store::ahash::AHashMap;
use utils::url_params::UrlParams;
use crate::{
api::{http::ToHttpResponse, HttpRequest, JsonResponse},
api::{
http::ToHttpResponse,
management::dkim::{obtain_dkim_public_key, Algorithm},
HttpRequest, JsonResponse,
},
JMAP,
};
#[derive(Debug, Serialize, Deserialize)]
struct DnsRecord {
#[serde(rename = "type")]
typ: String,
name: String,
content: String,
}
impl JMAP {
pub async fn handle_manage_domain(
&self,
@ -70,12 +84,17 @@ impl JMAP {
Err(err) => err.into_http_response(),
}
}
(Some(domain), &Method::POST) => {
// Make sure the current directory supports updates
if let Some(response) = self.assert_supported_directory() {
return response;
(Some(domain), &Method::GET) => {
// Obtain DNS records
match self.build_dns_records(domain).await {
Ok(records) => JsonResponse::new(json!({
"data": records,
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
}
(Some(domain), &Method::POST) => {
// Create domain
match self.core.storage.data.create_domain(domain).await {
Ok(_) => JsonResponse::new(json!({
@ -99,4 +118,98 @@ impl JMAP {
_ => RequestError::not_found().into_http_response(),
}
}
async fn build_dns_records(&self, domain_name: &str) -> store::Result<Vec<DnsRecord>> {
// Obtain server name
let server_name = self
.core
.storage
.config
.get("lookup.default.hostname")
.await?
.unwrap_or_else(|| "localhost".to_string());
let mut records = Vec::new();
// Obtain DKIM keys
let mut keys = AHashMap::new();
let mut signature_ids = Vec::new();
for (key, value) in self.core.storage.config.list("signature.", true).await? {
match key.strip_suffix(".domain") {
Some(key_id) if value == domain_name => {
signature_ids.push(key_id.to_string());
}
_ => (),
}
keys.insert(key, value);
}
// Add MX and CNAME records
records.push(DnsRecord {
typ: "MX".to_string(),
name: format!("{domain_name}."),
content: format!("10 {server_name}."),
});
if server_name
.strip_prefix("mail.")
.map_or(true, |s| s != domain_name)
{
records.push(DnsRecord {
typ: "CNAME".to_string(),
name: format!("mail.{domain_name}."),
content: format!("{server_name}."),
});
}
// Process DKIM keys
for signature_id in signature_ids {
if let (Some(algo), Some(pk), Some(selector)) = (
keys.get(&format!("{signature_id}.algorithm"))
.and_then(|algo| algo.parse::<Algorithm>().ok()),
keys.get(&format!("{signature_id}.private-key")),
keys.get(&format!("{signature_id}.selector")),
) {
match obtain_dkim_public_key(algo, pk) {
Ok(public) => {
records.push(DnsRecord {
typ: "TXT".to_string(),
name: format!("{selector}._domainkey.{domain_name}.",),
content: match algo {
Algorithm::Rsa => format!("v=DKIM1; k=rsa; h=sha256; p={public}"),
Algorithm::Ed25519 => {
format!("v=DKIM1; k=ed25519; h=sha256; p={public}")
}
},
});
}
Err(err) => {
tracing::debug!("Failed to obtain DKIM public key: {}", err);
}
}
}
}
// Add SPF records
if server_name.ends_with(&format!(".{domain_name}")) || server_name == domain_name {
records.push(DnsRecord {
typ: "TXT".to_string(),
name: format!("{server_name}."),
content: "v=spf1 a -all ra=postmaster".to_string(),
});
}
records.push(DnsRecord {
typ: "TXT".to_string(),
name: format!("{domain_name}."),
content: "v=spf1 mx -all ra=postmaster".to_string(),
});
// Add DMARC records
records.push(DnsRecord {
typ: "TXT".to_string(),
name: format!("_dmarc.{domain_name}."),
content: format!("v=DMARC1; p=reject; rua=mailto:postmaster@{domain_name}; ruf=mailto:postmaster@{domain_name}",),
});
Ok(records)
}
}

View file

@ -21,6 +21,7 @@
* for more details.
*/
pub mod dkim;
pub mod domain;
pub mod principal;
pub mod queue;
@ -83,6 +84,7 @@ impl JMAP {
"settings" if is_superuser => self.handle_manage_settings(req, path, body).await,
"queue" if is_superuser => self.handle_manage_queue(req, path).await,
"reports" if is_superuser => self.handle_manage_reports(req, path).await,
"dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await,
"oauth" => self.handle_oauth_api_request(access_token, body).await,
"crypto" => match *req.method() {
Method::POST => self.handle_crypto_post(access_token, body).await,

View file

@ -217,3 +217,36 @@ impl Config {
}
}
}
impl From<(String, String)> for ConfigKey {
fn from((key, value): (String, String)) -> Self {
Self { key, value }
}
}
impl From<(&str, &str)> for ConfigKey {
fn from((key, value): (&str, &str)) -> Self {
Self {
key: key.to_string(),
value: value.to_string(),
}
}
}
impl From<(&str, String)> for ConfigKey {
fn from((key, value): (&str, String)) -> Self {
Self {
key: key.to_string(),
value,
}
}
}
impl From<(String, &str)> for ConfigKey {
fn from((key, value): (String, &str)) -> Self {
Self {
key,
value: value.to_string(),
}
}
}

View file

@ -1,19 +1,19 @@
[[queue.quota]]
[queue.quota.queue-max-size]
messages = 100000
size = 10737418240 # 10gb
enable = true
[[queue.throttle]]
[queue.throttle.recipient-limit]
key = ["rcpt_domain"]
concurrency = 5
enable = true
[[session.throttle]]
[session.throttle.concurrency-by-remote-ip]
key = ["remote_ip"]
concurrency = 5
enable = true
[[session.throttle]]
[session.throttle.rate-by-sender]
key = ["sender_domain", "rcpt"]
rate = "25/1h"
enable = true

View file

@ -586,7 +586,7 @@ pub fn spawn_mock_smtp_server() -> (mpsc::Receiver<MockMessage>, Arc<Mutex<MockS
message.message = message.message.trim().to_string();
break;
} else {
message.message += &buf;
message.message += buf.as_str();
buf.clear();
}
}

View file

@ -80,9 +80,8 @@ set-body-length = true
report = true
[signature.ed]
public-key = '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='
private-key = '-----BEGIN PRIVATE KEY-----
nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=
MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B
-----END PRIVATE KEY-----'
domain = 'example.com'
selector = 'ed'
@ -142,6 +141,15 @@ verify = "relaxed"
#[tokio::test]
async fn sign_and_seal() {
// Enable logging
/*let disable = "true";
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::TRACE)
.finish(),
)
.unwrap();*/
let tmp_dir = TempDir::new("smtp_sign_test", true);
let mut config = Config::new(tmp_dir.update_config(CONFIG.to_string() + SIGNATURES)).unwrap();
let stores = Stores::parse_all(&mut config).await;