1use 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 #[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 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 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 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 let b = self.key.sign(SignableMessage {
76 headers: canonical_headers,
77 signature: &signature,
78 })?;
79
80 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 #[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 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}