mirror of
https://github.com/mailscope/kumomta.git
synced 2026-09-13 14:02:13 +00:00
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.
This commit is contained in:
Generated
+56
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ pub(crate) enum DkimPublicKey {
|
||||
pub enum DkimPrivateKey {
|
||||
Rsa(RsaPrivateKey),
|
||||
Ed25519(ed25519_dalek::Keypair),
|
||||
#[cfg(feature = "openssl")]
|
||||
OpenSSLRsa(openssl::rsa::Rsa<openssl::pkey::Private>),
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.3 Step 4
|
||||
|
||||
@@ -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 <sven@cloudflare.com>
|
||||
|
||||
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 <joe@football.example.com>
|
||||
|
||||
Reference in New Issue
Block a user