diff --git a/Cargo.lock b/Cargo.lock index 8bcc87dc..138ff1e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/crates/common/src/config/manager.rs b/crates/common/src/config/manager.rs index 0418e86b..f4c916ef 100644 --- a/crates/common/src/config/manager.rs +++ b/crates/common/src/config/manager.rs @@ -248,11 +248,16 @@ impl ConfigManager { Ok(results) } - pub async fn set(&self, keys: impl IntoIterator) -> store::Result<()> { + pub async fn set(&self, keys: I) -> store::Result<()> + where + I: IntoIterator, + T: Into, + { 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 { diff --git a/crates/common/src/config/scripts.rs b/crates/common/src/config/scripts.rs index a97e1e01..d5f0e720 100644 --- a/crates/common/src/config/scripts.rs +++ b/crates/common/src/config/scripts.rs @@ -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( diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index b782386a..f01e39d9 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -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> { + 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> { + 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>( config: &mut Config, id: &str, diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 011511df..890f5247 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -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 { diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index 6c0e4aa6..43b0d9bc 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -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"), }; diff --git a/crates/common/src/scripts/functions/unicode.rs b/crates/common/src/scripts/functions/unicode.rs index 2d085c8f..84c5daae 100644 --- a/crates/common/src/scripts/functions/unicode.rs +++ b/crates/common/src/scripts/functions/unicode.rs @@ -86,7 +86,7 @@ impl CharUtils for char { pub fn fn_cure_text<'x>(_: &'x Context<'x>, v: Vec) -> Variable { decancer::cure(v[0].to_string().as_ref(), decancer::Options::default()) - .map(|s| s.into_str()) + .map(String::from) .unwrap_or_default() .into() } diff --git a/crates/common/src/scripts/plugins/pyzor.rs b/crates/common/src/scripts/plugins/pyzor.rs index f6810383..40f8e549 100644 --- a/crates/common/src/scripts/plugins/pyzor.rs +++ b/crates/common/src/scripts/plugins/pyzor.rs @@ -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(), diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 0d7417e1..51647350 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -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" diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs new file mode 100644 index 00000000..95683344 --- /dev/null +++ b/crates/jmap/src/api/management/dkim.rs @@ -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 . + * + * 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, + algorithm: Algorithm, + domain: String, + selector: Option, +} + +impl JMAP { + pub async fn handle_manage_dkim( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option>, + ) -> 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::().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>) -> HttpResponse { + let request = + match serde_json::from_slice::(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, + domain: impl Into, + selector: impl Into, + ) -> 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 { + match simple_pem_parse(pk) { + Some(der) => match algo { + Algorithm::Rsa => match RsaKey::::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 { + match s.split_once('-').map(|(algo, _)| algo) { + Some("rsa") => Ok(Algorithm::Rsa), + Some("ed25519") => Ok(Algorithm::Ed25519), + _ => Err(()), + } + } +} diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs index ada8649a..1898de6c 100644 --- a/crates/jmap/src/api/management/domain.rs +++ b/crates/jmap/src/api/management/domain.rs @@ -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> { + // 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::().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) + } } diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index cedbb45e..d80acbc8 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -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, diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs index a7d87587..a28501cd 100644 --- a/crates/utils/src/config/mod.rs +++ b/crates/utils/src/config/mod.rs @@ -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(), + } + } +} diff --git a/resources/config/security.toml b/resources/config/security.toml index 9d73b280..d786b452 100644 --- a/resources/config/security.toml +++ b/resources/config/security.toml @@ -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 diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index 6bf1fe4a..ebced575 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -586,7 +586,7 @@ pub fn spawn_mock_smtp_server() -> (mpsc::Receiver, Arc