Commit
Author: Dirkjan Ochtman [dirkjan@ochtman.nl]
Hash: aaf6f56de68dd29a1a86511b869a2ad1d33ced09
Timestamp: Wed, 25 Jan 2023 10:34:29 +0000 (1 year ago)

+199 -120 +/-7 browse
Defer hashing to signing/verification impls
1diff --git a/src/arc/seal.rs b/src/arc/seal.rs
2index 0ba3101..0c24d6b 100644
3--- a/src/arc/seal.rs
4+++ b/src/arc/seal.rs
5 @@ -14,10 +14,10 @@ use mail_builder::encoders::base64::base64_encode;
6
7 use crate::{
8 common::{
9- crypto::{HashAlgorithm, HashContext, Sha256, SigningKey},
10- headers::Writer,
11+ crypto::{HashAlgorithm, Sha256, SigningKey},
12+ headers::{Writable, Writer},
13 },
14- dkim::{Canonicalization, Done},
15+ dkim::{canonicalize::CanonicalHeaders, Canonicalization, Done},
16 ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimResult, Error,
17 };
18
19 @@ -55,12 +55,9 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> {
20 _ => ChainValidation::Fail,
21 };
22 }
23- // Canonicalize headers
24- let mut header_hasher = self.key.hasher();
25- let signed_headers = set
26- .signature
27- .canonicalize_headers(message, &mut header_hasher)?;
28
29+ // Canonicalize headers
30+ let (canonical_headers, signed_headers) = set.signature.canonicalize_headers(message)?;
31 if signed_headers.is_empty() {
32 return Err(Error::NoHeadersFound);
33 }
34 @@ -78,15 +75,16 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> {
35 // Use cached hash
36 set.signature.bh = base64_encode(bh)?;
37 } else {
38- let mut body_hasher = self.key.hasher();
39- set.signature.cb.canonicalize_body(
40- message
41- .raw_message
42- .get(message.body_offset..)
43- .unwrap_or_default(),
44- &mut body_hasher,
45+ let hash = self.key.hash(
46+ set.signature.cb.canonical_body(
47+ message
48+ .raw_message
49+ .get(message.body_offset..)
50+ .unwrap_or_default(),
51+ u64::MAX,
52+ ),
53 );
54- set.signature.bh = base64_encode(body_hasher.complete().as_ref())?;
55+ set.signature.bh = base64_encode(hash.as_ref())?;
56 }
57
58 // Create Signature
59 @@ -103,39 +101,60 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> {
60 };
61 set.signature.h = signed_headers;
62
63- // Add signature to hash
64- set.signature.write(&mut header_hasher, false);
65-
66 // Sign
67- let b = self.key.sign(header_hasher.complete())?;
68+ let b = self.key.sign(SignableSet {
69+ set: &set,
70+ headers: canonical_headers,
71+ })?;
72 set.signature.b = base64_encode(&b)?;
73
74- // Hash ARC chain
75- let mut header_hasher = self.key.hasher();
76- if !arc_output.set.is_empty() {
77+ // Seal
78+ let b = self.key.sign(SignableChain {
79+ arc_output,
80+ set: &set,
81+ })?;
82+ set.seal.b = base64_encode(&b)?;
83+
84+ Ok(set)
85+ }
86+ }
87+
88+ struct SignableSet<'a> {
89+ set: &'a ArcSet<'a>,
90+ headers: CanonicalHeaders<'a>,
91+ }
92+
93+ impl<'a> Writable for SignableSet<'a> {
94+ fn write(self, writer: &mut impl Writer) {
95+ self.headers.write(writer);
96+ self.set.signature.write(writer, false);
97+ }
98+ }
99+
100+ struct SignableChain<'a> {
101+ arc_output: &'a ArcOutput<'a>,
102+ set: &'a ArcSet<'a>,
103+ }
104+
105+ impl<'a> Writable for SignableChain<'a> {
106+ fn write(self, writer: &mut impl Writer) {
107+ if !self.arc_output.set.is_empty() {
108 Canonicalization::Relaxed.canonicalize_headers(
109- &mut arc_output.set.iter().flat_map(|set| {
110+ self.arc_output.set.iter().flat_map(|set| {
111 [
112 (set.results.name, set.results.value),
113 (set.signature.name, set.signature.value),
114 (set.seal.name, set.seal.value),
115 ]
116 }),
117- &mut header_hasher,
118+ writer,
119 );
120 }
121
122- // Hash ARC headers for the current instance
123- set.results.write(&mut header_hasher, set.seal.i, false);
124- set.signature.write(&mut header_hasher, false);
125- header_hasher.write(b"\r\n");
126- set.seal.write(&mut header_hasher, false);
127-
128- // Seal
129- let b = self.key.sign(header_hasher.complete())?;
130- set.seal.b = base64_encode(&b)?;
131-
132- Ok(set)
133+ self.set.results.write(writer, self.set.seal.i, false);
134+ self.set.signature.write(writer, false);
135+ writer.write(b"\r\n");
136+ self.set.seal.write(writer, false);
137 }
138 }
139
140 @@ -143,8 +162,7 @@ impl Signature {
141 pub(crate) fn canonicalize_headers<'x>(
142 &self,
143 message: &'x AuthenticatedMessage<'x>,
144- header_hasher: &mut impl Writer,
145- ) -> crate::Result<Vec<String>> {
146+ ) -> crate::Result<(CanonicalHeaders<'x>, Vec<String>)> {
147 let mut headers = Vec::with_capacity(self.h.len());
148 let mut found_headers = vec![false; self.h.len()];
149 let mut signed_headers = Vec::with_capacity(self.h.len());
150 @@ -161,8 +179,7 @@ impl Signature {
151 }
152 }
153
154- self.ch
155- .canonicalize_headers(&mut headers.into_iter().rev(), header_hasher);
156+ let canonical_headers = self.ch.canonical_headers(headers);
157
158 // Add any missing headers
159 signed_headers.reverse();
160 @@ -172,7 +189,7 @@ impl Signature {
161 }
162 }
163
164- Ok(signed_headers)
165+ Ok((canonical_headers, signed_headers))
166 }
167 }
168
169 diff --git a/src/common/crypto/mod.rs b/src/common/crypto/mod.rs
170index 6c8693d..1ee3b41 100644
171--- a/src/common/crypto/mod.rs
172+++ b/src/common/crypto/mod.rs
173 @@ -2,7 +2,7 @@ use sha1::{digest::Output, Digest};
174
175 use crate::{dkim::Canonicalization, Result};
176
177- use super::headers::Writer;
178+ use super::headers::{Writable, Writer};
179
180 mod rust_crypto;
181 pub use rust_crypto::{Ed25519Key, RsaKey};
182 @@ -11,10 +11,12 @@ pub(crate) use rust_crypto::{Ed25519PublicKey, RsaPublicKey};
183 pub trait SigningKey {
184 type Hasher: HashImpl;
185
186- fn sign(&self, data: HashOutput) -> Result<Vec<u8>>;
187+ fn sign(&self, input: impl Writable) -> Result<Vec<u8>>;
188
189- fn hasher(&self) -> <Self::Hasher as HashImpl>::Context {
190- <Self::Hasher as HashImpl>::hasher()
191+ fn hash(&self, data: impl Writable) -> HashOutput {
192+ let mut hasher = <Self::Hasher as HashImpl>::hasher();
193+ data.write(&mut hasher);
194+ hasher.complete()
195 }
196
197 fn algorithm(&self) -> Algorithm;
198 @@ -71,10 +73,18 @@ pub enum HashAlgorithm {
199 }
200
201 impl HashAlgorithm {
202- pub fn hash(&self, data: &[u8]) -> HashOutput {
203+ pub fn hash(&self, data: impl Writable) -> HashOutput {
204 match self {
205- Self::Sha1 => HashOutput::Sha1(sha1::Sha1::digest(data)),
206- Self::Sha256 => HashOutput::Sha256(sha2::Sha256::digest(data)),
207+ Self::Sha1 => {
208+ let mut hasher = sha1::Sha1::new();
209+ data.write(&mut hasher);
210+ HashOutput::Sha1(hasher.finalize())
211+ }
212+ Self::Sha256 => {
213+ let mut hasher = sha2::Sha256::new();
214+ data.write(&mut hasher);
215+ HashOutput::Sha256(hasher.finalize())
216+ }
217 }
218 }
219 }
220 diff --git a/src/common/crypto/rust_crypto.rs b/src/common/crypto/rust_crypto.rs
221index 2d6c34d..a1af70d 100644
222--- a/src/common/crypto/rust_crypto.rs
223+++ b/src/common/crypto/rust_crypto.rs
224 @@ -4,7 +4,11 @@ use ed25519_dalek::Signer;
225 use rsa::{pkcs1::DecodeRsaPrivateKey, PaddingScheme, PublicKey as _, RsaPrivateKey};
226 use sha2::digest::Digest;
227
228- use crate::{common::headers::Writer, dkim::Canonicalization, Error, Result};
229+ use crate::{
230+ common::headers::{Writable, Writer},
231+ dkim::Canonicalization,
232+ Error, Result,
233+ };
234
235 use super::{Algorithm, HashContext, HashImpl, HashOutput, Sha1, Sha256, SigningKey, VerifyingKey};
236
237 @@ -41,11 +45,12 @@ impl<T: HashImpl> RsaKey<T> {
238 impl SigningKey for RsaKey<Sha1> {
239 type Hasher = Sha1;
240
241- fn sign(&self, data: HashOutput) -> Result<Vec<u8>> {
242+ fn sign(&self, input: impl Writable) -> Result<Vec<u8>> {
243+ let hash = self.hash(input);
244 self.inner
245 .sign(
246 PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
247- data.as_ref(),
248+ hash.as_ref(),
249 )
250 .map_err(|err| Error::CryptoError(err.to_string()))
251 }
252 @@ -58,11 +63,12 @@ impl SigningKey for RsaKey<Sha1> {
253 impl SigningKey for RsaKey<Sha256> {
254 type Hasher = Sha256;
255
256- fn sign(&self, data: HashOutput) -> Result<Vec<u8>> {
257+ fn sign(&self, input: impl Writable) -> Result<Vec<u8>> {
258+ let hash = self.hash(input);
259 self.inner
260 .sign(
261 PaddingScheme::new_pkcs1v15_sign::<<Self::Hasher as HashImpl>::Context>(),
262- data.as_ref(),
263+ hash.as_ref(),
264 )
265 .map_err(|err| Error::CryptoError(err.to_string()))
266 }
267 @@ -93,8 +99,9 @@ impl Ed25519Key {
268 impl SigningKey for Ed25519Key {
269 type Hasher = Sha256;
270
271- fn sign(&self, data: HashOutput) -> Result<Vec<u8>> {
272- Ok(self.inner.sign(data.as_ref()).to_bytes().to_vec())
273+ fn sign(&self, input: impl Writable) -> Result<Vec<u8>> {
274+ let hash = self.hash(input);
275+ Ok(self.inner.sign(hash.as_ref()).to_bytes().to_vec())
276 }
277
278 fn algorithm(&self) -> Algorithm {
279 @@ -128,7 +135,10 @@ impl VerifyingKey for RsaPublicKey {
280 ) -> Result<()> {
281 match algorithm {
282 Algorithm::RsaSha256 => {
283- let hash = canonicalization.hash_headers::<Sha256>(headers);
284+ let mut hasher = sha2::Sha256::new();
285+ canonicalization.canonicalize_headers(headers, &mut hasher);
286+ let hash = hasher.finalize();
287+
288 self.inner
289 .verify(
290 PaddingScheme::new_pkcs1v15_sign::<sha2::Sha256>(),
291 @@ -138,7 +148,10 @@ impl VerifyingKey for RsaPublicKey {
292 .map_err(|_| Error::FailedVerification)
293 }
294 Algorithm::RsaSha1 => {
295- let hash = canonicalization.hash_headers::<Sha1>(headers);
296+ let mut hasher = sha1::Sha1::new();
297+ canonicalization.canonicalize_headers(headers, &mut hasher);
298+ let hash = hasher.finalize();
299+
300 self.inner
301 .verify(
302 PaddingScheme::new_pkcs1v15_sign::<sha1::Sha1>(),
303 @@ -179,7 +192,10 @@ impl VerifyingKey for Ed25519PublicKey {
304 return Err(Error::IncompatibleAlgorithms);
305 }
306
307- let hash = canonicalization.hash_headers::<Sha256>(headers);
308+ let mut hasher = sha2::Sha256::new();
309+ canonicalization.canonicalize_headers(headers, &mut hasher);
310+ let hash = hasher.finalize();
311+
312 self.inner
313 .verify_strict(
314 hash.as_ref(),
315 diff --git a/src/common/headers.rs b/src/common/headers.rs
316index 4b24ea2..51cdb07 100644
317--- a/src/common/headers.rs
318+++ b/src/common/headers.rs
319 @@ -343,6 +343,16 @@ pub trait HeaderWriter: Sized {
320 }
321 }
322
323+ pub trait Writable {
324+ fn write(self, writer: &mut impl Writer);
325+ }
326+
327+ impl Writable for &[u8] {
328+ fn write(self, writer: &mut impl Writer) {
329+ writer.write(self);
330+ }
331+ }
332+
333 pub trait Writer {
334 fn write(&mut self, buf: &[u8]);
335
336 diff --git a/src/common/message.rs b/src/common/message.rs
337index c6ed497..b5a33c1 100644
338--- a/src/common/message.rs
339+++ b/src/common/message.rs
340 @@ -10,11 +10,7 @@
341
342 use mail_parser::{parsers::MessageStream, HeaderValue};
343
344- use crate::{
345- arc,
346- common::crypto::{HashAlgorithm, Sha1, Sha256},
347- dkim, AuthenticatedMessage,
348- };
349+ use crate::{arc, common::crypto::HashAlgorithm, dkim, AuthenticatedMessage};
350
351 use super::headers::{AuthenticatedHeader, Header, HeaderParser};
352
353 @@ -159,10 +155,7 @@ impl<'x> AuthenticatedMessage<'x> {
354
355 // Calculate body hashes
356 for (cb, ha, l, bh) in &mut message.body_hashes {
357- *bh = match ha {
358- HashAlgorithm::Sha256 => cb.hash_body::<Sha256>(body, *l).as_ref().to_vec(),
359- HashAlgorithm::Sha1 => cb.hash_body::<Sha1>(body, *l).as_ref().to_vec(),
360- };
361+ *bh = ha.hash(cb.canonical_body(body, *l)).as_ref().to_vec();
362 }
363
364 // Sort ARC headers
365 diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs
366index a0a8bd1..4184675 100644
367--- a/src/dkim/canonicalize.rs
368+++ b/src/dkim/canonicalize.rs
369 @@ -8,22 +8,24 @@
370 * except according to those terms.
371 */
372
373- use crate::common::{
374- crypto::{HashContext, HashImpl},
375- headers::{HeaderStream, Writer},
376- };
377+ use crate::common::headers::{HeaderStream, Writable, Writer};
378
379 use super::{Canonicalization, Signature};
380
381- impl Canonicalization {
382- pub fn canonicalize_body(&self, body: &[u8], hasher: &mut impl Writer) {
383+ pub struct CanonicalBody<'a> {
384+ canonicalization: Canonicalization,
385+ body: &'a [u8],
386+ }
387+
388+ impl Writable for CanonicalBody<'_> {
389+ fn write(self, hasher: &mut impl Writer) {
390 let mut crlf_seq = 0;
391
392- match self {
393+ match self.canonicalization {
394 Canonicalization::Relaxed => {
395 let mut last_ch = 0;
396
397- for &ch in body {
398+ for &ch in self.body {
399 match ch {
400 b' ' | b'\t' => {
401 while crlf_seq > 0 {
402 @@ -53,7 +55,7 @@ impl Canonicalization {
403 }
404 }
405 Canonicalization::Simple => {
406- for &ch in body {
407+ for &ch in self.body {
408 match ch {
409 b'\n' => {
410 crlf_seq += 1;
411 @@ -73,10 +75,12 @@ impl Canonicalization {
412
413 hasher.write(b"\r\n");
414 }
415+ }
416
417- pub fn canonicalize_headers<'x>(
418+ impl Canonicalization {
419+ pub fn canonicalize_headers<'a>(
420 &self,
421- headers: &mut dyn Iterator<Item = (&'x [u8], &'x [u8])>,
422+ headers: impl Iterator<Item = (&'a [u8], &'a [u8])>,
423 hasher: &mut impl Writer,
424 ) {
425 match self {
426 @@ -87,6 +91,7 @@ impl Canonicalization {
427 hasher.write(&[ch.to_ascii_lowercase()]);
428 }
429 }
430+
431 hasher.write(b":");
432 let mut bw = 0;
433 let mut last_ch = 0;
434 @@ -100,6 +105,7 @@ impl Canonicalization {
435 }
436 last_ch = ch;
437 }
438+
439 if last_ch == b'\n' {
440 hasher.write(b"\r\n");
441 }
442 @@ -115,26 +121,25 @@ impl Canonicalization {
443 }
444 }
445
446- pub fn hash_headers<'x, T: HashImpl>(
447+ pub fn canonical_headers<'a>(
448 &self,
449- headers: &mut dyn Iterator<Item = (&'x [u8], &'x [u8])>,
450- ) -> impl AsRef<[u8]> {
451- let mut hasher = T::hasher();
452- self.canonicalize_headers(headers, &mut hasher);
453- hasher.complete()
454+ headers: Vec<(&'a [u8], &'a [u8])>,
455+ ) -> CanonicalHeaders<'a> {
456+ CanonicalHeaders {
457+ canonicalization: *self,
458+ headers,
459+ }
460 }
461
462- pub fn hash_body<T: HashImpl>(&self, body: &[u8], l: u64) -> impl AsRef<[u8]> {
463- let mut hasher = T::hasher();
464- self.canonicalize_body(
465- if l == 0 || body.is_empty() {
466+ pub fn canonical_body<'a>(&self, body: &'a [u8], l: u64) -> CanonicalBody<'a> {
467+ CanonicalBody {
468+ canonicalization: *self,
469+ body: if l == 0 || body.is_empty() {
470 body
471 } else {
472 &body[..std::cmp::min(l as usize, body.len())]
473 },
474- &mut hasher,
475- );
476- hasher.complete()
477+ }
478 }
479
480 pub fn serialize_name(&self, writer: &mut impl Writer) {
481 @@ -146,13 +151,10 @@ impl Canonicalization {
482 }
483
484 impl Signature {
485- #[allow(clippy::while_let_on_iterator)]
486 pub(crate) fn canonicalize<'x>(
487 &self,
488 mut message: impl HeaderStream<'x>,
489- header_hasher: &mut impl Writer,
490- body_hasher: &mut impl Writer,
491- ) -> (usize, Vec<String>) {
492+ ) -> (usize, CanonicalHeaders<'x>, Vec<String>, CanonicalBody<'x>) {
493 let mut headers = Vec::with_capacity(self.h.len());
494 let mut found_headers = vec![false; self.h.len()];
495 let mut signed_headers = Vec::with_capacity(self.h.len());
496 @@ -171,9 +173,8 @@ impl Signature {
497
498 let body = message.body();
499 let body_len = body.len();
500- self.ch
501- .canonicalize_headers(&mut headers.into_iter().rev(), header_hasher);
502- self.cb.canonicalize_body(body, body_hasher);
503+ let canonical_headers = self.ch.canonical_headers(headers);
504+ let canonical_body = self.ch.canonical_body(&body, u64::MAX);
505
506 // Add any missing headers
507 signed_headers.reverse();
508 @@ -183,13 +184,29 @@ impl Signature {
509 }
510 }
511
512- (body_len, signed_headers)
513+ (body_len, canonical_headers, signed_headers, canonical_body)
514+ }
515+ }
516+
517+ pub struct CanonicalHeaders<'a> {
518+ canonicalization: Canonicalization,
519+ headers: Vec<(&'a [u8], &'a [u8])>,
520+ }
521+
522+ impl<'a> Writable for CanonicalHeaders<'a> {
523+ fn write(self, writer: &mut impl Writer) {
524+ self.canonicalization
525+ .canonicalize_headers(self.headers.into_iter().rev(), writer)
526 }
527 }
528
529 #[cfg(test)]
530 mod test {
531- use crate::{common::headers::HeaderIterator, dkim::Canonicalization};
532+ use super::{CanonicalBody, CanonicalHeaders};
533+ use crate::{
534+ common::headers::{HeaderIterator, Writable},
535+ dkim::Canonicalization,
536+ };
537
538 #[test]
539 #[allow(clippy::needless_collect)]
540 @@ -253,13 +270,19 @@ mod test {
541 (Canonicalization::Simple, simple_headers, simple_body),
542 ] {
543 let mut headers = Vec::new();
544- let mut body = Vec::new();
545-
546- canonicalization
547- .canonicalize_headers(&mut parsed_headers.clone().into_iter(), &mut headers);
548- canonicalization.canonicalize_body(raw_body, &mut body);
549-
550+ CanonicalHeaders {
551+ canonicalization,
552+ headers: parsed_headers.iter().cloned().rev().collect(),
553+ }
554+ .write(&mut headers);
555 assert_eq!(expected_headers, String::from_utf8(headers).unwrap());
556+
557+ let mut body = Vec::new();
558+ CanonicalBody {
559+ canonicalization,
560+ body: &raw_body,
561+ }
562+ .write(&mut body);
563 assert_eq!(expected_body, String::from_utf8(body).unwrap());
564 }
565 }
566 diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs
567index 5b47db7..7d9cf3f 100644
568--- a/src/dkim/sign.rs
569+++ b/src/dkim/sign.rs
570 @@ -12,11 +12,12 @@ use std::time::SystemTime;
571
572 use mail_builder::encoders::base64::base64_encode;
573
574- use super::{DkimSigner, Done, Signature};
575+ use super::{canonicalize::CanonicalHeaders, DkimSigner, Done, Signature};
576+
577 use crate::{
578 common::{
579- crypto::{HashContext, SigningKey},
580- headers::{ChainedHeaderIterator, HeaderIterator, HeaderStream},
581+ crypto::SigningKey,
582+ headers::{ChainedHeaderIterator, HeaderIterator, HeaderStream, Writable, Writer},
583 },
584 Error,
585 };
586 @@ -54,13 +55,9 @@ impl<T: SigningKey> DkimSigner<T, Done> {
587 message: impl HeaderStream<'x>,
588 now: u64,
589 ) -> crate::Result<Signature> {
590- let mut body_hasher = self.key.hasher();
591- let mut header_hasher = self.key.hasher();
592-
593 // Canonicalize headers and body
594- let (body_len, signed_headers) =
595- self.template
596- .canonicalize(message, &mut header_hasher, &mut body_hasher);
597+ let (body_len, canonical_headers, signed_headers, canonical_body) =
598+ self.template.canonicalize(message);
599
600 if signed_headers.is_empty() {
601 return Err(Error::NoHeadersFound);
602 @@ -68,7 +65,8 @@ impl<T: SigningKey> DkimSigner<T, Done> {
603
604 // Create Signature
605 let mut signature = self.template.clone();
606- signature.bh = base64_encode(body_hasher.complete().as_ref())?;
607+ let body_hash = self.key.hash(canonical_body);
608+ signature.bh = base64_encode(body_hash.as_ref())?;
609 signature.t = now;
610 signature.x = if signature.x > 0 {
611 now + signature.x
612 @@ -80,11 +78,11 @@ impl<T: SigningKey> DkimSigner<T, Done> {
613 signature.l = body_len as u64;
614 }
615
616- // Add signature to hash
617- signature.write(&mut header_hasher, false);
618-
619 // Sign
620- let b = self.key.sign(header_hasher.complete())?;
621+ let b = self.key.sign(SignableMessage {
622+ headers: canonical_headers,
623+ signature: &signature,
624+ })?;
625
626 // Encode
627 signature.b = base64_encode(&b)?;
628 @@ -93,6 +91,18 @@ impl<T: SigningKey> DkimSigner<T, Done> {
629 }
630 }
631
632+ pub(super) struct SignableMessage<'a> {
633+ headers: CanonicalHeaders<'a>,
634+ signature: &'a Signature,
635+ }
636+
637+ impl<'a> Writable for SignableMessage<'a> {
638+ fn write(self, writer: &mut impl Writer) {
639+ self.headers.write(writer);
640+ self.signature.write(writer, false);
641+ }
642+ }
643+
644 #[cfg(test)]
645 #[allow(unused)]
646 mod test {