1use super::{DkimSigner, Signature, canonicalize::CanonicalHeaders};
11use crate::SystemTime;
12use crate::signer::Ready;
13use crate::{
14 Error,
15 crypto::SigningKey,
16 headers::{ChainedHeaderIterator, HeaderIterator, HeaderStream, Writable, Writer},
17};
18
19impl<T: SigningKey> DkimSigner<T, Ready> {
20 #[inline(always)]
54 pub fn sign(&self, message: &[u8]) -> crate::Result<Signature> {
55 self.sign_stream(
56 HeaderIterator::new(message),
57 SystemTime::now()
58 .duration_since(SystemTime::UNIX_EPOCH)
59 .map(|d| d.as_secs())
60 .unwrap_or(0),
61 )
62 }
63
64 #[inline(always)]
65 pub fn sign_chained<'x>(
78 &self,
79 chunks: impl IntoIterator<Item = &'x [u8]>,
80 ) -> crate::Result<Signature> {
81 self.sign_stream(
82 ChainedHeaderIterator::new(chunks.into_iter()),
83 SystemTime::now()
84 .duration_since(SystemTime::UNIX_EPOCH)
85 .map(|d| d.as_secs())
86 .unwrap_or(0),
87 )
88 }
89
90 pub(crate) fn sign_stream<'x>(
91 &self,
92 message: impl HeaderStream<'x>,
93 now: u64,
94 ) -> crate::Result<Signature> {
95 if !self.template.a.is_dkim1()
96 || matches!(
97 self.template.atpsh,
98 Some(crate::crypto::HashAlgorithm::Sha512)
99 )
100 {
101 return Err(Error::Dkim(super::DkimError::UnsupportedAlgorithm));
102 }
103
104 let (body_len, canonical_headers, signed_headers, canonical_body) =
105 self.template.canonicalize(message);
106
107 if signed_headers.is_empty() {
108 return Err(Error::NoHeadersFound);
109 }
110
111 let mut signature = self.template.clone();
112 signature.bh = self.key.hash(canonical_body).as_ref().to_vec();
113 signature.t = now;
114 signature.x = if signature.x > 0 {
115 now.saturating_add(signature.x)
116 } else {
117 0
118 };
119 signature.h = signed_headers;
120 if signature.l > 0 {
121 signature.l = body_len as u64;
122 }
123
124 signature.b = self.key.sign(SignableMessage {
125 headers: canonical_headers,
126 signature: &signature,
127 })?;
128
129 Ok(signature)
130 }
131}
132
133pub(super) struct SignableMessage<'a> {
134 pub(super) headers: CanonicalHeaders<'a>,
135 pub(super) signature: &'a Signature,
136}
137
138impl Writable for SignableMessage<'_> {
139 fn write(self, writer: &mut impl Writer) {
140 self.headers.write(writer);
141 self.signature.write(writer, false);
142 }
143}
144
145#[cfg(test)]
146#[allow(unused)]
147pub(crate) mod test {
148 use crate::{
149 AuthenticatedMessage, DkimOutput, DkimResult, MessageAuthenticator,
150 crypto::{Ed25519Key, RsaKey, Sha256},
151 dkim::DomainKey,
152 dkim::{
153 AtpsRecord, Canonicalization, DkimReportRecord, DkimSigner, HashAlgorithm, Signature,
154 },
155 dns::cache::test::DummyCaches,
156 headers::HeaderIterator,
157 parse::TxtRecordParser,
158 };
159 use core::str;
160 use encodify::base64;
161 use mail_parser::MessageParser;
162 use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, pem::PemObject};
163 use std::time::{Duration, Instant};
164
165 const RSA_PRIVATE_KEY: &str = include_str!("../../resources/rsa-private.pem");
166
167 const RSA_PUBLIC_KEY: &str = concat!(
168 "v=DKIM1; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ",
169 "8AMIIBCgKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm",
170 "1agrbcnSk5oIaEfMZLUR/X8gPzoiNHZcfMZEVR6bAytxUh",
171 "c5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIaUmSACj0dL2/K",
172 "wW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bW",
173 "PZNkT4NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0",
174 "TqgXdtw6A4Sdsp97VXT9fkPW9rso3lrkpsl/9EQ1mR/DWK",
175 "6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Aed4l9",
176 "WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQAB",
177 );
178
179 const ED25519_PRIVATE_KEY: &str = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=";
180 const ED25519_PUBLIC_KEY: &str =
181 "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
182
183 #[test]
184 fn dkim_sign() {
185 let pk = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
186 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
187 ))
188 .unwrap();
189
190 let signature = DkimSigner::from_key(pk)
191 .domain("stalw.art")
192 .selector("default")
193 .headers(["From", "To", "Subject"])
194 .sign_stream(
195 HeaderIterator::new(
196 concat!(
197 "From: hello@stalw.art\r\n",
198 "To: dkim@stalw.art\r\n",
199 "Subject: Testing DKIM!\r\n\r\n",
200 "Here goes the test\r\n\r\n"
201 )
202 .as_bytes(),
203 ),
204 311923920,
205 )
206 .unwrap();
207
208 assert_eq!(
209 concat!(
210 "dkim-signature:v=1; a=rsa-sha256; s=default; d=stalw.art; ",
211 "c=relaxed/relaxed; h=Subject:To:From; t=311923920; ",
212 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Yl m5s=; ",
213 "b=B/p1FPSJ+Jl4A94381+DTZZnNO4c3fVqDnj0M0Vk5JuvnKb5",
214 "dKSwaoIHPO8UUJsroqH z+R0/eWyW1Vlz+uMIZc2j7MVPJcGaY",
215 "Ni85uCQbPd8VpDKWWab6m21ngXYIpagmzKOKYllyOeK3X qwDz",
216 "Bo0T2DdNjGyMUOAWHxrKGU+fbcPHQYxTBCpfOxE/nc/uxxqh+i",
217 "2uXrsxz7PdCEN01LZiYVV yOzcv0ER9A7aDReE2XPVHnFL8jxE",
218 "2BD53HRv3hGkIDcC6wKOKG/lmID+U8tQk5CP0dLmprgjgTv Se",
219 "bu6xNc6SSIgpvwryAAzJEVwmaBqvE8RNk3Vg10lBZEuNsj2Q==;",
220 ),
221 signature.to_string()
222 );
223 }
224
225 #[test]
226 fn dkim_expiration_saturates_and_rounds_up() {
227 let message = "From: hello@stalw.art\r\n\r\nbody\r\n".as_bytes();
228 for (expiration, expected_x) in [
229 (Duration::MAX, u64::MAX),
230 (Duration::from_millis(500), 311923921),
231 (Duration::from_secs(60), 311923980),
232 (Duration::ZERO, 0),
233 ] {
234 let pk = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
235 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
236 ))
237 .unwrap();
238 let signature = DkimSigner::from_key(pk)
239 .domain("stalw.art")
240 .selector("default")
241 .headers(["From"])
242 .expiration(expiration)
243 .sign_stream(HeaderIterator::new(message), 311923920)
244 .unwrap();
245 assert_eq!(signature.x, expected_x, "{expiration:?}");
246 }
247 }
248
249 #[tokio::test]
250 async fn dkim_sign_verify() {
251 use crate::dns::cache::test::DummyCaches;
252
253 let message = concat!(
254 "From: bill@example.com\r\n",
255 "To: jdoe@example.com\r\n",
256 "Subject: TPS Report\r\n",
257 "\r\n",
258 "I'm going to need those TPS reports ASAP. ",
259 "So, if you could do that, that'd be great.\r\n"
260 );
261 let empty_message = concat!(
262 "From: bill@example.com\r\n",
263 "To: jdoe@example.com\r\n",
264 "Subject: Empty TPS Report\r\n",
265 "\r\n",
266 "\r\n"
267 );
268 let message_multiheader = concat!(
269 "X-Duplicate-Header: 4\r\n",
270 "From: bill@example.com\r\n",
271 "X-Duplicate-Header: 3\r\n",
272 "To: jdoe@example.com\r\n",
273 "X-Duplicate-Header: 2\r\n",
274 "Subject: TPS Report\r\n",
275 "X-Duplicate-Header: 1\r\n",
276 "To: jane@example.com\r\n",
277 "\r\n",
278 "I'm going to need those TPS reports ASAP. ",
279 "So, if you could do that, that'd be great.\r\n"
280 );
281
282 let pk_ed = Ed25519Key::from_seed_and_public_key(
283 &base64::LENIENT.decode(ED25519_PRIVATE_KEY).unwrap(),
284 &base64::LENIENT
285 .decode(ED25519_PUBLIC_KEY.rsplit_once("p=").unwrap().1)
286 .unwrap(),
287 )
288 .unwrap();
289
290 let resolver = MessageAuthenticator::new_system_conf().unwrap();
291 let caches = DummyCaches::new()
292 .with_txt(
293 "default._domainkey.example.com.".to_string(),
294 DomainKey::parse(RSA_PUBLIC_KEY.as_bytes()).unwrap(),
295 Instant::now() + Duration::new(3600, 0),
296 )
297 .with_txt(
298 "ed._domainkey.example.com.".to_string(),
299 DomainKey::parse(ED25519_PUBLIC_KEY.as_bytes()).unwrap(),
300 Instant::now() + Duration::new(3600, 0),
301 )
302 .with_txt(
303 "_report._domainkey.example.com.".to_string(),
304 DkimReportRecord::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(),
305 Instant::now() + Duration::new(3600, 0),
306 );
307
308 dbg!("Test RSA-SHA256 relaxed/relaxed");
309 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
310 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
311 ))
312 .unwrap();
313 verify(
314 &resolver,
315 &caches,
316 DkimSigner::from_key(pk_rsa)
317 .domain("example.com")
318 .selector("default")
319 .headers(["From", "To", "Subject"])
320 .identity("\"John Doe\"@example.com")
321 .sign(message.as_bytes())
322 .unwrap(),
323 message,
324 Ok(()),
325 )
326 .await;
327
328 dbg!("Test ED25519-SHA256 relaxed/relaxed");
329 verify(
330 &resolver,
331 &caches,
332 DkimSigner::from_key(pk_ed)
333 .domain("example.com")
334 .selector("ed")
335 .headers(["From", "To", "Subject"])
336 .sign(message.as_bytes())
337 .unwrap(),
338 message,
339 Ok(()),
340 )
341 .await;
342
343 dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message");
344 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
345 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
346 ))
347 .unwrap();
348 verify(
349 &resolver,
350 &caches,
351 DkimSigner::from_key(pk_rsa)
352 .domain("example.com")
353 .selector("default")
354 .headers(["From", "To", "Subject"])
355 .identity("\"John Doe\"@example.com")
356 .sign(empty_message.as_bytes())
357 .unwrap(),
358 empty_message,
359 Ok(()),
360 )
361 .await;
362
363 dbg!("Test RSA-SHA256 simple/simple with an empty message");
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(
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_canonicalization(Canonicalization::Simple)
377 .identity("\"John Doe\"@example.com")
378 .sign(empty_message.as_bytes())
379 .unwrap(),
380 empty_message,
381 Ok(()),
382 )
383 .await;
384
385 dbg!("Test RSA-SHA256 simple/simple with duplicated headers");
386 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
387 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
388 ))
389 .unwrap();
390 verify(
391 &resolver,
392 &caches,
393 DkimSigner::from_key(pk_rsa)
394 .domain("example.com")
395 .selector("default")
396 .headers([
397 "From",
398 "To",
399 "Subject",
400 "X-Duplicate-Header",
401 "X-Does-Not-Exist",
402 ])
403 .header_canonicalization(Canonicalization::Simple)
404 .body_canonicalization(Canonicalization::Simple)
405 .sign(message_multiheader.as_bytes())
406 .unwrap(),
407 message_multiheader,
408 Ok(()),
409 )
410 .await;
411
412 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (relaxed)");
413 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
414 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
415 ))
416 .unwrap();
417 verify_with_opts(
418 &resolver,
419 &caches,
420 DkimSigner::from_key(pk_rsa)
421 .domain("example.com")
422 .selector("default")
423 .headers(["From", "To", "Subject"])
424 .header_canonicalization(Canonicalization::Simple)
425 .body_length(true)
426 .sign(message.as_bytes())
427 .unwrap(),
428 &(message.to_string() + "\r\n----- Mailing list"),
429 Ok(()),
430 false,
431 )
432 .await;
433
434 dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (strict)");
435 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
436 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
437 ))
438 .unwrap();
439 verify_with_opts(
440 &resolver,
441 &caches,
442 DkimSigner::from_key(pk_rsa)
443 .domain("example.com")
444 .selector("default")
445 .headers(["From", "To", "Subject"])
446 .header_canonicalization(Canonicalization::Simple)
447 .body_length(true)
448 .sign(message.as_bytes())
449 .unwrap(),
450 &(message.to_string() + "\r\n----- Mailing list"),
451 Err(super::Error::Dkim(crate::dkim::DkimError::BodyLengthTag)),
452 true,
453 )
454 .await;
455
456 let message_whitespace = concat!(
457 "From: bill@example.com\r\n",
458 "To: jdoe@example.com\r\n",
459 "Subject: TPS Report\r\n",
460 "\r\n",
461 "I'm going to need those TPS reports ASAP. \r\n",
462 "So, if you could do that, that'd be great. \r\n"
463 );
464
465 dbg!("Test RSA-SHA256 relaxed/simple (mismatched header/body canonicalization)");
466 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
467 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
468 ))
469 .unwrap();
470 verify(
471 &resolver,
472 &caches,
473 DkimSigner::from_key(pk_rsa)
474 .domain("example.com")
475 .selector("default")
476 .headers(["From", "To", "Subject"])
477 .header_canonicalization(Canonicalization::Relaxed)
478 .body_canonicalization(Canonicalization::Simple)
479 .sign(message_whitespace.as_bytes())
480 .unwrap(),
481 message_whitespace,
482 Ok(()),
483 )
484 .await;
485
486 dbg!("Test RSA-SHA256 simple/relaxed (mismatched header/body canonicalization)");
487 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
488 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
489 ))
490 .unwrap();
491 verify(
492 &resolver,
493 &caches,
494 DkimSigner::from_key(pk_rsa)
495 .domain("example.com")
496 .selector("default")
497 .headers(["From", "To", "Subject"])
498 .header_canonicalization(Canonicalization::Simple)
499 .body_canonicalization(Canonicalization::Relaxed)
500 .sign(message_whitespace.as_bytes())
501 .unwrap(),
502 message_whitespace,
503 Ok(()),
504 )
505 .await;
506
507 dbg!("Test AUID not matching domains");
508 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
509 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
510 ))
511 .unwrap();
512 verify(
513 &resolver,
514 &caches,
515 DkimSigner::from_key(pk_rsa)
516 .domain("example.com")
517 .selector("default")
518 .headers(["From", "To", "Subject"])
519 .identity("@wrongdomain.com")
520 .sign(message.as_bytes())
521 .unwrap(),
522 message,
523 Err(super::Error::Dkim(crate::dkim::DkimError::AuidMismatch)),
524 )
525 .await;
526
527 dbg!("Test expired signature and reporting");
528 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
529 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
530 ))
531 .unwrap();
532 let r = verify(
533 &resolver,
534 &caches,
535 DkimSigner::from_key(pk_rsa)
536 .domain("example.com")
537 .selector("default")
538 .headers(["From", "To", "Subject"])
539 .expiration(std::time::Duration::from_secs(12345))
540 .reporting(true)
541 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
542 .unwrap(),
543 message,
544 Err(super::Error::Dkim(crate::dkim::DkimError::SignatureExpired)),
545 )
546 .await
547 .pop()
548 .unwrap()
549 .report;
550 assert_eq!(r.as_deref(), Some("dkim-failures@example.com"));
551
552 dbg!("Verify ATPS (failure)");
553 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
554 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
555 ))
556 .unwrap();
557 verify(
558 &resolver,
559 &caches,
560 DkimSigner::from_key(pk_rsa)
561 .domain("example.com")
562 .selector("default")
563 .headers(["From", "To", "Subject"])
564 .atps("example.com")
565 .atps_hash(HashAlgorithm::Sha256)
566 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
567 .unwrap(),
568 message,
569 Err(super::Error::Dns(crate::DnsError::RecordNotFound(
570 crate::dns::DNS_RCODE_NXDOMAIN,
571 ))),
572 )
573 .await;
574
575 dbg!("Verify ATPS (success)");
576 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
577 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
578 ))
579 .unwrap();
580 caches.txt_add(
581 "UN42N5XOV642KXRXRQIYANHCOUPGQL5LT4WTBKYT2IJFLBWODFDQ._atps.example.com.".to_string(),
582 AtpsRecord::parse(b"v=ATPS1;").unwrap(),
583 Instant::now() + Duration::new(3600, 0),
584 );
585 verify(
586 &resolver,
587 &caches,
588 DkimSigner::from_key(pk_rsa)
589 .domain("example.com")
590 .selector("default")
591 .headers(["From", "To", "Subject"])
592 .atps("example.com")
593 .atps_hash(HashAlgorithm::Sha256)
594 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
595 .unwrap(),
596 message,
597 Ok(()),
598 )
599 .await;
600
601 dbg!("Verify ATPS (success - no hash)");
602 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
603 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
604 ))
605 .unwrap();
606 caches.txt_add(
607 "example.com._atps.example.com.".to_string(),
608 AtpsRecord::parse(b"v=ATPS1;").unwrap(),
609 Instant::now() + Duration::new(3600, 0),
610 );
611 verify(
612 &resolver,
613 &caches,
614 DkimSigner::from_key(pk_rsa)
615 .domain("example.com")
616 .selector("default")
617 .headers(["From", "To", "Subject"])
618 .atps("example.com")
619 .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
620 .unwrap(),
621 message,
622 Ok(()),
623 )
624 .await;
625 }
626
627 pub(crate) async fn verify_with_opts<'x>(
628 resolver: &MessageAuthenticator,
629 caches: &DummyCaches,
630 signature: Signature,
631 message_: &'x str,
632 expect: Result<(), super::Error>,
633 strict: bool,
634 ) -> Vec<DkimOutput<'x>> {
635 let mut raw_message = Vec::with_capacity(message_.len() + 100);
636 signature.write(&mut raw_message, true);
637 raw_message.extend_from_slice(message_.as_bytes());
638
639 let message = AuthenticatedMessage::parse_with_opts(&raw_message, None, strict).unwrap();
640 assert_eq!(
641 message,
642 AuthenticatedMessage::from_parsed(
643 &MessageParser::new().parse(&raw_message).unwrap(),
644 &raw_message,
645 strict
646 )
647 );
648 let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
649
650 match (dkim.last().unwrap().result(), &expect) {
651 (DkimResult::Pass, Ok(_)) => (),
652 (
653 DkimResult::Fail(hdr) | DkimResult::PermError(hdr) | DkimResult::Neutral(hdr),
654 Err(err),
655 ) if hdr == err => (),
656 (result, expect) => panic!("Expected {expect:?} but got {result:?}."),
657 }
658
659 dkim.into_iter()
660 .map(|d| DkimOutput {
661 result: d.result,
662 signature: None,
663 report: d.report,
664 is_atps: d.is_atps,
665 })
666 .collect()
667 }
668
669 pub(crate) async fn verify<'x>(
670 resolver: &MessageAuthenticator,
671 caches: &DummyCaches,
672 signature: Signature,
673 message_: &'x str,
674 expect: Result<(), super::Error>,
675 ) -> Vec<DkimOutput<'x>> {
676 verify_with_opts(resolver, caches, signature, message_, expect, true).await
677 }
678}