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};
16use mail_builder::encoders::base64::base64_encode;
17
18impl<T: SigningKey> DkimSigner<T, Done> {
19    /// Signs a message.
20    #[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    /// Signs a chained message.
33    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        // Canonicalize headers and body
52        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        // Create Signature
60        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        // Sign
75        let b = self.key.sign(SignableMessage {
76            headers: canonical_headers,
77            signature: &signature,
78        })?;
79
80        // Encode
81        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        // Create private keys
212        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        // Create resolver
219        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}