Commit
+208 -14 +/-9 browse
1 | diff --git a/CHANGELOG.md b/CHANGELOG.md |
2 | index 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 |
15 | index 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 |
53 | index 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 |
65 | index 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 |
116 | index 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 |
169 | index 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 |
191 | new file mode 100644 |
192 | index 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 |
357 | index 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 |
370 | index 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, |