Commit
Author: mdecimus [mauro@stalw.art]
Hash: 9e5d05536e96cacee3dee827854833f5417b33f2
Timestamp: Wed, 03 Apr 2024 15:35:26 +0000 (6 months ago)

+208 -14 +/-9 browse
v0.3.11
1diff --git a/CHANGELOG.md b/CHANGELOG.md
2index 5bfb8aa..5a9e8c0 100644
3--- a/CHANGELOG.md
4+++ b/CHANGELOG.md
5 @@ -1,3 +1,8 @@
6+ mail-auth 0.3.11
7+ ================================
8+ - Added: DKIM keypair generation for both RSA and Ed25519.
9+ - Fix: Check PTR against FQDN (including dot at the end) #28
10+
11 mail-auth 0.3.10
12 ================================
13 - Make `Resolver` cloneable.
14 diff --git a/Cargo.toml b/Cargo.toml
15index c916180..1899fe4 100644
16--- a/Cargo.toml
17+++ b/Cargo.toml
18 @@ -1,7 +1,7 @@
19 [package]
20 name = "mail-auth"
21 description = "DKIM, ARC, SPF and DMARC library for Rust"
22- version = "0.3.10"
23+ version = "0.3.11"
24 edition = "2021"
25 authors = [ "Stalwart Labs <hello@stalw.art>"]
26 license = "Apache-2.0 OR MIT"
27 @@ -18,6 +18,7 @@ doctest = false
28 [features]
29 default = ["ring", "rustls-pemfile"]
30 rust-crypto = ["ed25519-dalek", "rsa", "sha1", "sha2"]
31+ generate = ["rsa", "rand"]
32 test = []
33
34 [dependencies]
35 @@ -30,7 +31,7 @@ mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
36 parking_lot = "0.12.0"
37 quick-xml = "0.31"
38 ring = { version = "0.17", optional = true }
39- rsa = { version = "0.7", optional = true }
40+ rsa = { version = "0.9.6", optional = true }
41 rustls-pemfile = { version = "2", optional = true }
42 serde = { version = "1.0", features = ["derive"] }
43 serde_json = "1.0"
44 @@ -38,6 +39,7 @@ sha1 = { version = "0.10", features = ["oid"], optional = true }
45 sha2 = { version = "0.10.6", features = ["oid"], optional = true }
46 hickory-resolver = { version = "0.24", features = ["dns-over-rustls", "dnssec-ring"] }
47 zip = "0.6.3"
48+ rand = { version = "0.8.5", optional = true }
49
50 [dev-dependencies]
51 tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] }
52 diff --git a/README.md b/README.md
53index 59cf45f..28087f5 100644
54--- a/README.md
55+++ b/README.md
56 @@ -14,6 +14,7 @@ Features:
57 - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 signing and verification.
58 - DKIM Authorized Third-Party Signatures.
59 - DKIM failure reporting using the Abuse Reporting Format.
60+ - Key-pair generation for both RSA and Ed25519 (enabled by the `generate` feature).
61 - **Authenticated Received Chain (ARC)**:
62 - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification.
63 - ARC sealing.
64 diff --git a/src/common/crypto/ring_impls.rs b/src/common/crypto/ring_impls.rs
65index 02c40e9..307ed92 100644
66--- a/src/common/crypto/ring_impls.rs
67+++ b/src/common/crypto/ring_impls.rs
68 @@ -3,7 +3,7 @@ use std::marker::PhantomData;
69 use ring::digest::{Context, SHA1_FOR_LEGACY_USE_ONLY, SHA256};
70 use ring::rand::SystemRandom;
71 use ring::signature::{
72- Ed25519KeyPair, RsaKeyPair, UnparsedPublicKey, ED25519,
73+ Ed25519KeyPair, KeyPair, RsaKeyPair, UnparsedPublicKey, ED25519,
74 RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY,
75 RSA_PKCS1_SHA256,
76 };
77 @@ -68,6 +68,11 @@ impl<T: HashImpl> RsaKey<T> {
78 padding: PhantomData,
79 })
80 }
81+
82+ /// Returns the public key of the RSA key pair.
83+ pub fn public_key(&self) -> Vec<u8> {
84+ self.inner.public().as_ref().to_vec()
85+ }
86 }
87
88 impl SigningKey for RsaKey<Sha256> {
89 @@ -94,6 +99,13 @@ pub struct Ed25519Key {
90 }
91
92 impl Ed25519Key {
93+ pub fn generate_pkcs8() -> Result<Vec<u8>> {
94+ Ok(Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())
95+ .map_err(|err| Error::CryptoError(err.to_string()))?
96+ .as_ref()
97+ .to_vec())
98+ }
99+
100 pub fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self> {
101 Ok(Self {
102 inner: Ed25519KeyPair::from_pkcs8(pkcs8_der)
103 @@ -114,6 +126,11 @@ impl Ed25519Key {
104 .map_err(|err| Error::CryptoError(err.to_string()))?,
105 })
106 }
107+
108+ // Returns the public key of the Ed25519 key pair.
109+ pub fn public_key(&self) -> Vec<u8> {
110+ self.inner.public_key().as_ref().to_vec()
111+ }
112 }
113
114 impl SigningKey for Ed25519Key {
115 diff --git a/src/common/crypto/rust_crypto.rs b/src/common/crypto/rust_crypto.rs
116index 2ec8966..39e5632 100644
117--- a/src/common/crypto/rust_crypto.rs
118+++ b/src/common/crypto/rust_crypto.rs
119 @@ -2,7 +2,7 @@ use std::array::TryFromSliceError;
120 use std::marker::PhantomData;
121
122 use ed25519_dalek::Signer;
123- use rsa::{pkcs1::DecodeRsaPrivateKey, PaddingScheme, PublicKey as _, RsaPrivateKey};
124+ use rsa::{pkcs1::DecodeRsaPrivateKey, Pkcs1v15Sign, RsaPrivateKey};
125 use sha2::digest::Digest;
126
127 use crate::{
128 @@ -50,7 +50,7 @@ impl SigningKey for RsaKey<Sha1> {
129 let hash = self.hash(input);
130 self.inner
131 .sign(
132- PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
133+ Pkcs1v15Sign::new::<<Self::Hasher as HashImpl>::Context>(),
134 hash.as_ref(),
135 )
136 .map_err(|err| Error::CryptoError(err.to_string()))
137 @@ -68,7 +68,7 @@ impl SigningKey for RsaKey<Sha256> {
138 let hash = self.hash(input);
139 self.inner
140 .sign(
141- PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
142+ Pkcs1v15Sign::new::<<Self::Hasher as HashImpl>::Context>(),
143 hash.as_ref(),
144 )
145 .map_err(|err| Error::CryptoError(err.to_string()))
146 @@ -141,7 +141,7 @@ impl VerifyingKey for RsaPublicKey {
147
148 self.inner
149 .verify(
150- PaddingScheme::new_pkcs1v15_sign::<sha2::Sha256>(),
151+ Pkcs1v15Sign::new::<sha2::Sha256>(),
152 hash.as_ref(),
153 signature,
154 )
155 @@ -153,11 +153,7 @@ impl VerifyingKey for RsaPublicKey {
156 let hash = hasher.finalize();
157
158 self.inner
159- .verify(
160- PaddingScheme::new_pkcs1v15_sign::<sha1::Sha1>(),
161- hash.as_ref(),
162- signature,
163- )
164+ .verify(Pkcs1v15Sign::new::<sha1::Sha1>(), hash.as_ref(), signature)
165 .map_err(|_| Error::FailedVerification)
166 }
167 Algorithm::Ed25519Sha256 => Err(Error::IncompatibleAlgorithms),
168 diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs
169index e35ec3e..d27ac40 100644
170--- a/src/dkim/canonicalize.rs
171+++ b/src/dkim/canonicalize.rs
172 @@ -323,6 +323,17 @@ mod test {
173 }
174 .write(&mut hasher);
175
176+ #[cfg(feature = "sha1")]
177+ {
178+ use sha1::Digest;
179+ assert_eq!(
180+ String::from_utf8(base64_encode(hasher.finalize().as_ref()).unwrap())
181+ .unwrap(),
182+ hash,
183+ );
184+ }
185+
186+ #[cfg(all(feature = "ring", not(feature = "sha1")))]
187 assert_eq!(
188 String::from_utf8(base64_encode(hasher.finish().as_ref()).unwrap()).unwrap(),
189 hash,
190 diff --git a/src/dkim/generate.rs b/src/dkim/generate.rs
191new file mode 100644
192index 0000000..7e76a54
193--- /dev/null
194+++ b/src/dkim/generate.rs
195 @@ -0,0 +1,160 @@
196+ /*
197+ * Copyright (c) 2020-2023, Stalwart Labs Ltd.
198+ *
199+ * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
200+ * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
201+ * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
202+ * option. This file may not be copied, modified, or distributed
203+ * except according to those terms.
204+ */
205+
206+ use mail_builder::encoders::base64::base64_encode;
207+ use rsa::{
208+ pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey},
209+ RsaPrivateKey, RsaPublicKey,
210+ };
211+
212+ use crate::{common::crypto::Ed25519Key, Error};
213+
214+ pub struct DkimKeyPair {
215+ private_key: Vec<u8>,
216+ public_key: Vec<u8>,
217+ }
218+
219+ impl DkimKeyPair {
220+ /// Generates a new RSA key pair encoded in PKCS#1 DER format with the given number of bits
221+ pub fn generate_rsa(bits: usize) -> crate::Result<Self> {
222+ //TODO: Use `ring` once it supports RSA key generation
223+ let priv_key = RsaPrivateKey::new(&mut rand::thread_rng(), bits)
224+ .map_err(|err| Error::CryptoError(err.to_string()))?;
225+ let pub_key = RsaPublicKey::from(&priv_key);
226+
227+ Ok(DkimKeyPair {
228+ private_key: priv_key
229+ .to_pkcs1_der()
230+ .map_err(|err| Error::CryptoError(err.to_string()))?
231+ .as_bytes()
232+ .to_vec(),
233+ public_key: pub_key
234+ .to_pkcs1_der()
235+ .map_err(|err| Error::CryptoError(err.to_string()))?
236+ .as_bytes()
237+ .to_vec(),
238+ })
239+ }
240+
241+ /// Generates a new Ed25519 key pair encoded in PKCS#8 DER format
242+ pub fn generate_ed25519() -> crate::Result<Self> {
243+ let pkcs8_der =
244+ Ed25519Key::generate_pkcs8().map_err(|err| Error::CryptoError(err.to_string()))?;
245+ let key = Ed25519Key::from_pkcs8_der(&pkcs8_der).unwrap();
246+
247+ Ok(DkimKeyPair {
248+ private_key: pkcs8_der,
249+ public_key: key.public_key(),
250+ })
251+ }
252+
253+ pub fn public_key(&self) -> &[u8] {
254+ &self.public_key
255+ }
256+
257+ pub fn private_key(&self) -> &[u8] {
258+ &self.private_key
259+ }
260+
261+ pub fn into_inner(self) -> (Vec<u8>, Vec<u8>) {
262+ (self.private_key, self.public_key)
263+ }
264+
265+ pub fn encoded_public_key(&self) -> String {
266+ String::from_utf8(base64_encode(&self.public_key).unwrap_or_default()).unwrap_or_default()
267+ }
268+ }
269+
270+ #[cfg(test)]
271+ mod test {
272+ use crate::dkim::sign::test::verify;
273+ use std::time::{Duration, Instant};
274+
275+ use crate::{
276+ common::{
277+ crypto::{Ed25519Key, RsaKey, Sha256},
278+ parse::TxtRecordParser,
279+ verify::DomainKey,
280+ },
281+ dkim::{generate::DkimKeyPair, DkimSigner, DomainKeyReport},
282+ Resolver,
283+ };
284+
285+ #[tokio::test]
286+ async fn dkim_generate_verify() {
287+ let rsa_pkcs = DkimKeyPair::generate_rsa(2048).unwrap();
288+ let ed_pkcs = DkimKeyPair::generate_ed25519().unwrap();
289+
290+ let rsa_public = format!("v=DKIM1; t=s; p={}", rsa_pkcs.encoded_public_key());
291+ let ed_public = format!("v=DKIM1; k=ed25519; p={}", ed_pkcs.encoded_public_key());
292+
293+ let pk_ed = Ed25519Key::from_pkcs8_der(&ed_pkcs.private_key).unwrap();
294+ let pk_rsa = RsaKey::<Sha256>::from_der(&rsa_pkcs.private_key).unwrap();
295+
296+ // Create resolver
297+ let resolver = Resolver::new_system_conf().unwrap();
298+ #[cfg(any(test, feature = "test"))]
299+ {
300+ resolver.txt_add(
301+ "default._domainkey.example.com.".to_string(),
302+ DomainKey::parse(rsa_public.as_bytes()).unwrap(),
303+ Instant::now() + Duration::new(3600, 0),
304+ );
305+ resolver.txt_add(
306+ "ed._domainkey.example.com.".to_string(),
307+ DomainKey::parse(ed_public.as_bytes()).unwrap(),
308+ Instant::now() + Duration::new(3600, 0),
309+ );
310+ resolver.txt_add(
311+ "_report._domainkey.example.com.".to_string(),
312+ DomainKeyReport::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(),
313+ Instant::now() + Duration::new(3600, 0),
314+ );
315+ }
316+
317+ let message = concat!(
318+ "From: bill@example.com\r\n",
319+ "To: jdoe@example.com\r\n",
320+ "Subject: TPS Report\r\n",
321+ "\r\n",
322+ "I'm going to need those TPS reports ASAP. ",
323+ "So, if you could do that, that'd be great.\r\n"
324+ );
325+
326+ dbg!("Test generated RSA key");
327+ verify(
328+ &resolver,
329+ DkimSigner::from_key(pk_rsa)
330+ .domain("example.com")
331+ .selector("default")
332+ .headers(["From", "To", "Subject"])
333+ .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
334+ .sign(message.as_bytes())
335+ .unwrap(),
336+ message,
337+ Ok(()),
338+ )
339+ .await;
340+
341+ dbg!("Test ED25519 generated key");
342+ verify(
343+ &resolver,
344+ DkimSigner::from_key(pk_ed)
345+ .domain("example.com")
346+ .selector("ed")
347+ .headers(["From", "To", "Subject"])
348+ .sign(message.as_bytes())
349+ .unwrap(),
350+ message,
351+ Ok(()),
352+ )
353+ .await;
354+ }
355+ }
356 diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs
357index 3994232..ee4b110 100644
358--- a/src/dkim/mod.rs
359+++ b/src/dkim/mod.rs
360 @@ -19,6 +19,8 @@ use crate::{
361
362 pub mod builder;
363 pub mod canonicalize;
364+ #[cfg(feature = "generate")]
365+ pub mod generate;
366 pub mod headers;
367 pub mod parse;
368 pub mod sign;
369 diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs
370index 99fdf8d..7e1c9ec 100644
371--- a/src/dkim/sign.rs
372+++ b/src/dkim/sign.rs
373 @@ -105,7 +105,7 @@ impl<'a> Writable for SignableMessage<'a> {
374
375 #[cfg(test)]
376 #[allow(unused)]
377- mod test {
378+ pub mod test {
379 use std::time::{Duration, Instant};
380
381 use hickory_resolver::proto::op::ResponseCode;
382 @@ -486,7 +486,7 @@ mod test {
383 .await;
384 }
385
386- async fn verify<'x>(
387+ pub async fn verify<'x>(
388 resolver: &Resolver,
389 signature: Signature,
390 message_: &'x str,