1use super::{DkimSigner, Done, Signature, canonicalize::CanonicalHeaders};
8use crate::SystemTime;
9use crate::{
10 Error,
11 common::{
12 crypto::SigningKey,
13 headers::{ChainedHeaderIterator, HeaderIterator, HeaderStream, Writable, Writer},
14 },
15};
16use mail_builder::encoders::base64::base64_encode;
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 IntoIterator<Item = &'x [u8]>,
36 ) -> crate::Result<Signature> {
37 self.sign_stream(
38 ChainedHeaderIterator::new(chunks.into_iter()),
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 mail_parser::{MessageParser, decoders::base64::base64_decode};
115 use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, pem::PemObject};
116 use std::time::{Duration, Instant};
117
118 const RSA_PRIVATE_KEY: &str = include_str!("../../resources/rsa-private.pem");
119
120 const RSA_PUBLIC_KEY: &str = concat!(
121 "v=DKIM1; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ",
122 "8AMIIBCgKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm",
123 "1agrbcnSk5oIaEfMZLUR/X8gPzoiNHZcfMZEVR6bAytxUh",
124 "c5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIaUmSACj0dL2/K",
125 "wW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bW",
126 "PZNkT4NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0",
127 "TqgXdtw6A4Sdsp97VXT9fkPW9rso3lrkpsl/9EQ1mR/DWK",
128 "6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Aed4l9",
129 "WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQAB",
130 );
131
132 const ED25519_PRIVATE_KEY: &str = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=";
133 const ED25519_PUBLIC_KEY: &str =
134 "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
135
136 #[test]
137 fn dkim_sign() {
138 let pk = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
139 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
140 ))
141 .unwrap();
142
143 let signature = DkimSigner::from_key(pk)
144 .domain("stalw.art")
145 .selector("default")
146 .headers(["From", "To", "Subject"])
147 .sign_stream(
148 HeaderIterator::new(
149 concat!(
150 "From: hello@stalw.art\r\n",
151 "To: dkim@stalw.art\r\n",
152 "Subject: Testing DKIM!\r\n\r\n",
153 "Here goes the test\r\n\r\n"
154 )
155 .as_bytes(),
156 ),
157 311923920,
158 )
159 .unwrap();
160
161 assert_eq!(
162 concat!(
163 "dkim-signature:v=1; a=rsa-sha256; s=default; d=stalw.art; ",
164 "c=relaxed/relaxed; h=Subject:To:From; t=311923920; ",
165 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Yl m5s=; ",
166 "b=B/p1FPSJ+Jl4A94381+DTZZnNO4c3fVqDnj0M0Vk5JuvnKb5",
167 "dKSwaoIHPO8UUJsroqH z+R0/eWyW1Vlz+uMIZc2j7MVPJcGaY",
168 "Ni85uCQbPd8VpDKWWab6m21ngXYIpagmzKOKYllyOeK3X qwDz",
169 "Bo0T2DdNjGyMUOAWHxrKGU+fbcPHQYxTBCpfOxE/nc/uxxqh+i",
170 "2uXrsxz7PdCEN01LZiYVV yOzcv0ER9A7aDReE2XPVHnFL8jxE",
171 "2BD53HRv3hGkIDcC6wKOKG/lmID+U8tQk5CP0dLmprgjgTv Se",
172 "bu6xNc6SSIgpvwryAAzJEVwmaBqvE8RNk3Vg10lBZEuNsj2Q==;",
173 ),
174 signature.to_string()
175 );
176 }
177
178 #[tokio::test]
179 async fn dkim_sign_verify() {
180 use crate::common::cache::test::DummyCaches;
181
182 let message = concat!(
183 "From: bill@example.com\r\n",
184 "To: jdoe@example.com\r\n",
185 "Subject: TPS Report\r\n",
186 "\r\n",
187 "I'm going to need those TPS reports ASAP. ",
188 "So, if you could do that, that'd be great.\r\n"
189 );
190 let empty_message = concat!(
191 "From: bill@example.com\r\n",
192 "To: jdoe@example.com\r\n",
193 "Subject: Empty TPS Report\r\n",
194 "\r\n",
195 "\r\n"
196 );
197 let message_multiheader = concat!(
198 "X-Duplicate-Header: 4\r\n",
199 "From: bill@example.com\r\n",
200 "X-Duplicate-Header: 3\r\n",
201 "To: jdoe@example.com\r\n",
202 "X-Duplicate-Header: 2\r\n",
203 "Subject: TPS Report\r\n",
204 "X-Duplicate-Header: 1\r\n",
205 "To: jane@example.com\r\n",
206 "\r\n",
207 "I'm going to need those TPS reports ASAP. ",
208 "So, if you could do that, that'd be great.\r\n"
209 );
210
211 let pk_ed = Ed25519Key::from_seed_and_public_key(
213 &base64_decode(ED25519_PRIVATE_KEY.as_bytes()).unwrap(),
214 &base64_decode(ED25519_PUBLIC_KEY.rsplit_once("p=").unwrap().1.as_bytes()).unwrap(),
215 )
216 .unwrap();
217
218 let resolver = MessageAuthenticator::new_system_conf().unwrap();
220 let caches = DummyCaches::new()
221 .with_txt(
222 "default._domainkey.example.com.".to_string(),
223 DomainKey::parse(RSA_PUBLIC_KEY.as_bytes()).unwrap(),
224 Instant::now() + Duration::new(3600, 0),
225 )
226 .with_txt(
227 "ed._domainkey.example.com.".to_string(),
228 DomainKey::parse(ED25519_PUBLIC_KEY.as_bytes()).unwrap(),
229 Instant::now() + Duration::new(3600, 0),
230 )
231 .with_txt(
232 "_report._domainkey.example.com.".to_string(),
233 DomainKeyReport::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(),
234 Instant::now() + Duration::new(3600, 0),
235 );
236
237 dbg!("Test RSA-SHA256 relaxed/relaxed");
238 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
239 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
240 ))
241 .unwrap();
242 verify(
243 &resolver,
244 &caches,
245 DkimSigner::from_key(pk_rsa)
246 .domain("example.com")
247 .selector("default")
248 .headers(["From", "To", "Subject"])
249 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
250 .sign(message.as_bytes())
251 .unwrap(),
252 message,
253 Ok(()),
254 )
255 .await;
256
257 dbg!("Test ED25519-SHA256 relaxed/relaxed");
258 verify(
259 &resolver,
260 &caches,
261 DkimSigner::from_key(pk_ed)
262 .domain("example.com")
263 .selector("ed")
264 .headers(["From", "To", "Subject"])
265 .sign(message.as_bytes())
266 .unwrap(),
267 message,
268 Ok(()),
269 )
270 .await;
271
272 dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message");
273 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
274 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
275 ))
276 .unwrap();
277 verify(
278 &resolver,
279 &caches,
280 DkimSigner::from_key(pk_rsa)
281 .domain("example.com")
282 .selector("default")
283 .headers(["From", "To", "Subject"])
284 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
285 .sign(empty_message.as_bytes())
286 .unwrap(),
287 empty_message,
288 Ok(()),
289 )
290 .await;
291
292 dbg!("Test RSA-SHA256 simple/simple with an empty message");
293 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
294 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
295 ))
296 .unwrap();
297 verify(
298 &resolver,
299 &caches,
300 DkimSigner::from_key(pk_rsa)
301 .domain("example.com")
302 .selector("default")
303 .headers(["From", "To", "Subject"])
304 .header_canonicalization(Canonicalization::Simple)
305 .body_canonicalization(Canonicalization::Simple)
306 .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
307 .sign(empty_message.as_bytes())
308 .unwrap(),
309 empty_message,
310 Ok(()),
311 )
312 .await;
313
314 dbg!("Test RSA-SHA256 simple/simple with duplicated headers");
315 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
316 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
317 ))
318 .unwrap();
319 verify(
320 &resolver,
321 &caches,
322 DkimSigner::from_key(pk_rsa)
323 .domain("example.com")
324 .selector("default")
325 .headers([
326 "From",
327 "To",
328 "Subject",
329 "X-Duplicate-Header",
330 "X-Does-Not-Exist",
331 ])
332 .header_canonicalization(Canonicalization::Simple)
333 .body_canonicalization(Canonicalization::Simple)
334 .sign(message_multiheader.as_bytes())
335 .unwrap(),
336 message_multiheader,
337 Ok(()),
338 )
339 .await;
340
341 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (relaxed)");
342 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
343 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
344 ))
345 .unwrap();
346 verify_with_opts(
347 &resolver,
348 &caches,
349 DkimSigner::from_key(pk_rsa)
350 .domain("example.com")
351 .selector("default")
352 .headers(["From", "To", "Subject"])
353 .header_canonicalization(Canonicalization::Simple)
354 .body_length(true)
355 .sign(message.as_bytes())
356 .unwrap(),
357 &(message.to_string() + "\r\n----- Mailing list"),
358 Ok(()),
359 false,
360 )
361 .await;
362
363 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (strict)");
364 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
365 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
366 ))
367 .unwrap();
368 verify_with_opts(
369 &resolver,
370 &caches,
371 DkimSigner::from_key(pk_rsa)
372 .domain("example.com")
373 .selector("default")
374 .headers(["From", "To", "Subject"])
375 .header_canonicalization(Canonicalization::Simple)
376 .body_length(true)
377 .sign(message.as_bytes())
378 .unwrap(),
379 &(message.to_string() + "\r\n----- Mailing list"),
380 Err(super::Error::Dkim(crate::dkim::DkimError::SignatureLength)),
381 true,
382 )
383 .await;
384
385 let message_whitespace = concat!(
386 "From: bill@example.com\r\n",
387 "To: jdoe@example.com\r\n",
388 "Subject: TPS Report\r\n",
389 "\r\n",
390 "I'm going to need those TPS reports ASAP. \r\n",
391 "So, if you could do that, that'd be great. \r\n"
392 );
393
394 dbg!("Test RSA-SHA256 relaxed/simple (mismatched header/body canonicalization)");
395 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
396 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
397 ))
398 .unwrap();
399 verify(
400 &resolver,
401 &caches,
402 DkimSigner::from_key(pk_rsa)
403 .domain("example.com")
404 .selector("default")
405 .headers(["From", "To", "Subject"])
406 .header_canonicalization(Canonicalization::Relaxed)
407 .body_canonicalization(Canonicalization::Simple)
408 .sign(message_whitespace.as_bytes())
409 .unwrap(),
410 message_whitespace,
411 Ok(()),
412 )
413 .await;
414
415 dbg!("Test RSA-SHA256 simple/relaxed (mismatched header/body canonicalization)");
416 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
417 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
418 ))
419 .unwrap();
420 verify(
421 &resolver,
422 &caches,
423 DkimSigner::from_key(pk_rsa)
424 .domain("example.com")
425 .selector("default")
426 .headers(["From", "To", "Subject"])
427 .header_canonicalization(Canonicalization::Simple)
428 .body_canonicalization(Canonicalization::Relaxed)
429 .sign(message_whitespace.as_bytes())
430 .unwrap(),
431 message_whitespace,
432 Ok(()),
433 )
434 .await;
435
436 dbg!("Test AUID not matching domains");
437 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
438 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
439 ))
440 .unwrap();
441 verify(
442 &resolver,
443 &caches,
444 DkimSigner::from_key(pk_rsa)
445 .domain("example.com")
446 .selector("default")
447 .headers(["From", "To", "Subject"])
448 .agent_user_identifier("@wrongdomain.com")
449 .sign(message.as_bytes())
450 .unwrap(),
451 message,
452 Err(super::Error::Dkim(crate::dkim::DkimError::FailedAuidMatch)),
453 )
454 .await;
455
456 dbg!("Test expired signature and reporting");
457 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
458 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
459 ))
460 .unwrap();
461 let r = verify(
462 &resolver,
463 &caches,
464 DkimSigner::from_key(pk_rsa)
465 .domain("example.com")
466 .selector("default")
467 .headers(["From", "To", "Subject"])
468 .expiration(12345)
469 .reporting(true)
470 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
471 .unwrap(),
472 message,
473 Err(super::Error::Dkim(crate::dkim::DkimError::SignatureExpired)),
474 )
475 .await
476 .pop()
477 .unwrap()
478 .report;
479 assert_eq!(r.as_deref(), Some("dkim-failures@example.com"));
480
481 dbg!("Verify ATPS (failure)");
482 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
483 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
484 ))
485 .unwrap();
486 verify(
487 &resolver,
488 &caches,
489 DkimSigner::from_key(pk_rsa)
490 .domain("example.com")
491 .selector("default")
492 .headers(["From", "To", "Subject"])
493 .atps("example.com")
494 .atpsh(HashAlgorithm::Sha256)
495 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
496 .unwrap(),
497 message,
498 Err(super::Error::Dns(crate::DnsError::RecordNotFound(
499 crate::DNS_RCODE_NXDOMAIN,
500 ))),
501 )
502 .await;
503
504 dbg!("Verify ATPS (success)");
505 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
506 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
507 ))
508 .unwrap();
509 caches.txt_add(
510 "UN42N5XOV642KXRXRQIYANHCOUPGQL5LT4WTBKYT2IJFLBWODFDQ._atps.example.com.".to_string(),
511 Atps::parse(b"v=ATPS1;").unwrap(),
512 Instant::now() + Duration::new(3600, 0),
513 );
514 verify(
515 &resolver,
516 &caches,
517 DkimSigner::from_key(pk_rsa)
518 .domain("example.com")
519 .selector("default")
520 .headers(["From", "To", "Subject"])
521 .atps("example.com")
522 .atpsh(HashAlgorithm::Sha256)
523 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
524 .unwrap(),
525 message,
526 Ok(()),
527 )
528 .await;
529
530 dbg!("Verify ATPS (success - no hash)");
531 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
532 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
533 ))
534 .unwrap();
535 caches.txt_add(
536 "example.com._atps.example.com.".to_string(),
537 Atps::parse(b"v=ATPS1;").unwrap(),
538 Instant::now() + Duration::new(3600, 0),
539 );
540 verify(
541 &resolver,
542 &caches,
543 DkimSigner::from_key(pk_rsa)
544 .domain("example.com")
545 .selector("default")
546 .headers(["From", "To", "Subject"])
547 .atps("example.com")
548 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
549 .unwrap(),
550 message,
551 Ok(()),
552 )
553 .await;
554 }
555
556 pub(crate) async fn verify_with_opts<'x>(
557 resolver: &MessageAuthenticator,
558 caches: &DummyCaches,
559 signature: Signature,
560 message_: &'x str,
561 expect: Result<(), super::Error>,
562 strict: bool,
563 ) -> Vec<DkimOutput<'x>> {
564 let mut raw_message = Vec::with_capacity(message_.len() + 100);
565 signature.write(&mut raw_message, true);
566 raw_message.extend_from_slice(message_.as_bytes());
567
568 let message = AuthenticatedMessage::parse_with_opts(&raw_message, None, strict).unwrap();
569 assert_eq!(
570 message,
571 AuthenticatedMessage::from_parsed(
572 &MessageParser::new().parse(&raw_message).unwrap(),
573 &raw_message,
574 strict
575 )
576 );
577 let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
578
579 match (dkim.last().unwrap().result(), &expect) {
580 (DkimResult::Pass, Ok(_)) => (),
581 (
582 DkimResult::Fail(hdr) | DkimResult::PermError(hdr) | DkimResult::Neutral(hdr),
583 Err(err),
584 ) if hdr == err => (),
585 (result, expect) => panic!("Expected {expect:?} but got {result:?}."),
586 }
587
588 dkim.into_iter()
589 .map(|d| DkimOutput {
590 result: d.result,
591 signature: None,
592 report: d.report,
593 is_atps: d.is_atps,
594 })
595 .collect()
596 }
597
598 pub(crate) async fn verify<'x>(
599 resolver: &MessageAuthenticator,
600 caches: &DummyCaches,
601 signature: Signature,
602 message_: &'x str,
603 expect: Result<(), super::Error>,
604 ) -> Vec<DkimOutput<'x>> {
605 verify_with_opts(resolver, caches, signature, message_, expect, true).await
606 }
607}