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    #[cfg(any(feature = "rust-crypto", feature = "ring"))]
138    #[test]
139    fn dkim_sign() {
140        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
141        let pk = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
142            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
143        ))
144        .unwrap();
145        #[cfg(feature = "rust-crypto")]
146        let pk = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
147        let signature = DkimSigner::from_key(pk)
148            .domain("stalw.art")
149            .selector("default")
150            .headers(["From", "To", "Subject"])
151            .sign_stream(
152                HeaderIterator::new(
153                    concat!(
154                        "From: hello@stalw.art\r\n",
155                        "To: dkim@stalw.art\r\n",
156                        "Subject: Testing  DKIM!\r\n\r\n",
157                        "Here goes the test\r\n\r\n"
158                    )
159                    .as_bytes(),
160                ),
161                311923920,
162            )
163            .unwrap();
164
165        assert_eq!(
166            concat!(
167                "dkim-signature:v=1; a=rsa-sha256; s=default; d=stalw.art; ",
168                "c=relaxed/relaxed; h=Subject:To:From; t=311923920; ",
169                "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Yl m5s=; ",
170                "b=B/p1FPSJ+Jl4A94381+DTZZnNO4c3fVqDnj0M0Vk5JuvnKb5",
171                "dKSwaoIHPO8UUJsroqH z+R0/eWyW1Vlz+uMIZc2j7MVPJcGaY",
172                "Ni85uCQbPd8VpDKWWab6m21ngXYIpagmzKOKYllyOeK3X qwDz",
173                "Bo0T2DdNjGyMUOAWHxrKGU+fbcPHQYxTBCpfOxE/nc/uxxqh+i",
174                "2uXrsxz7PdCEN01LZiYVV yOzcv0ER9A7aDReE2XPVHnFL8jxE",
175                "2BD53HRv3hGkIDcC6wKOKG/lmID+U8tQk5CP0dLmprgjgTv Se",
176                "bu6xNc6SSIgpvwryAAzJEVwmaBqvE8RNk3Vg10lBZEuNsj2Q==;",
177            ),
178            signature.to_string()
179        );
180    }
181
182    #[cfg(any(feature = "rust-crypto", feature = "ring"))]
183    #[tokio::test]
184    async fn dkim_sign_verify() {
185        use crate::common::cache::test::DummyCaches;
186
187        let message = concat!(
188            "From: bill@example.com\r\n",
189            "To: jdoe@example.com\r\n",
190            "Subject: TPS Report\r\n",
191            "\r\n",
192            "I'm going to need those TPS reports ASAP. ",
193            "So, if you could do that, that'd be great.\r\n"
194        );
195        let empty_message = concat!(
196            "From: bill@example.com\r\n",
197            "To: jdoe@example.com\r\n",
198            "Subject: Empty TPS Report\r\n",
199            "\r\n",
200            "\r\n"
201        );
202        let message_multiheader = concat!(
203            "X-Duplicate-Header: 4\r\n",
204            "From: bill@example.com\r\n",
205            "X-Duplicate-Header: 3\r\n",
206            "To: jdoe@example.com\r\n",
207            "X-Duplicate-Header: 2\r\n",
208            "Subject: TPS Report\r\n",
209            "X-Duplicate-Header: 1\r\n",
210            "To: jane@example.com\r\n",
211            "\r\n",
212            "I'm going to need those TPS reports ASAP. ",
213            "So, if you could do that, that'd be great.\r\n"
214        );
215
216        // Create private keys
217        #[cfg(feature = "rust-crypto")]
218        let pk_ed = Ed25519Key::from_bytes(&base64_decode(ED25519_PRIVATE_KEY.as_bytes()).unwrap())
219            .unwrap();
220        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
221        let pk_ed = Ed25519Key::from_seed_and_public_key(
222            &base64_decode(ED25519_PRIVATE_KEY.as_bytes()).unwrap(),
223            &base64_decode(ED25519_PUBLIC_KEY.rsplit_once("p=").unwrap().1.as_bytes()).unwrap(),
224        )
225        .unwrap();
226
227        // Create resolver
228        let resolver = MessageAuthenticator::new_system_conf().unwrap();
229        let caches = DummyCaches::new()
230            .with_txt(
231                "default._domainkey.example.com.".to_string(),
232                DomainKey::parse(RSA_PUBLIC_KEY.as_bytes()).unwrap(),
233                Instant::now() + Duration::new(3600, 0),
234            )
235            .with_txt(
236                "ed._domainkey.example.com.".to_string(),
237                DomainKey::parse(ED25519_PUBLIC_KEY.as_bytes()).unwrap(),
238                Instant::now() + Duration::new(3600, 0),
239            )
240            .with_txt(
241                "_report._domainkey.example.com.".to_string(),
242                DomainKeyReport::parse("ra=dkim-failures; rp=100; rr=x".as_bytes()).unwrap(),
243                Instant::now() + Duration::new(3600, 0),
244            );
245
246        dbg!("Test RSA-SHA256 relaxed/relaxed");
247        #[cfg(feature = "rust-crypto")]
248        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
249        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
250        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
251            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
252        ))
253        .unwrap();
254        verify(
255            &resolver,
256            &caches,
257            DkimSigner::from_key(pk_rsa)
258                .domain("example.com")
259                .selector("default")
260                .headers(["From", "To", "Subject"])
261                .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
262                .sign(message.as_bytes())
263                .unwrap(),
264            message,
265            Ok(()),
266        )
267        .await;
268
269        dbg!("Test ED25519-SHA256 relaxed/relaxed");
270        verify(
271            &resolver,
272            &caches,
273            DkimSigner::from_key(pk_ed)
274                .domain("example.com")
275                .selector("ed")
276                .headers(["From", "To", "Subject"])
277                .sign(message.as_bytes())
278                .unwrap(),
279            message,
280            Ok(()),
281        )
282        .await;
283
284        dbg!("Test RSA-SHA256 relaxed/relaxed with an empty message");
285        #[cfg(feature = "rust-crypto")]
286        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
287        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
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                .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
300                .sign(empty_message.as_bytes())
301                .unwrap(),
302            empty_message,
303            Ok(()),
304        )
305        .await;
306
307        dbg!("Test RSA-SHA256 simple/simple with an empty message");
308        #[cfg(feature = "rust-crypto")]
309        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
310        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
311        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
312            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
313        ))
314        .unwrap();
315        verify(
316            &resolver,
317            &caches,
318            DkimSigner::from_key(pk_rsa)
319                .domain("example.com")
320                .selector("default")
321                .headers(["From", "To", "Subject"])
322                .header_canonicalization(Canonicalization::Simple)
323                .body_canonicalization(Canonicalization::Simple)
324                .agent_user_identifier("\"John Doe\" <jdoe@example.com>")
325                .sign(empty_message.as_bytes())
326                .unwrap(),
327            empty_message,
328            Ok(()),
329        )
330        .await;
331
332        dbg!("Test RSA-SHA256 simple/simple with duplicated headers");
333        #[cfg(feature = "rust-crypto")]
334        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
335        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
336        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
337            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
338        ))
339        .unwrap();
340        verify(
341            &resolver,
342            &caches,
343            DkimSigner::from_key(pk_rsa)
344                .domain("example.com")
345                .selector("default")
346                .headers([
347                    "From",
348                    "To",
349                    "Subject",
350                    "X-Duplicate-Header",
351                    "X-Does-Not-Exist",
352                ])
353                .header_canonicalization(Canonicalization::Simple)
354                .body_canonicalization(Canonicalization::Simple)
355                .sign(message_multiheader.as_bytes())
356                .unwrap(),
357            message_multiheader,
358            Ok(()),
359        )
360        .await;
361
362        dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (relaxed)");
363        #[cfg(feature = "rust-crypto")]
364        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
365        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
366        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
367            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
368        ))
369        .unwrap();
370        verify_with_opts(
371            &resolver,
372            &caches,
373            DkimSigner::from_key(pk_rsa)
374                .domain("example.com")
375                .selector("default")
376                .headers(["From", "To", "Subject"])
377                .header_canonicalization(Canonicalization::Simple)
378                .body_length(true)
379                .sign(message.as_bytes())
380                .unwrap(),
381            &(message.to_string() + "\r\n----- Mailing list"),
382            Ok(()),
383            false,
384        )
385        .await;
386
387        dbg!("Test RSA-SHA256 simple/relaxed with fixed body length (strict)");
388        #[cfg(feature = "rust-crypto")]
389        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
390        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
391        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
392            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
393        ))
394        .unwrap();
395        verify_with_opts(
396            &resolver,
397            &caches,
398            DkimSigner::from_key(pk_rsa)
399                .domain("example.com")
400                .selector("default")
401                .headers(["From", "To", "Subject"])
402                .header_canonicalization(Canonicalization::Simple)
403                .body_length(true)
404                .sign(message.as_bytes())
405                .unwrap(),
406            &(message.to_string() + "\r\n----- Mailing list"),
407            Err(super::Error::SignatureLength),
408            true,
409        )
410        .await;
411
412        dbg!("Test AUID not matching domains");
413        #[cfg(feature = "rust-crypto")]
414        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
415        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
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                .agent_user_identifier("@wrongdomain.com")
428                .sign(message.as_bytes())
429                .unwrap(),
430            message,
431            Err(super::Error::FailedAuidMatch),
432        )
433        .await;
434
435        dbg!("Test expired signature and reporting");
436        #[cfg(feature = "rust-crypto")]
437        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
438        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
439        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
440            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
441        ))
442        .unwrap();
443        let r = verify(
444            &resolver,
445            &caches,
446            DkimSigner::from_key(pk_rsa)
447                .domain("example.com")
448                .selector("default")
449                .headers(["From", "To", "Subject"])
450                .expiration(12345)
451                .reporting(true)
452                .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
453                .unwrap(),
454            message,
455            Err(super::Error::SignatureExpired),
456        )
457        .await
458        .pop()
459        .unwrap()
460        .report;
461        assert_eq!(r.as_deref(), Some("dkim-failures@example.com"));
462
463        dbg!("Verify ATPS (failure)");
464        #[cfg(feature = "rust-crypto")]
465        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
466        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
467        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
468            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
469        ))
470        .unwrap();
471        verify(
472            &resolver,
473            &caches,
474            DkimSigner::from_key(pk_rsa)
475                .domain("example.com")
476                .selector("default")
477                .headers(["From", "To", "Subject"])
478                .atps("example.com")
479                .atpsh(HashAlgorithm::Sha256)
480                .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
481                .unwrap(),
482            message,
483            Err(super::Error::DnsRecordNotFound(ResponseCode::NXDomain)),
484        )
485        .await;
486
487        dbg!("Verify ATPS (success)");
488        #[cfg(feature = "rust-crypto")]
489        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
490        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
491        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
492            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
493        ))
494        .unwrap();
495        caches.txt_add(
496            "UN42N5XOV642KXRXRQIYANHCOUPGQL5LT4WTBKYT2IJFLBWODFDQ._atps.example.com.".to_string(),
497            Atps::parse(b"v=ATPS1;").unwrap(),
498            Instant::now() + Duration::new(3600, 0),
499        );
500        verify(
501            &resolver,
502            &caches,
503            DkimSigner::from_key(pk_rsa)
504                .domain("example.com")
505                .selector("default")
506                .headers(["From", "To", "Subject"])
507                .atps("example.com")
508                .atpsh(HashAlgorithm::Sha256)
509                .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
510                .unwrap(),
511            message,
512            Ok(()),
513        )
514        .await;
515
516        dbg!("Verify ATPS (success - no hash)");
517        #[cfg(feature = "rust-crypto")]
518        let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap();
519        #[cfg(all(feature = "ring", not(feature = "rust-crypto")))]
520        let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
521            PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
522        ))
523        .unwrap();
524        caches.txt_add(
525            "example.com._atps.example.com.".to_string(),
526            Atps::parse(b"v=ATPS1;").unwrap(),
527            Instant::now() + Duration::new(3600, 0),
528        );
529        verify(
530            &resolver,
531            &caches,
532            DkimSigner::from_key(pk_rsa)
533                .domain("example.com")
534                .selector("default")
535                .headers(["From", "To", "Subject"])
536                .atps("example.com")
537                .sign_stream(HeaderIterator::new(message.as_bytes()), 12345)
538                .unwrap(),
539            message,
540            Ok(()),
541        )
542        .await;
543    }
544
545    pub(crate) async fn verify_with_opts<'x>(
546        resolver: &MessageAuthenticator,
547        caches: &DummyCaches,
548        signature: Signature,
549        message_: &'x str,
550        expect: Result<(), super::Error>,
551        strict: bool,
552    ) -> Vec<DkimOutput<'x>> {
553        let mut raw_message = Vec::with_capacity(message_.len() + 100);
554        signature.write(&mut raw_message, true);
555        raw_message.extend_from_slice(message_.as_bytes());
556
557        let message = AuthenticatedMessage::parse_with_opts(&raw_message, strict).unwrap();
558        assert_eq!(
559            message,
560            AuthenticatedMessage::from_parsed(
561                &MessageParser::new().parse(&raw_message).unwrap(),
562                strict
563            )
564        );
565        let dkim = resolver.verify_dkim(caches.parameters(&message)).await;
566
567        match (dkim.last().unwrap().result(), &expect) {
568            (DkimResult::Pass, Ok(_)) => (),
569            (
570                DkimResult::Fail(hdr) | DkimResult::PermError(hdr) | DkimResult::Neutral(hdr),
571                Err(err),
572            ) if hdr == err => (),
573            (result, expect) => panic!("Expected {expect:?} but got {result:?}."),
574        }
575
576        dkim.into_iter()
577            .map(|d| DkimOutput {
578                result: d.result,
579                signature: None,
580                report: d.report,
581                is_atps: d.is_atps,
582            })
583            .collect()
584    }
585
586    pub(crate) async fn verify<'x>(
587        resolver: &MessageAuthenticator,
588        caches: &DummyCaches,
589        signature: Signature,
590        message_: &'x str,
591        expect: Result<(), super::Error>,
592    ) -> Vec<DkimOutput<'x>> {
593        verify_with_opts(resolver, caches, signature, message_, expect, true).await
594    }
595}