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