Skip to main content

mail_auth/dkim/
sign.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use 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    /// Signs a message.
19    #[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    /// Signs a chained message.
32    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        // Canonicalize headers and body
51        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        // Create Signature
59        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        // Sign
73        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        // Create private keys
207        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        // Create resolver
214        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}