Skip to content

Commit

Permalink
dkim: remove usage of rsa crate
Browse files Browse the repository at this point in the history
We keep getting asked about
https://rustsec.org/advisories/RUSTSEC-2023-0071.html and how it impacts
kumomta.

The answer to that question is: in the default build configuration, we
use openssl's RSA signing implementation rather than that of the rsa
crate.  The reason for this is that OpenSSL's RSA implementation is due
to the performance gap between the two implementations
(RustCrypto/RSA#339). The result of this is
that the problematic code and attack vector described in the security
advisory does not apply to KumoMTA, because it is not used to compute
any signatures.

In the interest of not raising any false alarms as more and more people
perform security analyses on kumomta, this commit removes the `rsa`
crate from the build graph. In order to do so, we need to port
verification over to the openssl RSA implementation which is what this
commit does.

I look forward to a future version of the `rsa` crate being published
that has this issue resolved, and that closes the performance gap!

refs: RustCrypto/RSA#390
  • Loading branch information
wez committed Apr 10, 2024
1 parent f0fdec1 commit fa1dec6
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 196 deletions.
54 changes: 0 additions & 54 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 4 additions & 7 deletions crates/dkim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,21 @@ readme = "README.md"
license = "MIT"

[features]
openssl = ["dep:openssl", "dep:openssl-sys", "dep:foreign-types"]
default = ["openssl"]

[dependencies]
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] }
data-encoding = "2.5"
ed25519-dalek = {workspace=true, features=["pkcs8"]}
ed25519-dalek = {workspace=true, features=["pkcs8", "pem"]}
futures = {workspace=true}
indexmap = "1.9.3"
mailparsing = { path="../mailparsing" }
memchr = "2.5"
nom = "7.1.0"
once_cell = "1.17"
foreign-types = {version="0.3", optional=true}
openssl = { workspace=true, optional=true}
openssl-sys = { workspace=true, optional=true}
foreign-types = "0.3"
openssl = { workspace=true }
openssl-sys = { workspace=true }
quick-error = "2.0.1"
rsa = "0.9"
sha-1 = { version = "0.10", features = ["oid"] }
sha2 = { version = "0.10", features = ["oid"] }
textwrap = "0.16"
Expand Down
28 changes: 0 additions & 28 deletions crates/dkim/benches/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use chrono::TimeZone;
use criterion::{black_box, criterion_group, criterion_main, Criterion, SamplingMode, Throughput};
use kumo_dkim::canonicalization::Type;
use kumo_dkim::{DkimPrivateKey, ParsedEmail, SignerBuilder};
use rsa::pkcs1::DecodeRsaPrivateKey;

