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