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