From ffe2fd08e190a8cb1cc5aa8880e9ec750f0eab6a Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 15 Jun 2023 19:45:39 -0700 Subject: [PATCH] dkim: add option to use openssl for RSA signing This is not on by default, and is not currently enabled for KumoMTA. Using openssl is currently about 3x faster than the pure rust RSA crate, and inline with the perf of the ring crate, which we could use here instead, but which doesn't support keys of size 1024 which are currently widely deployed by the industry, and for which larger sizes do not fit in DNS records. --- Cargo.lock | 56 ++++++++++++++++++++++ crates/dkim/Cargo.toml | 6 +++ crates/dkim/benches/sign.rs | 27 +++++++++++ crates/dkim/examples/sign_bench.rs | 33 ++++++++++++- crates/dkim/src/lib.rs | 2 + crates/dkim/src/sign.rs | 75 ++++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d23378b3..cbb3f7c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,7 @@ dependencies = [ "chrono", "criterion", "ed25519-dalek", + "foreign-types", "futures", "indexmap", "mail-auth", @@ -670,6 +671,8 @@ dependencies = [ "memchr", "nom", "once_cell", + "openssl", + "openssl-sys", "quick-error 2.0.1", "regex", "rsa", @@ -1468,6 +1471,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -3077,12 +3095,50 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "2.10.0" diff --git a/crates/dkim/Cargo.toml b/crates/dkim/Cargo.toml index 0f641b95..ad93187d 100644 --- a/crates/dkim/Cargo.toml +++ b/crates/dkim/Cargo.toml @@ -11,6 +11,9 @@ keywords = ["email", "dkim", "authentification"] readme = "README.md" license = "MIT" +[features] +openssl = ["dep:openssl", "dep:openssl-sys", "dep:foreign-types"] + [dependencies] base64 = "0.21.0" chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] } @@ -21,6 +24,9 @@ mailparse = "0.14" memchr = "2.5" nom = "7.1.0" once_cell = "1.17" +foreign-types = {version="0.3", optional=true} +openssl = { version="0.10.54", optional=true} +openssl-sys = { version="0.9", optional=true} quick-error = "2.0.1" rsa = "0.9" sha-1 = { version = "0.10", features = ["oid"] } diff --git a/crates/dkim/benches/sign.rs b/crates/dkim/benches/sign.rs index c00239b5..332a3798 100644 --- a/crates/dkim/benches/sign.rs +++ b/crates/dkim/benches/sign.rs @@ -80,6 +80,33 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); group.finish(); } + + #[cfg(feature = "openssl")] + for canon in [Type::Simple, Type::Relaxed] { + let data = std::fs::read("./test/keys/2022.private").unwrap(); + let pkey = openssl::rsa::Rsa::private_key_from_pem(&data).unwrap(); + let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap(); + + let signer = SignerBuilder::new() + .with_signed_headers(["From", "Subject"]) + .unwrap() + .with_body_canonicalization(canon) + .with_header_canonicalization(canon) + .with_private_key(DkimPrivateKey::OpenSSLRsa(pkey)) + .with_selector("s20") + .with_signing_domain("example.com") + .with_time(time) + .build() + .unwrap(); + + let mut group = c.benchmark_group("cfdkim openssl signing"); + group.sampling_mode(SamplingMode::Flat); + group.throughput(Throughput::Bytes(email_text.len() as u64)); + group.bench_function(&format!("sign {canon:?}"), |b| { + b.iter(|| signer.sign(black_box(&email)).unwrap()) + }); + group.finish(); + } } pub fn mail_auth_benchmark(c: &mut Criterion) { diff --git a/crates/dkim/examples/sign_bench.rs b/crates/dkim/examples/sign_bench.rs index 3122531e..c671a720 100644 --- a/crates/dkim/examples/sign_bench.rs +++ b/crates/dkim/examples/sign_bench.rs @@ -1,8 +1,8 @@ -use std::time::Instant; use cfdkim::canonicalization::Type; use cfdkim::{DkimPrivateKey, ParsedEmail, SignerBuilder}; use chrono::TimeZone; use rsa::pkcs1::DecodeRsaPrivateKey; +use std::time::Instant; fn email_text() -> String { r#"Subject: subject @@ -51,7 +51,7 @@ ipsum dolor sit a. .replace("\n", "\r\n") } -fn main(){ +fn main() { let email_text = email_text(); let email = ParsedEmail::parse_bytes(email_text.as_bytes()).unwrap(); @@ -79,4 +79,33 @@ fn main(){ } println!("{canon:?}: Did {num_iters} iters in {:?}", start.elapsed()); } + + #[cfg(feature = "openssl")] + for canon in [Type::Simple, Type::Relaxed] { + let data = std::fs::read("./crates/dkim/test/keys/2022.private").unwrap(); + let pkey = openssl::rsa::Rsa::private_key_from_pem(&data).unwrap(); + let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap(); + + let signer = SignerBuilder::new() + .with_signed_headers(["From", "Subject"]) + .unwrap() + .with_body_canonicalization(canon) + .with_header_canonicalization(canon) + .with_private_key(DkimPrivateKey::OpenSSLRsa(pkey)) + .with_selector("s20") + .with_signing_domain("example.com") + .with_time(time) + .build() + .unwrap(); + + let start = Instant::now(); + let num_iters = 1_000; + for _ in 0..num_iters { + signer.sign(&email).unwrap(); + } + println!( + "openssl {canon:?}: Did {num_iters} iters in {:?}", + start.elapsed() + ); + } } diff --git a/crates/dkim/src/lib.rs b/crates/dkim/src/lib.rs index 4fc16676..4c48ebed 100644 --- a/crates/dkim/src/lib.rs +++ b/crates/dkim/src/lib.rs @@ -46,6 +46,8 @@ pub(crate) enum DkimPublicKey { pub enum DkimPrivateKey { Rsa(RsaPrivateKey), Ed25519(ed25519_dalek::Keypair), + #[cfg(feature = "openssl")] + OpenSSLRsa(openssl::rsa::Rsa), } // https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.3 Step 4 diff --git a/crates/dkim/src/sign.rs b/crates/dkim/src/sign.rs index a1318359..8043f9c4 100644 --- a/crates/dkim/src/sign.rs +++ b/crates/dkim/src/sign.rs @@ -104,6 +104,8 @@ impl SignerBuilder { .ok_or(BuilderError("missing required private key"))?; let hash_algo = match private_key { DkimPrivateKey::Rsa(_) => hash::HashAlgo::RsaSha256, + #[cfg(feature = "openssl")] + DkimPrivateKey::OpenSSLRsa(_) => hash::HashAlgo::RsaSha256, DkimPrivateKey::Ed25519(_) => hash::HashAlgo::Ed25519Sha256, }; @@ -176,6 +178,49 @@ impl Signer { .to_bytes() .into() } + #[cfg(feature = "openssl")] + DkimPrivateKey::OpenSSLRsa(private_key) => { + use foreign_types::ForeignType; + + let mut siglen = private_key.size(); + let mut sigbuf = vec![0u8; siglen as usize]; + + // We need to grub around a bit to call into RSA_sign: + // The higher level wrappers available in the openssl + // crate only include EVP_DigestSign which doesn't + // accept a pre-calculated digest like we have here. + + let status = unsafe { + openssl_sys::RSA_sign( + match self.hash_algo { + hash::HashAlgo::RsaSha1 => openssl_sys::NID_sha1, + hash::HashAlgo::RsaSha256 => openssl_sys::NID_sha256, + hash => { + return Err(DKIMError::UnsupportedHashAlgorithm(format!( + "{:?}", + hash + ))) + } + }, + header_hash.as_ptr(), + header_hash.len() as _, + // unsafety: sigbuf must be >= siglen in size + sigbuf.as_mut_ptr(), + &mut siglen, + private_key.as_ptr(), + ) + }; + + if status != 1 || siglen == 0 { + return Err(DKIMError::FailedToSign(format!( + "RSA_sign failed status={status} siglen={siglen} {:?}", + openssl::error::Error::get() + ))); + } + + sigbuf.truncate(siglen as usize); + sigbuf + } }; // add the signature into the DKIM header and generate the header @@ -278,6 +323,36 @@ Hello Alice assert_eq!(header, "DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=s20; c=simple/simple; bh=KXQwQpX2zFwgixPbV6Dd18ZMJU04lLeRnwqzUp8uGwI=; h=from:subject; t=1609459201; b=FNNP5LMX1IK5bnUTZOovwYB5TNGBPInKc2fcyCd2r7zWwe1TpvhoOfqC5emuk1BUHsYzZ0uuR6C6/vMFHi6xwuqzOjnMxd+EKHBBP1ONpK4KTU8+kzSCYWHjb3dq1q8EI8wWXqSC942Lj4qZ4A3cwYia5fF2KVJoeY45T4/xS9oZiNYLrVe1Cwak7ms2zmFcc2r6yK5BAxcxRJx6Ez3/1/N0QSidaEY17HOj9R3bVAYPGarG5oS2mAxNK/iVYxCVP43pqrQwYDoxdZR89VcPQAiYZvDJXpkAs2LcUBWqaV3TDT2LLmg0orA/66LI66JYMqZ9JFr6V/+GkxFJBh8enA==;"); } + #[cfg(feature = "openssl")] + #[test] + fn test_sign_rsa_openssl() { + let raw_email = r#"Subject: subject +From: Sven Sauleau + +Hello Alice + "# + .replace("\n", "\r\n"); + let email = ParsedEmail::parse_bytes(raw_email.as_bytes()).unwrap(); + + let data = std::fs::read("./test/keys/2022.private").unwrap(); + let pkey = openssl::rsa::Rsa::private_key_from_pem(&data).unwrap(); + + let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap(); + + let signer = SignerBuilder::new() + .with_signed_headers(["From", "Subject"]) + .unwrap() + .with_private_key(DkimPrivateKey::OpenSSLRsa(pkey)) + .with_selector("s20") + .with_signing_domain("example.com") + .with_time(time) + .build() + .unwrap(); + let header = signer.sign(&email).unwrap(); + + assert_eq!(header, "DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=s20; c=simple/simple; bh=KXQwQpX2zFwgixPbV6Dd18ZMJU04lLeRnwqzUp8uGwI=; h=from:subject; t=1609459201; b=FNNP5LMX1IK5bnUTZOovwYB5TNGBPInKc2fcyCd2r7zWwe1TpvhoOfqC5emuk1BUHsYzZ0uuR6C6/vMFHi6xwuqzOjnMxd+EKHBBP1ONpK4KTU8+kzSCYWHjb3dq1q8EI8wWXqSC942Lj4qZ4A3cwYia5fF2KVJoeY45T4/xS9oZiNYLrVe1Cwak7ms2zmFcc2r6yK5BAxcxRJx6Ez3/1/N0QSidaEY17HOj9R3bVAYPGarG5oS2mAxNK/iVYxCVP43pqrQwYDoxdZR89VcPQAiYZvDJXpkAs2LcUBWqaV3TDT2LLmg0orA/66LI66JYMqZ9JFr6V/+GkxFJBh8enA==;"); + } + #[test] fn test_sign_ed25519() { let raw_email = r#"From: Joe SixPack