1use super::{DkimSigner, Done, Signature, canonicalize::CanonicalHeaders};
8use crate::{
9 Error,
10 common::{
11 crypto::SigningKey,
12 headers::{ChainedHeaderIterator, HeaderIterator, HeaderStream, Writable, Writer},
13 },
14};
15use mail_builder::encoders::base64::base64_encode;
16use std::time::SystemTime;
17
18impl<T: SigningKey> DkimSigner<T, Done> {
19 #[inline(always)]
21 pub fn sign(&self, message: &[u8]) -> crate::Result<Signature> {
22 self.sign_stream(
23 HeaderIterator::new(message),
24 SystemTime::now()
25 .duration_since(SystemTime::UNIX_EPOCH)
26 .map(|d| d.as_secs())
27 .unwrap_or(0),
28 )
29 }
30
31 #[inline(always)]
32 pub fn sign_chained<'x>(
34 &self,
35 chunks: impl Iterator<Item = &'x [u8]>,
36 ) -> crate::Result<Signature> {
37 self.sign_stream(
38 ChainedHeaderIterator::new(chunks),
39 SystemTime::now()
40 .duration_since(SystemTime::UNIX_EPOCH)
41 .map(|d| d.as_secs())
42 .unwrap_or(0),
43 )
44 }
45
46 fn sign_stream<'x>(
47 &self,
48 message: impl HeaderStream<'x>,
49 now: u64,
50 ) -> crate::Result<Signature> {
51 let (body_len, canonical_headers, signed_headers, canonical_body) =
53 self.template.canonicalize(message);
54
55 if signed_headers.is_empty() {
56 return Err(Error::NoHeadersFound);
57 }
58
59 let mut signature = self.template.clone();
61 let body_hash = self.key.hash(canonical_body);
62 signature.bh = base64_encode(body_hash.as_ref())?;
63 signature.t = now;
64 signature.x = if signature.x > 0 {
65 now + signature.x
66 } else {
67 0
68 };
69 signature.h = signed_headers;
70 if signature.l > 0 {
71 signature.l = body_len as u64;
72 }
73
74 let b = self.key.sign(SignableMessage {
76 headers: canonical_headers,
77 signature: &signature,
78 })?;
79
80 signature.b = base64_encode(&b)?;
82
83 Ok(signature)
84 }
85}
86
87pub(super) struct SignableMessage<'a> {
88 pub(super) headers: CanonicalHeaders<'a>,
89 pub(super) signature: &'a Signature,
90}
91
92impl Writable for SignableMessage<'_> {
93 fn write(self, writer: &mut impl Writer) {
94 self.headers.write(writer);
95 self.signature.write(writer, false);
96 }
97}
98
99#[cfg(test)]
100#[allow(unused)]
101pub mod test {
102 use crate::{
103 AuthenticatedMessage, DkimOutput, DkimResult, MessageAuthenticator,
104 common::{
105 cache::test::DummyCaches,
106 crypto::{Ed25519Key, RsaKey, Sha256},
107 headers::HeaderIterator,
108 parse::TxtRecordParser,
109 verify::DomainKey,
110 },
111 dkim::{Atps, Canonicalization, DkimSigner, DomainKeyReport, HashAlgorithm, Signature},
112 };
113 use core::str;
114 use hickory_resolver::proto::op::ResponseCode;
115 use mail_parser::{MessageParser, decoders::base64::base64_decode};
116 use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, pem::PemObject};
117 use std::time::{Duration, Instant};
118
119 const RSA_PRIVATE_KEY: &str = include_str!("../../resources/rsa-private.pem");
120
121 const RSA_PUBLIC_KEY: &str = concat!(
122 "v=DKIM1; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ",
123 "8AMIIBCgKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm",
124 "1agrbcnSk5oIaEfMZLUR/X8gPzoiNHZcfMZEVR6bAytxUh",
125 "c5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIaUmSACj0dL2/K",
126 "wW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bW",
127 "PZNkT4NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0",
128 "TqgXdtw6A4Sdsp97VXT9fkPW9rso3lrkpsl/9EQ1mR/DWK",
129 "6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Aed4l9",
130 "WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQAB",
131 );
132
133 const ED25519_PRIVATE_KEY: &str = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=";
134 const ED25519_PUBLIC_KEY: &str =
135 "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
136
137 #[test]
138 fn dkim_sign() {
139 let pk = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
140 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
141 ))
142 .unwrap();
143
144 let signature = DkimSigner::from_key(pk)
145 .domain("stalw.art")
146 .selector("default")
147 .headers(["From", "To", "Subject"])
148 .sign_stream(
149 HeaderIterator::new(
150 concat!(
151 "From: hello@stalw.art\r\n",
152 "To: dkim@stalw.art\r\n",
153 "Subject: Testing DKIM!\r\n\r\n",
154 "Here goes the test\r\n\r\n"
155 )
156 .as_bytes(),
157 ),
158 311923920,
159 )
160 .unwrap();
161
162 assert_eq!(
163 concat!(
164 "dkim-signature:v=1; a=rsa-sha256; s=default; d=stalw.art; ",
165 "c=relaxed/relaxed; h=Subject:To:From; t=311923920; ",
166 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Yl m5s=; ",
167 "b=B/p1FPSJ+Jl4A94381+DTZZnNO4c3fVqDnj0M0Vk5JuvnKb5",
168 "dKSwaoIHPO8UUJsroqH z+R0/eWyW1Vlz+uMIZc2j7MVPJcGaY",
169 "Ni85uCQbPd8VpDKWWab6m21ngXYIpagmzKOKYllyOeK3X qwDz",
170 "Bo0T2DdNjGyMUOAWHxrKGU+fbcPHQYxTBCpfOxE/nc/uxxqh+i",
171 "2uXrsxz7PdCEN01LZiYVV yOzcv0ER9A7aDReE2XPVHnFL8jxE",
172 "2BD53HRv3hGkIDcC6wKOKG/lmID+U8tQk5CP0dLmprgjgTv Se",
173 "bu6xNc6SSIgpvwryAAzJEVwmaBqvE8RNk3Vg10lBZEuNsj2Q==;",
174 ),
175 signature.to_string()
176 );
177 }
178
179 #[tokio::test]
180 async fn dkim_sign_verify() {
181 use crate::common::cache::test::DummyCaches;
182
183 let message = concat!(
184 "From: bill@example.com\r\n",
185 "To: jdoe@example.com\r\n",
186 "Subject: TPS Report\r\n",
187 "\r\n",
188 "I'm going to need those TPS reports ASAP. ",
189 "So, if you could do that, that'd be great.\r\n"
190 );
191 let empty_message = concat!(
192 "From: bill@example.com\r\n",
193 "To: jdoe@example.com\r\n",
194 "Subject: Empty TPS Report\r\n",
195 "\r\n",
196 "\r\n"
197 );
198 let message_multiheader = concat!(
199 "X-Duplicate-Header: 4\r\n",
200 "From: bill@example.com\r\n",
201 "X-Duplicate-Header: 3\r\n",
202 "To: jdoe@example.com\r\n",
203 "X-Duplicate-Header: 2\r\n",
204 "Subject: TPS Report\r\n",
205 "X-Duplicate-Header: 1\r\n",
206 "To: jane@example.com\r\n",
207 "\r\n",
208 "I'm going to need those TPS reports ASAP. ",
209 "So, if you could do that, that'd be great.\r\n"
210 );
211
212 let pk_ed = Ed25519Key::from_seed_and_public_key(
214 &base64_decode(ED25519_PRIVATE_KEY.as_bytes()).unwrap(),
215 &base64_decode(ED25519_PUBLIC_KEY.rsplit_once("p=").unwrap().1.as_bytes()).unwrap(),
216 )
217 .unwrap();
218
219 let resolver = MessageAuthenticator::new_system_conf().unwrap();
221 let caches = DummyCaches::new()
222 .with_txt(
223 "default._domainkey.example.com.".to_string(),
224 DomainKey::parse(RSA_PUBLIC_KEY.as_bytes()).unwrap(),
225 Instant::now() + Duration::new(3600, 0),
226 )
227 .with_txt(
228 "ed._domainkey.example.com.".to_string(),
229 DomainKey::parse(ED25519_PUBLIC_KEY.as_bytes()).unwrap(),
230 Instant::now() + Duration::new(3600, 0),
231 )
232 .with_txt(
233 "_report._domainkey.example.com.".to_string(),
234 DomainKeyReport::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(),
235 Instant::now() + Duration::new(3600, 0),
236 );
237
238 dbg!("Test RSA-SHA256 relaxed/relaxed");
239 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
240 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
241 ))
242 .unwrap();
243 verify(
244 &resolver,
245 &caches,
246 DkimSigner::from_key(pk_rsa)
247 .domain("example.com")
248 .selector("default")
249 .headers(["From", "To", "Subject"])
250 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
251 .sign(message.as_bytes())
252 .unwrap(),
253 message,
254 Ok(()),
255 )
256 .await;
257
258 dbg!("Test ED25519-SHA256 relaxed/relaxed");
259 verify(
260 &resolver,
261 &caches,
262 DkimSigner::from_key(pk_ed)
263 .domain("example.com")
264 .selector("ed")
265 .headers(["From", "To", "Subject"])
266 .sign(message.as_bytes())
267 .unwrap(),
268 message,
269 Ok(()),
270 )
271 .await;
272
273 dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message");
274 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
275 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
276 ))
277 .unwrap();
278 verify(
279 &resolver,
280 &caches,
281 DkimSigner::from_key(pk_rsa)
282 .domain("example.com")
283 .selector("default")
284 .headers(["From", "To", "Subject"])
285 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
286 .sign(empty_message.as_bytes())
287 .unwrap(),
288 empty_message,
289 Ok(()),
290 )
291 .await;
292
293 dbg!("Test RSA-SHA256 simple/simple with an empty message");
294 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
295 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
296 ))
297 .unwrap();
298 verify(
299 &resolver,
300 &caches,
301 DkimSigner::from_key(pk_rsa)
302 .domain("example.com")
303 .selector("default")
304 .headers(["From", "To", "Subject"])
305 .header_canonicalization(Canonicalization::Simple)
306 .body_canonicalization(Canonicalization::Simple)
307 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
308 .sign(empty_message.as_bytes())
309 .unwrap(),
310 empty_message,
311 Ok(()),
312 )
313 .await;
314
315 dbg!("Test RSA-SHA256 simple/simple with duplicated headers");
316 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
317 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
318 ))
319 .unwrap();
320 verify(
321 &resolver,
322 &caches,
323 DkimSigner::from_key(pk_rsa)
324 .domain("example.com")
325 .selector("default")
326 .headers([
327 "From",
328 "To",
329 "Subject",
330 "X-Duplicate-Header",
331 "X-Does-Not-Exist",
332 ])
333 .header_canonicalization(Canonicalization::Simple)
334 .body_canonicalization(Canonicalization::Simple)
335 .sign(message_multiheader.as_bytes())
336 .unwrap(),
337 message_multiheader,
338 Ok(()),
339 )
340 .await;
341
342 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (relaxed)");
343 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
344 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
345 ))
346 .unwrap();
347 verify_with_opts(
348 &resolver,
349 &caches,
350 DkimSigner::from_key(pk_rsa)
351 .domain("example.com")
352 .selector("default")
353 .headers(["From", "To", "Subject"])
354 .header_canonicalization(Canonicalization::Simple)
355 .body_length(true)
356 .sign(message.as_bytes())
357 .unwrap(),
358 &(message.to_string() + "\r\n----- Mailing list"),
359 Ok(()),
360 false,
361 )
362 .await;
363
364 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (strict)");
365 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
366 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
367 ))
368 .unwrap();
369 verify_with_opts(
370 &resolver,
371 &caches,
372 DkimSigner::from_key(pk_rsa)
373 .domain("example.com")
374 .selector("default")
375 .headers(["From", "To", "Subject"])
376 .header_canonicalization(Canonicalization::Simple)
377 .body_length(true)
378 .sign(message.as_bytes())
379 .unwrap(),
380 &(message.to_string() + "\r\n----- Mailing list"),
381 Err(super::Error::SignatureLength),
382 true,
383 )
384 .await;
385
386 dbg!("Test AUID not matching domains");
387 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
388 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
389 ))
390 .unwrap();
391 verify(
392 &resolver,
393 &caches,
394 DkimSigner::from_key(pk_rsa)
395 .domain("example.com")
396 .selector("default")
397 .headers(["From", "To", "Subject"])
398 .agent_user_identifier("@wrongdomain.com")
399 .sign(message.as_bytes())
400 .unwrap(),
401 message,
402 Err(super::Error::FailedAuidMatch),
403 )
404 .await;
405
406 dbg!("Test expired signature and reporting");
407 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
408 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
409 ))
410 .unwrap();
411 let r = verify(
412 &resolver,
413 &caches,
414 DkimSigner::from_key(pk_rsa)
415 .domain("example.com")
416 .selector("default")
417 .headers(["From", "To", "Subject"])
418 .expiration(12345)
419 .reporting(true)
420 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
421 .unwrap(),
422 message,
423 Err(super::Error::SignatureExpired),
424 )
425 .await
426 .pop()
427 .unwrap()
428 .report;
429 assert_eq!(r.as_deref(), Some("dkim-failures@example.com"));
430
431 dbg!("Verify ATPS (failure)");
432 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
433 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
434 ))
435 .unwrap();
436 verify(
437 &resolver,
438 &caches,
439 DkimSigner::from_key(pk_rsa)
440 .domain("example.com")
441 .selector("default")
442 .headers(["From", "To", "Subject"])
443 .atps("example.com")
444 .atpsh(HashAlgorithm::Sha256)
445 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
446 .unwrap(),
447 message,
448 Err(super::Error::DnsRecordNotFound(ResponseCode::NXDomain)),
449 )
450 .await;
451
452 dbg!("Verify ATPS (success)");
453 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
454 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
455 ))
456 .unwrap();
457 caches.txt_add(
458 "UN42N5XOV642KXRXRQIYANHCOUPGQL5LT4WTBKYT2IJFLBWODFDQ._atps.example.com.".to_string(),
459 Atps::parse(b"v=ATPS1;").unwrap(),
460 Instant::now() + Duration::new(3600, 0),
461 );
462 verify(
463 &resolver,
464 &caches,
465 DkimSigner::from_key(pk_rsa)
466 .domain("example.com")
467 .selector("default")
468 .headers(["From", "To", "Subject"])
469 .atps("example.com")
470 .atpsh(HashAlgorithm::Sha256)
471 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
472 .unwrap(),
473 message,
474 Ok(()),
475 )
476 .await;
477
478 dbg!("Verify ATPS (success - no hash)");
479 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
480 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
481 ))
482 .unwrap();
483 caches.txt_add(
484 "example.com._atps.example.com.".to_string(),
485 Atps::parse(b"v=ATPS1;").unwrap(),
486 Instant::now() + Duration::new(3600, 0),
487 );
488 verify(
489 &resolver,
490 &caches,
491 DkimSigner::from_key(pk_rsa)
492 .domain("example.com")
493 .selector("default")
494 .headers(["From", "To", "Subject"])
495 .atps("example.com")
496 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
497 .unwrap(),
498 message,
499 Ok(()),
500 )
501 .await;
502 }
503
504 pub(crate) async fn verify_with_opts<'x>(
505 resolver: &MessageAuthenticator,
506 caches: &DummyCaches,
507 signature: Signature,
508 message_: &'x str,
509 expect: Result<(), super::Error>,
510 strict: bool,
511 ) -> Vec<DkimOutput<'x>> {
512 let mut raw_message = Vec::with_capacity(message_.len() + 100);
513 signature.write(&mut raw_message, true);
514 raw_message.extend_from_slice(message_.as_bytes());
515
516 let message = AuthenticatedMessage::parse_with_opts(&raw_message, strict).unwrap();
517 assert_eq!(
518 message,
519 AuthenticatedMessage::from_parsed(
520 &MessageParser::new().parse(&raw_message).unwrap(),
521 strict
522 )
523 );
524 let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
525
526 match (dkim.last().unwrap().result(), &expect) {
527 (DkimResult::Pass, Ok(_)) => (),
528 (
529 DkimResult::Fail(hdr) | DkimResult::PermError(hdr) | DkimResult::Neutral(hdr),
530 Err(err),
531 ) if hdr == err => (),
532 (result, expect) => panic!("Expected {expect:?} but got {result:?}."),
533 }
534
535 dkim.into_iter()
536 .map(|d| DkimOutput {
537 result: d.result,
538 signature: None,
539 report: d.report,
540 is_atps: d.is_atps,
541 })
542 .collect()
543 }
544
545 pub(crate) async fn verify<'x>(
546 resolver: &MessageAuthenticator,
547 caches: &DummyCaches,
548 signature: Signature,
549 message_: &'x str,
550 expect: Result<(), super::Error>,
551 ) -> Vec<DkimOutput<'x>> {
552 verify_with_opts(resolver, caches, signature, message_, expect, true).await
553 }
554}