diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bfb8aa..5a9e8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +mail-auth 0.3.11 +================================ +- Added: DKIM keypair generation for both RSA and Ed25519. +- Fix: Check PTR against FQDN (including dot at the end) #28 + mail-auth 0.3.10 ================================ - Make `Resolver` cloneable. diff --git a/Cargo.toml b/Cargo.toml index c916180..1899fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mail-auth" description = "DKIM, ARC, SPF and DMARC library for Rust" -version = "0.3.10" +version = "0.3.11" edition = "2021" authors = [ "Stalwart Labs "] license = "Apache-2.0 OR MIT" @@ -18,6 +18,7 @@ doctest = false [features] default = ["ring", "rustls-pemfile"] rust-crypto = ["ed25519-dalek", "rsa", "sha1", "sha2"] +generate = ["rsa", "rand"] test = [] [dependencies] @@ -30,7 +31,7 @@ mail-builder = { version = "0.3", features = ["ludicrous_mode"] } parking_lot = "0.12.0" quick-xml = "0.31" ring = { version = "0.17", optional = true } -rsa = { version = "0.7", optional = true } +rsa = { version = "0.9.6", optional = true } rustls-pemfile = { version = "2", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -38,6 +39,7 @@ sha1 = { version = "0.10", features = ["oid"], optional = true } sha2 = { version = "0.10.6", features = ["oid"], optional = true } hickory-resolver = { version = "0.24", features = ["dns-over-rustls", "dnssec-ring"] } zip = "0.6.3" +rand = { version = "0.8.5", optional = true } [dev-dependencies] tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] } diff --git a/README.md b/README.md index 59cf45f..28087f5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Features: - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 signing and verification. - DKIM Authorized Third-Party Signatures. - DKIM failure reporting using the Abuse Reporting Format. + - Key-pair generation for both RSA and Ed25519 (enabled by the `generate` feature). - **Authenticated Received Chain (ARC)**: - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification. - ARC sealing. diff --git a/src/common/crypto/ring_impls.rs b/src/common/crypto/ring_impls.rs index 02c40e9..307ed92 100644 --- a/src/common/crypto/ring_impls.rs +++ b/src/common/crypto/ring_impls.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use ring::digest::{Context, SHA1_FOR_LEGACY_USE_ONLY, SHA256}; use ring::rand::SystemRandom; use ring::signature::{ - Ed25519KeyPair, RsaKeyPair, UnparsedPublicKey, ED25519, + Ed25519KeyPair, KeyPair, RsaKeyPair, UnparsedPublicKey, ED25519, RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, RSA_PKCS1_SHA256, }; @@ -68,6 +68,11 @@ impl RsaKey { padding: PhantomData, }) } + + /// Returns the public key of the RSA key pair. + pub fn public_key(&self) -> Vec { + self.inner.public().as_ref().to_vec() + } } impl SigningKey for RsaKey { @@ -94,6 +99,13 @@ pub struct Ed25519Key { } impl Ed25519Key { + pub fn generate_pkcs8() -> Result> { + Ok(Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()) + .map_err(|err| Error::CryptoError(err.to_string()))? + .as_ref() + .to_vec()) + } + pub fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result { Ok(Self { inner: Ed25519KeyPair::from_pkcs8(pkcs8_der) @@ -114,6 +126,11 @@ impl Ed25519Key { .map_err(|err| Error::CryptoError(err.to_string()))?, }) } + + // Returns the public key of the Ed25519 key pair. + pub fn public_key(&self) -> Vec { + self.inner.public_key().as_ref().to_vec() + } } impl SigningKey for Ed25519Key { diff --git a/src/common/crypto/rust_crypto.rs b/src/common/crypto/rust_crypto.rs index 2ec8966..39e5632 100644 --- a/src/common/crypto/rust_crypto.rs +++ b/src/common/crypto/rust_crypto.rs @@ -2,7 +2,7 @@ use std::array::TryFromSliceError; use std::marker::PhantomData; use ed25519_dalek::Signer; -use rsa::{pkcs1::DecodeRsaPrivateKey, PaddingScheme, PublicKey as _, RsaPrivateKey}; +use rsa::{pkcs1::DecodeRsaPrivateKey, Pkcs1v15Sign, RsaPrivateKey}; use sha2::digest::Digest; use crate::{ @@ -50,7 +50,7 @@ impl SigningKey for RsaKey { let hash = self.hash(input); self.inner .sign( - PaddingScheme::new_pkcs1v15_sign::<::Context>(), + Pkcs1v15Sign::new::<::Context>(), hash.as_ref(), ) .map_err(|err| Error::CryptoError(err.to_string())) @@ -68,7 +68,7 @@ impl SigningKey for RsaKey { let hash = self.hash(input); self.inner .sign( - PaddingScheme::new_pkcs1v15_sign::<::Context>(), + Pkcs1v15Sign::new::<::Context>(), hash.as_ref(), ) .map_err(|err| Error::CryptoError(err.to_string())) @@ -141,7 +141,7 @@ impl VerifyingKey for RsaPublicKey { self.inner .verify( - PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), hash.as_ref(), signature, ) @@ -153,11 +153,7 @@ impl VerifyingKey for RsaPublicKey { let hash = hasher.finalize(); self.inner - .verify( - PaddingScheme::new_pkcs1v15_sign::(), - hash.as_ref(), - signature, - ) + .verify(Pkcs1v15Sign::new::(), hash.as_ref(), signature) .map_err(|_| Error::FailedVerification) } Algorithm::Ed25519Sha256 => Err(Error::IncompatibleAlgorithms), diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs index e35ec3e..d27ac40 100644 --- a/src/dkim/canonicalize.rs +++ b/src/dkim/canonicalize.rs @@ -323,6 +323,17 @@ mod test { } .write(&mut hasher); + #[cfg(feature = "sha1")] + { + use sha1::Digest; + assert_eq!( + String::from_utf8(base64_encode(hasher.finalize().as_ref()).unwrap()) + .unwrap(), + hash, + ); + } + + #[cfg(all(feature = "ring", not(feature = "sha1")))] assert_eq!( String::from_utf8(base64_encode(hasher.finish().as_ref()).unwrap()).unwrap(), hash, diff --git a/src/dkim/generate.rs b/src/dkim/generate.rs new file mode 100644 index 0000000..7e76a54 --- /dev/null +++ b/src/dkim/generate.rs @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 or the MIT license + * , at your + * option. This file may not be copied, modified, or distributed + * except according to those terms. + */ + +use mail_builder::encoders::base64::base64_encode; +use rsa::{ + pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey}, + RsaPrivateKey, RsaPublicKey, +}; + +use crate::{common::crypto::Ed25519Key, Error}; + +pub struct DkimKeyPair { + private_key: Vec, + public_key: Vec, +} + +impl DkimKeyPair { + /// Generates a new RSA key pair encoded in PKCS#1 DER format with the given number of bits + pub fn generate_rsa(bits: usize) -> crate::Result { + //TODO: Use `ring` once it supports RSA key generation + let priv_key = RsaPrivateKey::new(&mut rand::thread_rng(), bits) + .map_err(|err| Error::CryptoError(err.to_string()))?; + let pub_key = RsaPublicKey::from(&priv_key); + + Ok(DkimKeyPair { + private_key: priv_key + .to_pkcs1_der() + .map_err(|err| Error::CryptoError(err.to_string()))? + .as_bytes() + .to_vec(), + public_key: pub_key + .to_pkcs1_der() + .map_err(|err| Error::CryptoError(err.to_string()))? + .as_bytes() + .to_vec(), + }) + } + + /// Generates a new Ed25519 key pair encoded in PKCS#8 DER format + pub fn generate_ed25519() -> crate::Result { + let pkcs8_der = + Ed25519Key::generate_pkcs8().map_err(|err| Error::CryptoError(err.to_string()))?; + let key = Ed25519Key::from_pkcs8_der(&pkcs8_der).unwrap(); + + Ok(DkimKeyPair { + private_key: pkcs8_der, + public_key: key.public_key(), + }) + } + + pub fn public_key(&self) -> &[u8] { + &self.public_key + } + + pub fn private_key(&self) -> &[u8] { + &self.private_key + } + + pub fn into_inner(self) -> (Vec, Vec) { + (self.private_key, self.public_key) + } + + pub fn encoded_public_key(&self) -> String { + String::from_utf8(base64_encode(&self.public_key).unwrap_or_default()).unwrap_or_default() + } +} + +#[cfg(test)] +mod test { + use crate::dkim::sign::test::verify; + use std::time::{Duration, Instant}; + + use crate::{ + common::{ + crypto::{Ed25519Key, RsaKey, Sha256}, + parse::TxtRecordParser, + verify::DomainKey, + }, + dkim::{generate::DkimKeyPair, DkimSigner, DomainKeyReport}, + Resolver, + }; + + #[tokio::test] + async fn dkim_generate_verify() { + let rsa_pkcs = DkimKeyPair::generate_rsa(2048).unwrap(); + let ed_pkcs = DkimKeyPair::generate_ed25519().unwrap(); + + let rsa_public = format!("v=DKIM1; t=s; p={}", rsa_pkcs.encoded_public_key()); + let ed_public = format!("v=DKIM1; k=ed25519; p={}", ed_pkcs.encoded_public_key()); + + let pk_ed = Ed25519Key::from_pkcs8_der(&ed_pkcs.private_key).unwrap(); + let pk_rsa = RsaKey::::from_der(&rsa_pkcs.private_key).unwrap(); + + // Create resolver + let resolver = Resolver::new_system_conf().unwrap(); + #[cfg(any(test, feature = "test"))] + { + resolver.txt_add( + "default._domainkey.example.com.".to_string(), + DomainKey::parse(rsa_public.as_bytes()).unwrap(), + Instant::now() + Duration::new(3600, 0), + ); + resolver.txt_add( + "ed._domainkey.example.com.".to_string(), + DomainKey::parse(ed_public.as_bytes()).unwrap(), + Instant::now() + Duration::new(3600, 0), + ); + resolver.txt_add( + "_report._domainkey.example.com.".to_string(), + DomainKeyReport::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(), + Instant::now() + Duration::new(3600, 0), + ); + } + + let message = concat!( + "From: bill@example.com\r\n", + "To: jdoe@example.com\r\n", + "Subject: TPS Report\r\n", + "\r\n", + "I'm going to need those TPS reports ASAP. ", + "So, if you could do that, that'd be great.\r\n" + ); + + dbg!("Test generated RSA key"); + verify( + &resolver, + DkimSigner::from_key(pk_rsa) + .domain("example.com") + .selector("default") + .headers(["From", "To", "Subject"]) + .agent_user_identifier("\"John Doe\" ") + .sign(message.as_bytes()) + .unwrap(), + message, + Ok(()), + ) + .await; + + dbg!("Test ED25519 generated key"); + verify( + &resolver, + DkimSigner::from_key(pk_ed) + .domain("example.com") + .selector("ed") + .headers(["From", "To", "Subject"]) + .sign(message.as_bytes()) + .unwrap(), + message, + Ok(()), + ) + .await; + } +} diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs index 3994232..ee4b110 100644 --- a/src/dkim/mod.rs +++ b/src/dkim/mod.rs @@ -19,6 +19,8 @@ use crate::{ pub mod builder; pub mod canonicalize; +#[cfg(feature = "generate")] +pub mod generate; pub mod headers; pub mod parse; pub mod sign; diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs index 99fdf8d..7e1c9ec 100644 --- a/src/dkim/sign.rs +++ b/src/dkim/sign.rs @@ -105,7 +105,7 @@ impl<'a> Writable for SignableMessage<'a> { #[cfg(test)] #[allow(unused)] -mod test { +pub mod test { use std::time::{Duration, Instant}; use hickory_resolver::proto::op::ResponseCode; @@ -486,7 +486,7 @@ mod test { .await; } - async fn verify<'x>( + pub async fn verify<'x>( resolver: &Resolver, signature: Signature, message_: &'x str,