fn email_text() -> String {
r#"Subject: subject
Expand Down Expand Up @@ -55,33 +54,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let email_text = email_text();
let email = ParsedEmail::parse(email_text.clone()).unwrap();

for canon in [Type::Simple, Type::Relaxed] {
let private_key =
rsa::RsaPrivateKey::read_pkcs1_pem_file("./test/keys/2022.private").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::Rsa(private_key))
.with_selector("s20")
.with_signing_domain("example.com")
.with_time(time)
.build()
.unwrap();

let mut group = c.benchmark_group("kumo_dkim 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();
}

#[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();
Expand Down
27 changes: 0 additions & 27 deletions crates/dkim/examples/sign_bench.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use chrono::TimeZone;
use kumo_dkim::canonicalization::Type;
use kumo_dkim::{DkimPrivateKey, ParsedEmail, SignerBuilder};
use rsa::pkcs1::DecodeRsaPrivateKey;
use std::time::Instant;

fn email_text() -> String {
Expand Down Expand Up @@ -55,32 +54,6 @@ fn main() {
let email_text = email_text();
let email = ParsedEmail::parse(email_text).unwrap();

for canon in [Type::Simple, Type::Relaxed] {
let private_key =
rsa::RsaPrivateKey::read_pkcs1_pem_file("crates/dkim/test/keys/2022.private").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::Rsa(private_key))
.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!("{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();
Expand Down
95 changes: 40 additions & 55 deletions crates/dkim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

use crate::errors::Status;
use crate::hash::HeaderList;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::SigningKey;
use hickory_resolver::TokioAsyncResolver;
use mailparsing::AuthenticationResult;
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use sha1::Sha1;
use sha2::Sha256;
use openssl::md::Md;
use openssl::pkey::PKey;
use openssl::pkey_ctx::PkeyCtx;
use openssl::rsa::{Padding, Rsa};
use std::collections::BTreeMap;

#[macro_use]
Expand Down Expand Up @@ -37,58 +37,29 @@ const DNS_NAMESPACE: &str = "_domainkey";

#[derive(Debug)]
pub(crate) enum DkimPublicKey {
Rsa(RsaPublicKey),
Rsa(PKey<openssl::pkey::Public>),
Ed25519(ed25519_dalek::VerifyingKey),
}

#[derive(Debug)]
pub enum DkimPrivateKey {
Rsa(RsaPrivateKey),
Ed25519(SigningKey),
#[cfg(feature = "openssl")]
OpenSSLRsa(openssl::rsa::Rsa<openssl::pkey::Private>),
OpenSSLRsa(Rsa<openssl::pkey::Private>),
}

impl DkimPrivateKey {
/// Parse RSA key data into a DkimPrivateKey
pub fn rsa_key(data: &[u8]) -> Result<Self, DKIMError> {
let mut errors = vec![];

#[cfg(feature = "openssl")]
{
use openssl::rsa::Rsa;

match Rsa::private_key_from_pem(data) {
Ok(key) => return Ok(Self::OpenSSLRsa(key)),
Err(err) => errors.push(format!("openssl private_key_from_pem: {err:#}")),
};
match Rsa::private_key_from_der(data) {
Ok(key) => return Ok(Self::OpenSSLRsa(key)),
Err(err) => errors.push(format!("openssl private_key_from_der: {err:#}")),
};
}
match RsaPrivateKey::from_pkcs1_der(data) {
Ok(key) => return Ok(Self::Rsa(key)),
Err(err) => errors.push(format!("from_pkcs1_der: {err:#}")),
}
match RsaPrivateKey::from_pkcs8_der(data) {
Ok(key) => return Ok(Self::Rsa(key)),
Err(err) => errors.push(format!("from_pkcs8_der: {err:#}")),
}

match std::str::from_utf8(data) {
Ok(s) => {
match RsaPrivateKey::from_pkcs1_pem(s) {
Ok(key) => return Ok(Self::Rsa(key)),
Err(err) => errors.push(format!("from_pkcs1_pem: {err:#}")),
}
match RsaPrivateKey::from_pkcs8_pem(s) {
Ok(key) => return Ok(Self::Rsa(key)),
Err(err) => errors.push(format!("from_pkcs8_pem: {err:#}")),
}
}
Err(err) => errors.push(format!("from_pkcs1_pem: data is not UTF-8: {err:#}")),
}
match Rsa::private_key_from_pem(data) {
Ok(key) => return Ok(Self::OpenSSLRsa(key)),
Err(err) => errors.push(format!("openssl private_key_from_pem: {err:#}")),
};
match Rsa::private_key_from_der(data) {
Ok(key) => return Ok(Self::OpenSSLRsa(key)),
Err(err) => errors.push(format!("openssl private_key_from_der: {err:#}")),
};

Err(DKIMError::PrivateKeyLoadError(errors.join(". ")))
}
Expand Down Expand Up @@ -134,17 +105,31 @@ fn verify_signature(
public_key: DkimPublicKey,
) -> Result<bool, DKIMError> {
Ok(match public_key {
DkimPublicKey::Rsa(public_key) => public_key
.verify(
match hash_algo {
hash::HashAlgo::RsaSha1 => Pkcs1v15Sign::new::<Sha1>(),
hash::HashAlgo::RsaSha256 => Pkcs1v15Sign::new::<Sha256>(),
hash => return Err(DKIMError::UnsupportedHashAlgorithm(format!("{:?}", hash))),
},
header_hash,
signature,
)
.is_ok(),
DkimPublicKey::Rsa(public_key) => {
let md = match hash_algo {
hash::HashAlgo::RsaSha1 => Md::sha1(),
hash::HashAlgo::RsaSha256 => Md::sha256(),
hash => return Err(DKIMError::UnsupportedHashAlgorithm(format!("{:?}", hash))),
};

let mut ctx = PkeyCtx::new(&public_key).map_err(|err| {
DKIMError::SignatureSyntaxError(format!("Error loading RSA public key: {err}"))
})?;

ctx.verify_init().map_err(|err| {
DKIMError::UnknownInternalError(format!("ctx.verify_init failed: {err}"))
})?;
ctx.set_rsa_padding(Padding::PKCS1).map_err(|err| {
DKIMError::UnknownInternalError(format!("ctx.set_rsa_padding failed: {err}"))
})?;
ctx.set_signature_md(&md).map_err(|err| {
DKIMError::UnknownInternalError(format!("ctx.set_signature_md failed: {err}"))
})?;
match ctx.verify(header_hash, signature) {
Ok(result) => result,
Err(_) => false,
}
}
DkimPublicKey::Ed25519(public_key) => {
let mut sig_bytes = [0u8; ed25519_dalek::Signature::BYTE_SIZE];
if signature.len() != sig_bytes.len() {
Expand Down
18 changes: 12 additions & 6 deletions crates/dkim/src/public_key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use rsa::{pkcs1, pkcs8};
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use std::collections::HashMap;

use crate::{dns, parser, DKIMError, DkimPublicKey, DNS_NAMESPACE};
Expand Down Expand Up @@ -56,11 +57,16 @@ pub(crate) async fn retrieve_public_key(
})?;
let key = if key_type == RSA_KEY_TYPE {
DkimPublicKey::Rsa(
pkcs8::DecodePublicKey::from_public_key_der(&bytes)
.or_else(|_| pkcs1::DecodeRsaPublicKey::from_pkcs1_der(&bytes))
.map_err(|err| {
DKIMError::KeyUnavailable(format!("failed to parse public key: {}", err))
})?,
PKey::from_rsa(
Rsa::public_key_from_der(&bytes)
.or_else(|_| Rsa::public_key_from_der_pkcs1(&bytes))
.map_err(|err| {
DKIMError::KeyUnavailable(format!("failed to parse public key: {}", err))
})?,
)
.map_err(|err| {
DKIMError::KeyUnavailable(format!("failed to parse public key: {}", err))
})?,
)
} else {
let mut key_bytes = [0u8; ed25519_dalek::PUBLIC_KEY_LENGTH];
Expand Down
Loading

0 comments on commit fa1dec6

Please sign in to comment.