Skip to content

Commit

Permalink
v0.3.11
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Apr 3, 2024
1 parent 3378f96 commit 9e5d055
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <hello@stalw.art>"]
license = "Apache-2.0 OR MIT"
Expand All @@ -18,6 +18,7 @@ doctest = false
[features]
default = ["ring", "rustls-pemfile"]
rust-crypto = ["ed25519-dalek", "rsa", "sha1", "sha2"]
generate = ["rsa", "rand"]
test = []

[dependencies]
Expand All @@ -30,14 +31,15 @@ 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"
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"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 18 additions & 1 deletion src/common/crypto/ring_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -68,6 +68,11 @@ impl<T: HashImpl> RsaKey<T> {
padding: PhantomData,
})
}

/// Returns the public key of the RSA key pair.
pub fn public_key(&self) -> Vec<u8> {
self.inner.public().as_ref().to_vec()
}
}

impl SigningKey for RsaKey<Sha256> {
Expand All @@ -94,6 +99,13 @@ pub struct Ed25519Key {
}

impl Ed25519Key {
pub fn generate_pkcs8() -> Result<Vec<u8>> {
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<Self> {
Ok(Self {
inner: Ed25519KeyPair::from_pkcs8(pkcs8_der)
Expand All @@ -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<u8> {
self.inner.public_key().as_ref().to_vec()
}
}

impl SigningKey for Ed25519Key {
Expand Down
14 changes: 5 additions & 9 deletions src/common/crypto/rust_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -50,7 +50,7 @@ impl SigningKey for RsaKey<Sha1> {
let hash = self.hash(input);
self.inner
.sign(
PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
Pkcs1v15Sign::new::<<Self::Hasher as HashImpl>::Context>(),
hash.as_ref(),
)
.map_err(|err| Error::CryptoError(err.to_string()))
Expand All @@ -68,7 +68,7 @@ impl SigningKey for RsaKey<Sha256> {
let hash = self.hash(input);
self.inner
.sign(
PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
Pkcs1v15Sign::new::<<Self::Hasher as HashImpl>::Context>(),
hash.as_ref(),
)
.map_err(|err| Error::CryptoError(err.to_string()))
Expand Down Expand Up @@ -141,7 +141,7 @@ impl VerifyingKey for RsaPublicKey {

self.inner
.verify(
PaddingScheme::new_pkcs1v15_sign::<sha2::Sha256>(),
Pkcs1v15Sign::new::<sha2::Sha256>(),
hash.as_ref(),
signature,
)
Expand All @@ -153,11 +153,7 @@ impl VerifyingKey for RsaPublicKey {
let hash = hasher.finalize();

self.inner
.verify(
PaddingScheme::new_pkcs1v15_sign::<sha1::Sha1>(),
hash.as_ref(),
signature,
)
.verify(Pkcs1v15Sign::new::<sha1::Sha1>(), hash.as_ref(), signature)
.map_err(|_| Error::FailedVerification)
}
Algorithm::Ed25519Sha256 => Err(Error::IncompatibleAlgorithms),
Expand Down
11 changes: 11 additions & 0 deletions src/dkim/canonicalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
160 changes: 160 additions & 0 deletions src/dkim/generate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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<u8>,
public_key: Vec<u8>,
}

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<Self> {
//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<Self> {
let pkcs8_der =
Ed25519Key::generate_pkcs8().map_err(|err| Error::CryptoError(err.to_string()))?;

Check failure on line 49 in src/dkim/generate.rs

View workflow job for this annotation

GitHub Actions / clippy

no function or associated item named `generate_pkcs8` found for struct `common::crypto::rust_crypto::Ed25519Key` in the current scope

error[E0599]: no function or associated item named `generate_pkcs8` found for struct `common::crypto::rust_crypto::Ed25519Key` in the current scope --> src/dkim/generate.rs:49:25 | 49 | Ed25519Key::generate_pkcs8().map_err(|err| Error::CryptoError(err.to_string()))?; | ^^^^^^^^^^^^^^ function or associated item not found in `Ed25519Key` | ::: src/common/crypto/rust_crypto.rs:82:1 | 82 | pub struct Ed25519Key { | --------------------- function or associated item `generate_pkcs8` not found for this struct | note: if you're trying to build a new `common::crypto::rust_crypto::Ed25519Key`, consider using `common::crypto::rust_crypto::Ed25519Key::from_bytes` which returns `std::result::Result<common::crypto::rust_crypto::Ed25519Key, Error>` --> src/common/crypto/rust_crypto.rs:88:5 | 88 | pub fn from_bytes(private_key_bytes: &[u8]) -> crate::Result<Self> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
let key = Ed25519Key::from_pkcs8_der(&pkcs8_der).unwrap();

Check failure on line 50 in src/dkim/generate.rs

View workflow job for this annotation

GitHub Actions / clippy

no function or associated item named `from_pkcs8_der` found for struct `common::crypto::rust_crypto::Ed25519Key` in the current scope

error[E0599]: no function or associated item named `from_pkcs8_der` found for struct `common::crypto::rust_crypto::Ed25519Key` in the current scope --> src/dkim/generate.rs:50:31 | 50 | let key = Ed25519Key::from_pkcs8_der(&pkcs8_der).unwrap(); | ^^^^^^^^^^^^^^ function or associated item not found in `Ed25519Key` | ::: src/common/crypto/rust_crypto.rs:82:1 | 82 | pub struct Ed25519Key { | --------------------- function or associated item `from_pkcs8_der` not found for this struct | note: if you're trying to build a new `common::crypto::rust_crypto::Ed25519Key`, consider using `common::crypto::rust_crypto::Ed25519Key::from_bytes` which returns `std::result::Result<common::crypto::rust_crypto::Ed25519Key, Error>` --> src/common/crypto/rust_crypto.rs:88:5 | 88 | pub fn from_bytes(private_key_bytes: &[u8]) -> crate::Result<Self> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `from_pkcs8_der`, perhaps you need to implement it: candidate #1: `rsa::pkcs8::DecodePrivateKey`

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<u8>, Vec<u8>) {
(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::<Sha256>::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\" <jdoe@example.com>")
.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;
}
}
2 changes: 2 additions & 0 deletions src/dkim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/dkim/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -486,7 +486,7 @@ mod test {
.await;
}

async fn verify<'x>(
pub async fn verify<'x>(
resolver: &Resolver,
signature: Signature,
message_: &'x str,
Expand Down

0 comments on commit 9e5d055

Please sign in to comment.