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 let message_whitespace = concat!(
387 "From: bill@example.com\r\n",
388 "To: jdoe@example.com\r\n",
389 "Subject: TPS Report\r\n",
390 "\r\n",
391 "I'm going to need those TPS reports ASAP. \r\n",
392 "So, if you could do that, that'd be great. \r\n"
393 );
394
395 dbg!("Test RSA-SHA256 relaxed/simple (mismatched header/body canonicalization)");
396 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
397 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
398 ))
399 .unwrap();
400 verify(
401 &resolver,
402 &caches,
403 DkimSigner::from_key(pk_rsa)
404 .domain("example.com")
405 .selector("default")
406 .headers(["From", "To", "Subject"])
407 .header_canonicalization(Canonicalization::Relaxed)
408 .body_canonicalization(Canonicalization::Simple)
409 .sign(message_whitespace.as_bytes())
410 .unwrap(),
411 message_whitespace,
412 Ok(()),
413 )
414 .await;
415
416 dbg!("Test RSA-SHA256 simple/relaxed (mismatched header/body canonicalization)");
417 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
418 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
419 ))
420 .unwrap();
421 verify(
422 &resolver,
423 &caches,
424 DkimSigner::from_key(pk_rsa)
425 .domain("example.com")
426 .selector("default")
427 .headers(["From", "To", "Subject"])
428 .header_canonicalization(Canonicalization::Simple)
429 .body_canonicalization(Canonicalization::Relaxed)
430 .sign(message_whitespace.as_bytes())
431 .unwrap(),
432 message_whitespace,
433 Ok(()),
434 )
435 .await;
436
437 dbg!("Test AUID not matching domains");
438 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
439 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
440 ))
441 .unwrap();
442 verify(
443 &resolver,
444 &caches,
445 DkimSigner::from_key(pk_rsa)
446 .domain("example.com")
447 .selector("default")
448 .headers(["From", "To", "Subject"])
449 .agent_user_identifier("@wrongdomain.com")
450 .sign(message.as_bytes())
451 .unwrap(),
452 message,
453 Err(super::Error::FailedAuidMatch),
454 )
455 .await;
456
457 dbg!("Test expired signature and reporting");
458 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
459 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
460 ))
461 .unwrap();
462 let r = verify(
463 &resolver,
464 &caches,
465 DkimSigner::from_key(pk_rsa)
466 .domain("example.com")
467 .selector("default")
468 .headers(["From", "To", "Subject"])
469 .expiration(12345)
470 .reporting(true)
471 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
472 .unwrap(),
473 message,
474 Err(super::Error::SignatureExpired),
475 )
476 .await
477 .pop()
478 .unwrap()
479 .report;
480 assert_eq!(r.as_deref(), Some("dkim-failures@example.com"));
481
482 dbg!("Verify ATPS (failure)");
483 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
484 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
485 ))
486 .unwrap();
487 verify(
488 &resolver,
489 &caches,
490 DkimSigner::from_key(pk_rsa)
491 .domain("example.com")
492 .selector("default")
493 .headers(["From", "To", "Subject"])
494 .atps("example.com")
495 .atpsh(HashAlgorithm::Sha256)
496 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
497 .unwrap(),
498 message,
499 Err(super::Error::DnsRecordNotFound(ResponseCode::NXDomain)),
500 )
501 .await;
502
503 dbg!("Verify ATPS (success)");
504 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
505 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
506 ))
507 .unwrap();
508 caches.txt_add(
509 "UN42N5XOV642KXRXRQIYANHCOUPGQL5LT4WTBKYT2IJFLBWODFDQ._atps.example.com.".to_string(),
510 Atps::parse(b"v=ATPS1;").unwrap(),
511 Instant::now() + Duration::new(3600, 0),
512 );
513 verify(
514 &resolver,
515 &caches,
516 DkimSigner::from_key(pk_rsa)
517 .domain("example.com")
518 .selector("default")
519 .headers(["From", "To", "Subject"])
520 .atps("example.com")
521 .atpsh(HashAlgorithm::Sha256)
522 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
523 .unwrap(),
524 message,
525 Ok(()),
526 )
527 .await;
528
529 dbg!("Verify ATPS (success - no hash)");
530 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
531 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
532 ))
533 .unwrap();
534 caches.txt_add(
535 "example.com._atps.example.com.".to_string(),
536 Atps::parse(b"v=ATPS1;").unwrap(),
537 Instant::now() + Duration::new(3600, 0),
538 );
539 verify(
540 &resolver,
541 &caches,
542 DkimSigner::from_key(pk_rsa)
543 .domain("example.com")
544 .selector("default")
545 .headers(["From", "To", "Subject"])
546 .atps("example.com")
547 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
548 .unwrap(),
549 message,
550 Ok(()),
551 )
552 .await;
553 }
554
555 pub(crate) async fn verify_with_opts<'x>(
556 resolver: &MessageAuthenticator,
557 caches: &DummyCaches,
558 signature: Signature,
559 message_: &'x str,
560 expect: Result<(), super::Error>,
561 strict: bool,
562 ) -> Vec<DkimOutput<'x>> {
563 let mut raw_message = Vec::with_capacity(message_.len() + 100);
564 signature.write(&mut raw_message, true);
565 raw_message.extend_from_slice(message_.as_bytes());
566
567 let message = AuthenticatedMessage::parse_with_opts(&raw_message, strict).unwrap();
568 assert_eq!(
569 message,
570 AuthenticatedMessage::from_parsed(
571 &MessageParser::new().parse(&raw_message).unwrap(),
572 strict
573 )
574 );
575 let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
576
577 match (dkim.last().unwrap().result(), &expect) {
578 (DkimResult::Pass, Ok(_)) => (),
579 (
580 DkimResult::Fail(hdr) | DkimResult::PermError(hdr) | DkimResult::Neutral(hdr),
581 Err(err),
582 ) if hdr == err => (),
583 (result, expect) => panic!("Expected {expect:?} but got {result:?}."),
584 }
585
586 dkim.into_iter()
587 .map(|d| DkimOutput {
588 result: d.result,
589 signature: None,
590 report: d.report,
591 is_atps: d.is_atps,
592 })
593 .collect()
594 }
595
596 pub(crate) async fn verify<'x>(
597 resolver: &MessageAuthenticator,
598 caches: &DummyCaches,
599 signature: Signature,
600 message_: &'x str,
601 expect: Result<(), super::Error>,
602 ) -> Vec<DkimOutput<'x>> {
603 verify_with_opts(resolver, caches, signature, message_, expect, true).await
604 }
605}