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
7//! One-shot DKIM1 signing of a message held in memory, as a single buffer or
8//! as a sequence of chunks.
9
10use 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    /// Signs a complete RFC 5322 message (headers and body).
21    ///
22    /// Headers listed with [`headers`](DkimSigner::headers) are signed, the
23    /// body is hashed with the configured canonicalization, `t=` is set to
24    /// the current time and `x=` and `l=` are filled in when enabled. The
25    /// returned [`Signature`] is written in front of the message as a
26    /// `DKIM-Signature` header with
27    /// [`HeaderWriter`](crate::headers::HeaderWriter).
28    ///
29    /// # Errors
30    ///
31    /// Returns [`Error::NoHeadersFound`] when the list of headers to sign is
32    /// empty, or [`Error::Crypto`] when the signing operation fails. Listed
33    /// headers that are absent from the message are still signed (as empty),
34    /// which prevents them from being added later.
35    ///
36    /// # Example
37    ///
38    /// ```rust,no_run
39    /// use mail_auth::{crypto::Ed25519Key, dkim::DkimSigner, headers::HeaderWriter};
40    ///
41    /// # fn main() -> mail_auth::Result<()> {
42    /// # let pkcs8_der: Vec<u8> = Vec::new();
43    /// let message = "From: bill@example.com\r\nSubject: Hi\r\n\r\nHello.\r\n";
44    /// let signature = DkimSigner::from_key(Ed25519Key::from_pkcs8_der(&pkcs8_der)?)
45    ///     .domain("example.com")
46    ///     .selector("ed")
47    ///     .headers(["From", "Subject"])
48    ///     .sign(message.as_bytes())?;
49    /// println!("{}{}", signature.to_header(), message);
50    /// # Ok(())
51    /// # }
52    /// ```
53    #[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    /// Signs a message supplied as consecutive chunks, for example extra
66    /// headers prepended to a stored message.
67    ///
68    /// The chunks are read as if concatenated; otherwise identical to
69    /// [`sign`](DkimSigner::sign).
70    ///
71    /// # Errors
72    ///
73    /// Returns [`Error::NoHeadersFound`] when the list of headers to sign is
74    /// empty, or [`Error::Crypto`] when the signing operation fails. Listed
75    /// headers that are absent from the message are still signed (as empty),
76    /// which prevents them from being added later.
77    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}