viadkim/verifier/mod.rs
1// viadkim – implementation of the DKIM specification
2// Copyright © 2022–2024 David Bürgin <dbuergin@gluet.ch>
3//
4// This program is free software: you can redistribute it and/or modify it under
5// the terms of the GNU General Public License as published by the Free Software
6// Foundation, either version 3 of the License, or (at your option) any later
7// version.
8//
9// This program is distributed in the hope that it will be useful, but WITHOUT
10// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12// details.
13//
14// You should have received a copy of the GNU General Public License along with
15// this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Verifier and supporting types.
18//!
19//! The main API items are [`Verifier`] and convenience function
20//! [`verify`][`verify()`].
21
22mod header;
23mod lookup;
24mod query;
25mod verify;
26
27pub use lookup::LookupTxt;
28
29use crate::{
30 crypto,
31 header::{FieldName, HeaderFields},
32 message_hash::{
33 body_hasher_key, BodyHashError, BodyHashResults, BodyHasher, BodyHasherBuilder,
34 BodyHasherStance,
35 },
36 record::{DkimKeyRecord, DkimKeyRecordError},
37 signature::{DkimSignature, DkimSignatureError, DkimSignatureErrorKind},
38 util::CanonicalStr,
39 verifier::header::{HeaderVerifier, VerifyStatus},
40};
41use std::{
42 error::Error,
43 fmt::{self, Display, Formatter},
44 sync::Arc,
45 time::{Duration, SystemTime},
46};
47use tracing::trace;
48
49/// Configuration for a verifier.
50///
51/// The configuration settings to do with verification policy map to a
52/// [`PolicyError`] variant.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct Config {
55 /// The maximum duration of public key record lookups. When this duration is
56 /// exceeded evaluation fails with a temporary error.
57 ///
58 /// The default is 10 seconds.
59 pub lookup_timeout: Duration,
60
61 /// Only validate at most this number of signatures, any extra signatures
62 /// are ignored. Signatures are selected starting at the top of the header.
63 ///
64 /// The default is 10.
65 pub max_signatures: usize,
66
67 /// If any of the given header names is not included in a DKIM signature’s
68 /// *h=* tag, the signature will not validate. Note that the header *From*
69 /// is always required to be included by RFC 6376 independent of this
70 /// configuration setting.
71 ///
72 /// By default, no additional headers are required to be included in a
73 /// signature.
74 pub headers_required_in_signature: Vec<FieldName>,
75
76 /// If any of the given header names appears in the message header, but not
77 /// all occurrences of like-named headers are included in the DKIM
78 /// signature, then the signature will not validate.
79 ///
80 /// The default includes the *From* header. (This default setting renders
81 /// the attack in RFC 6376, section 8.15 ineffective, as an added *From*
82 /// header would invalidate the signature. In other words it makes the
83 /// common ‘oversigning’ mitigation applied to the *From* header
84 /// unnecessary.)
85 pub headers_forbidden_to_be_unsigned: Vec<FieldName>,
86
87 /// Minimum acceptable key size in bits. This limit is applied to keys that
88 /// provide a key size in
89 /// [`VerifyingKey::key_size`][crate::crypto::VerifyingKey::key_size]
90 /// (currently RSA keys only). When the key size of a verifying key is below
91 /// this value, the signature will not validate.
92 ///
93 /// Note that there is a compile-time hard lower bound of acceptable key
94 /// sizes, which will lead to failure to validate independent of this
95 /// setting. By default, the absolute minimum key size is 1024; when feature
96 /// `pre-rfc8301` is enabled, the absolute minimum key size is 512.
97 ///
98 /// The default is 1024 bits.
99 pub min_key_bits: usize,
100
101 /// When this flag is set, signatures using the SHA-1 hash algorithm are
102 /// acceptable.
103 ///
104 /// Note that the SHA-1 hash algorithm is only available when feature
105 /// `pre-rfc8301` is enabled. This setting is only effective when that
106 /// feature is enabled.
107 ///
108 /// The default is false.
109 pub allow_sha1: bool,
110
111 /// If a DKIM signature has the *l=* tag, and the body length given in this
112 /// tag is less than the actual message body length, the signature will not
113 /// validate. In other words, signatures that cover only part of the message
114 /// body are not accepted.
115 ///
116 /// The default is false.
117 pub forbid_unsigned_content: bool,
118
119 /// When this flag is set, an expired DKIM signature (*x=*) is acceptable.
120 ///
121 /// The default is false.
122 pub allow_expired: bool,
123
124 /// When this flag is set, a DKIM signature with a timestamp in the future
125 /// (*t=*) is acceptable.
126 ///
127 /// The default is false.
128 pub allow_timestamp_in_future: bool,
129
130 /// Tolerance applied to time values when checking signature expiration or
131 /// timestamp validity, to allow for clock drift. Resolution is in seconds.
132 ///
133 /// The default is 5 minutes.
134 pub time_tolerance: Duration,
135
136 /// The `SystemTime` value to use as the instant ‘now’. If `None`, the value
137 /// of `SystemTime::now()` is used for the instant ‘now’.
138 ///
139 /// The default is `None`.
140 pub fixed_system_time: Option<SystemTime>,
141}
142
143impl Config {
144 fn current_timestamp(&self) -> u64 {
145 self.fixed_system_time
146 .unwrap_or_else(SystemTime::now)
147 .duration_since(SystemTime::UNIX_EPOCH)
148 .unwrap_or_default()
149 .as_secs()
150 }
151}
152
153impl Default for Config {
154 fn default() -> Self {
155 Self {
156 lookup_timeout: Duration::from_secs(10),
157 max_signatures: 10,
158 headers_required_in_signature: vec![],
159 headers_forbidden_to_be_unsigned: vec![FieldName::new("From").unwrap()],
160 min_key_bits: 1024,
161 allow_sha1: false,
162 forbid_unsigned_content: false,
163 allow_expired: false,
164 allow_timestamp_in_future: false,
165 time_tolerance: Duration::from_secs(5 * 60),
166 fixed_system_time: None,
167 }
168 }
169}
170
171/// A verification result arrived at for some DKIM signature header.
172#[derive(Clone, Debug, Eq, Hash, PartialEq)]
173pub struct VerificationResult {
174 /// The verification status.
175 pub status: VerificationStatus,
176 /// The index of the evaluated *DKIM-Signature* header in the original
177 /// `HeaderFields` input. This value is unique among the
178 /// `VerificationResult`s returned by a call to [`Verifier::finish`].
179 pub index: usize,
180 /// The parsed DKIM signature data obtained from the *DKIM-Signature*
181 /// header, if available.
182 pub signature: Option<DkimSignature>,
183 /// The parsed DKIM public key record data used in the verification, if
184 /// available.
185 ///
186 /// The record is behind an `Arc` only so that it may be shared among the
187 /// `VerificationResult`s returned by a call to [`Verifier::finish`].
188 pub key_record: Option<Arc<DkimKeyRecord>>,
189}
190
191/// The verification status of an evaluated DKIM signature.
192///
193/// This type encodes the three DKIM output states described in RFC 6376,
194/// section 3.9: `Success` corresponds to *SUCCESS*, `Failure` corresponds to
195/// both *PERMFAIL* and *TEMPFAIL*. Use [`VerificationStatus::to_dkim_result`]
196/// to convert to an RFC 8601 result.
197#[derive(Clone, Debug, Eq, Hash, PartialEq)]
198pub enum VerificationStatus {
199 /// A *SUCCESS* result status.
200 Success,
201 /// A *PERMFAIL* or *TEMPFAIL* result status, with failure cause attached.
202 Failure(VerificationError),
203}
204
205impl VerificationStatus {
206 /// Converts this verification status to an RFC 8601 DKIM result.
207 pub fn to_dkim_result(&self) -> DkimResult {
208 use VerificationError::*;
209
210 match self {
211 Self::Success => DkimResult::Pass,
212 Self::Failure(error) => match error {
213 DkimSignatureFormat(error) => match error.kind {
214 DkimSignatureErrorKind::Utf8Encoding
215 | DkimSignatureErrorKind::TagListFormat
216 | DkimSignatureErrorKind::IncompatibleVersion
217 | DkimSignatureErrorKind::UnsupportedAlgorithm
218 | DkimSignatureErrorKind::InvalidBase64
219 | DkimSignatureErrorKind::UnsupportedCanonicalization
220 | DkimSignatureErrorKind::InvalidDomain
221 | DkimSignatureErrorKind::InvalidSignedHeaderName
222 | DkimSignatureErrorKind::InvalidIdentity
223 | DkimSignatureErrorKind::InvalidBodyLength
224 | DkimSignatureErrorKind::InvalidQueryMethod
225 | DkimSignatureErrorKind::NoSupportedQueryMethods
226 | DkimSignatureErrorKind::InvalidSelector
227 | DkimSignatureErrorKind::InvalidTimestamp
228 | DkimSignatureErrorKind::InvalidExpiration
229 | DkimSignatureErrorKind::InvalidCopiedHeaderField => DkimResult::Neutral,
230 DkimSignatureErrorKind::HistoricAlgorithm
231 | DkimSignatureErrorKind::EmptySignatureTag
232 | DkimSignatureErrorKind::EmptyBodyHashTag
233 | DkimSignatureErrorKind::EmptySignedHeadersTag
234 | DkimSignatureErrorKind::FromHeaderNotSigned
235 | DkimSignatureErrorKind::MissingVersionTag
236 | DkimSignatureErrorKind::MissingAlgorithmTag
237 | DkimSignatureErrorKind::MissingSignatureTag
238 | DkimSignatureErrorKind::MissingBodyHashTag
239 | DkimSignatureErrorKind::MissingDomainTag
240 | DkimSignatureErrorKind::MissingSignedHeadersTag
241 | DkimSignatureErrorKind::MissingSelectorTag
242 | DkimSignatureErrorKind::DomainMismatch
243 | DkimSignatureErrorKind::ExpirationNotAfterTimestamp => DkimResult::Permerror,
244 },
245 Overflow => DkimResult::Neutral,
246 NoKeyFound
247 | InvalidKeyDomain
248 | WrongKeyType
249 | KeyRevoked
250 | DisallowedHashAlgorithm
251 | DomainMismatch
252 | InsufficientContent => DkimResult::Permerror,
253 Timeout | KeyLookup => DkimResult::Temperror,
254 KeyRecordFormat(error) => match error {
255 DkimKeyRecordError::RecordFormat
256 | DkimKeyRecordError::TagListFormat
257 | DkimKeyRecordError::IncompatibleVersion
258 | DkimKeyRecordError::InvalidHashAlgorithm
259 | DkimKeyRecordError::NoSupportedHashAlgorithms
260 | DkimKeyRecordError::UnsupportedKeyType
261 | DkimKeyRecordError::InvalidQuotedPrintable
262 | DkimKeyRecordError::InvalidBase64
263 | DkimKeyRecordError::InvalidServiceType
264 | DkimKeyRecordError::NoSupportedServiceTypes
265 | DkimKeyRecordError::InvalidFlag => DkimResult::Neutral,
266 DkimKeyRecordError::MisplacedVersionTag
267 | DkimKeyRecordError::MissingKeyTag => DkimResult::Permerror,
268 },
269 VerificationFailure(error) => match error {
270 crypto::VerificationError::InvalidKey
271 | crypto::VerificationError::InsufficientKeySize => DkimResult::Permerror,
272 crypto::VerificationError::VerificationFailure => DkimResult::Fail,
273 },
274 BodyHashMismatch => DkimResult::Fail,
275 Policy(_) => DkimResult::Policy,
276 },
277 }
278 }
279}
280
281/// An error that occurs due to a policy violation.
282///
283/// All policy errors can be disabled via [`Config`].
284#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
285pub enum PolicyError {
286 /// A header required to be signed is not included in the signature.
287 ///
288 /// Configurable through [`Config::headers_required_in_signature`].
289 RequiredHeaderNotSigned,
290
291 /// Not all instances of a particular header name included in the signature
292 /// are signed.
293 ///
294 /// Configurable through [`Config::headers_forbidden_to_be_unsigned`].
295 UnsignedHeaderOccurrence,
296
297 /// A signature using the *l=* tag covered only part of the message body.
298 ///
299 /// Configurable through [`Config::forbid_unsigned_content`].
300 UnsignedContent,
301
302 /// Signature is expired.
303 ///
304 /// Configurable through [`Config::allow_expired`].
305 SignatureExpired,
306
307 /// A signature’s timestamp is in the future.
308 ///
309 /// Configurable through [`Config::allow_timestamp_in_future`].
310 TimestampInFuture,
311
312 /// Signature algorithm using SHA-1 hash algorithm.
313 ///
314 /// Configurable through [`Config::allow_sha1`].
315 Sha1HashAlgorithm,
316
317 /// Public key of smaller than acceptable key size.
318 ///
319 /// Configurable through [`Config::min_key_bits`].
320 KeyTooSmall,
321}
322
323impl Display for PolicyError {
324 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
325 match self {
326 Self::RequiredHeaderNotSigned => write!(f, "required header not signed"),
327 Self::UnsignedHeaderOccurrence => write!(f, "unsigned occurrence of signed header"),
328 Self::UnsignedContent => write!(f, "unsigned content in message body"),
329 Self::SignatureExpired => write!(f, "signature expired"),
330 Self::TimestampInFuture => write!(f, "timestamp in future"),
331 Self::Sha1HashAlgorithm => write!(f, "SHA-1 hash algorithm not acceptable"),
332 Self::KeyTooSmall => write!(f, "public key too small"),
333 }
334 }
335}
336
337impl Error for PolicyError {}
338
339/// An error that occurs when performing verification.
340#[derive(Clone, Debug, Eq, Hash, PartialEq)]
341pub enum VerificationError {
342 /// The *DKIM-Signature* header does not fulfil basic format requirements
343 /// (syntax or other).
344 DkimSignatureFormat(DkimSignatureError),
345 /// Conversion from or to a requested integer data type is not supported in
346 /// this implementation or on the current platform.
347 Overflow,
348 /// No public key record was found at the specified location in DNS.
349 NoKeyFound,
350 /// The public key record domain name is syntactically invalid.
351 InvalidKeyDomain,
352 /// The public key record query timed out.
353 Timeout,
354 /// The public key record query failed due to an unspecified DNS error.
355 KeyLookup,
356 /// The DKIM public key record does not fulfil basic format requirements
357 /// (syntax or other).
358 KeyRecordFormat(DkimKeyRecordError),
359 /// The public key record uses a different key type.
360 WrongKeyType,
361 /// The key in the public key record has been revoked.
362 KeyRevoked,
363 /// The used hash algorithm is not allowed according to the public key
364 /// record.
365 DisallowedHashAlgorithm,
366 /// The *i=* and *d=* domains differ while they must be equal according to
367 /// the public key record.
368 DomainMismatch,
369 /// Failure when performing the cryptographic verification operation.
370 VerificationFailure(crypto::VerificationError),
371 /// Less than the number of bytes specified in *l=* were fed to the
372 /// verification process.
373 InsufficientContent,
374 /// The body hash did not match. (Note: implies that signature verification
375 /// was successful.)
376 BodyHashMismatch,
377 /// Failure due to a local policy violation.
378 Policy(PolicyError),
379}
380
381impl Display for VerificationError {
382 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
383 match self {
384 Self::DkimSignatureFormat(_) => write!(f, "unusable DKIM signature header"),
385 Self::Overflow => write!(f, "integer too large"),
386 Self::NoKeyFound => write!(f, "no key record found"),
387 Self::InvalidKeyDomain => write!(f, "invalid key record domain name"),
388 Self::Timeout => write!(f, "key record lookup timed out"),
389 Self::KeyLookup => write!(f, "key record lookup failed"),
390 Self::KeyRecordFormat(_) => write!(f, "unusable public key record"),
391 Self::WrongKeyType => write!(f, "wrong key type in key record"),
392 Self::KeyRevoked => write!(f, "key revoked"),
393 Self::DisallowedHashAlgorithm => write!(f, "hash algorithm disallowed in key record"),
394 Self::DomainMismatch => write!(f, "domain mismatch"),
395 Self::VerificationFailure(_) => write!(f, "signature verification failed"),
396 Self::InsufficientContent => write!(f, "not enough message body content"),
397 Self::BodyHashMismatch => write!(f, "body hash did not verify"),
398 Self::Policy(_) => write!(f, "local policy violation"),
399 }
400 }
401}
402
403impl Error for VerificationError {
404 fn source(&self) -> Option<&(dyn Error + 'static)> {
405 match self {
406 Self::Overflow
407 | Self::NoKeyFound
408 | Self::InvalidKeyDomain
409 | Self::Timeout
410 | Self::KeyLookup
411 | Self::WrongKeyType
412 | Self::KeyRevoked
413 | Self::DisallowedHashAlgorithm
414 | Self::DomainMismatch
415 | Self::InsufficientContent
416 | Self::BodyHashMismatch => None,
417 Self::DkimSignatureFormat(error) => Some(error),
418 Self::KeyRecordFormat(error) => Some(error),
419 Self::VerificationFailure(error) => Some(error),
420 Self::Policy(error) => Some(error),
421 }
422 }
423}
424
425/// An RFC 8601 DKIM result.
426///
427/// The mapping of an RFC 6376 *SUCCESS*, *PERMFAIL*, or *TEMPFAIL* result to an
428/// [RFC 8601] DKIM result is not well defined. Our interpretation of each
429/// result is given in detail below.
430///
431/// As a general rule, of the error results `Neutral` is the early bail-out
432/// error result, which signals that verification didn’t proceed past a basic
433/// attempt at parsing a DKIM signature (or DKIM public key record), while
434/// `Fail`, `Policy`, `Temperror`, and `Permerror` are error results that are
435/// used for more concrete problems concerning a well-understood DKIM signature
436/// (or DKIM public key record).
437///
438/// [RFC 8601]: https://www.rfc-editor.org/rfc/rfc8601
439#[derive(Clone, Copy, Eq, Hash, PartialEq)]
440pub enum DkimResult {
441 /// The *none* result. This result indicates that a message was not signed.
442 /// (Not used in this library.)
443 None,
444
445 /// The *pass* result. This result means that verification could be
446 /// performed on a DKIM signature, and the verification was successful.
447 Pass,
448
449 /// The *fail* result. This result means that a DKIM signature was
450 /// understood and verification could be performed, and the verification
451 /// result was failure.
452 ///
453 /// Examples include: cryptographic verification failure, body hash
454 /// mismatch.
455 Fail,
456
457 /// The *policy* result. This result means that a DKIM signature could not
458 /// be or was not verified, because some aspect of it was unacceptable due
459 /// to a configurable policy reason.
460 ///
461 /// Examples include: signature expired, configuration required a header to
462 /// be signed, but it wasn’t.
463 Policy,
464
465 /// The *neutral* result. This result means that a DKIM signature could not
466 /// be entirely understood or cannot be processed by this implementation
467 /// (but might be by other implementations).
468 ///
469 /// Examples include: syntax errors, an unsupported cryptographic or other
470 /// algorithm.
471 Neutral,
472
473 /// The *temperror* result. This result means that signature evaluation
474 /// could not be performed due to a temporary reason. Retrying evaluation
475 /// might produce a different, definitive result.
476 ///
477 /// Examples include: DNS lookup timeout.
478 Temperror,
479
480 /// The *permerror* result. This result means that a DKIM signature was
481 /// determined to be definitely broken or not verifiable. The problem with
482 /// the signature is understood, is permanent, and the signature must be
483 /// rejected (by this and any other implementation).
484 ///
485 /// Examples include: missing required tag in signature, missing public key
486 /// record in DNS, *l=* tag larger than message body length.
487 Permerror,
488}
489
490impl CanonicalStr for DkimResult {
491 fn canonical_str(&self) -> &'static str {
492 match self {
493 Self::None => "none",
494 Self::Pass => "pass",
495 Self::Fail => "fail",
496 Self::Policy => "policy",
497 Self::Neutral => "neutral",
498 Self::Temperror => "temperror",
499 Self::Permerror => "permerror",
500 }
501 }
502}
503
504impl Display for DkimResult {
505 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
506 f.write_str(self.canonical_str())
507 }
508}
509
510impl fmt::Debug for DkimResult {
511 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
512 write!(f, "{self}")
513 }
514}
515
516struct VerifierTask {
517 status: VerificationStatus,
518 index: usize,
519 signature: Option<DkimSignature>,
520 key_record: Option<Arc<DkimKeyRecord>>,
521}
522
523/// A verifier of DKIM signatures in an email message.
524///
525/// `Verifier` is the high-level API for verifying a message. It implements a
526/// three-phase, staged design that allows processing the message in chunks, and
527/// shortcutting unnecessary body processing.
528///
529/// 1. **[`verify_header`][Verifier::verify_header]** (async): first, perform
530/// signature verification on the message header and return a verifier that
531/// carries the preliminary results; this is where most of the actual work is
532/// done
533/// 2. [`process_body_chunk`][Verifier::process_body_chunk]: then, any number of
534/// chunks of the message body are fed to the verification process
535/// 3. [`finish`][Verifier::finish]: finally, the body hashes are computed and
536/// the final verification results are returned
537///
538/// Compare this with the similar but distinct procedure of
539/// [`Signer`][crate::signer::Signer].
540///
541/// For convenience, the free function [`verify`][`verify()`] can be used to
542/// perform all stages in one go.
543///
544/// # Examples
545///
546/// The following example shows how to verify a message’s signatures using the
547/// high-level API.
548///
549/// ```
550/// # use std::{future::Future, io::{self, ErrorKind}, pin::Pin, time::{Duration, SystemTime}};
551/// # use tokio::runtime::Builder;
552/// #
553/// # #[derive(Clone)]
554/// # struct MockLookupTxt;
555/// #
556/// # impl viadkim::verifier::LookupTxt for MockLookupTxt {
557/// # type Answer = Vec<io::Result<Vec<u8>>>;
558/// # type Query<'a> = Pin<Box<dyn Future<Output = io::Result<Self::Answer>> + Send + 'a>>;
559/// #
560/// # fn lookup_txt(&self, domain: &str) -> Self::Query<'_> {
561/// # let domain = domain.to_owned();
562/// # Box::pin(async move {
563/// # match domain.as_str() {
564/// # "selector._domainkey.example.com." => {
565/// # Ok(vec![
566/// # Ok(b"v=DKIM1; k=ed25519; p=f8IRGiRaCQ83GCI56F77ueW0l5hinwOG31ZmlSyReBk=".to_vec()),
567/// # ])
568/// # }
569/// # _ => unimplemented!(),
570/// # }
571/// # })
572/// # }
573/// # }
574/// #
575/// # let rt = Builder::new_current_thread().enable_all().build().unwrap();
576/// # rt.block_on(async {
577/// use viadkim::*;
578///
579/// let header =
580/// "DKIM-Signature: v=1; d=example.com; s=selector; a=ed25519-sha256; c=relaxed;\r\n\
581/// \tt=1687435395; x=1687867395; h=Date:Subject:To:From; bh=1zGfaauQ3vmMhm21CGMC23\r\n\
582/// \taJE1JrOoKsgT/wvw9owzE=; b=neMHc/e6jrqSscL1pc/fTxOU/CjuvYzvnGbTABQvYkzlIvazqp3\r\n\
583/// \tiR7RXUZi0CbOAq13IEUZPc6S0/63cfAO4CA==\r\n\
584/// Received: from submit.example.com by mail.example.com\r\n\
585/// \twith ESMTPSA id A6DE7475; Thu, 22 Jun 2023 14:03:14 +0200\r\n\
586/// From: me@example.com\r\n\
587/// To: you@example.org\r\n\
588/// Subject: Re: Thursday 8pm\r\n\
589/// Date: Thu, 22 Jun 2023 14:03:12 +0200\r\n".parse()?;
590/// let body = b"Hey,\r\n\
591/// \r\n\
592/// Ready for tonight? ;)\r\n";
593///
594/// // Note: Enable Cargo feature `hickory-resolver` to make an implementation
595/// // of trait `LookupTxt` available for Hickory DNS’s `TokioAsyncResolver`.
596/// let resolver /* = TokioAsyncResolver::tokio(...) */;
597/// # resolver = MockLookupTxt;
598///
599/// let config = Config::default();
600/// # let mut config = config;
601/// # config.fixed_system_time = Some(SystemTime::UNIX_EPOCH + Duration::from_secs(1687435411));
602///
603/// let mut verifier = Verifier::verify_header(&resolver, &header, &config)
604/// .await
605/// .unwrap();
606///
607/// let _ = verifier.process_body_chunk(body);
608///
609/// let results = verifier.finish();
610///
611/// let signature = results.into_iter().next().unwrap();
612///
613/// assert_eq!(signature.status, VerificationStatus::Success);
614/// assert_eq!(signature.status.to_dkim_result(), DkimResult::Pass);
615/// # Ok::<_, Box<dyn std::error::Error>>(())
616/// # }).unwrap();
617/// ```
618///
619/// See [`Signer`][crate::signer::Signer] for how the above example message was
620/// signed.
621pub struct Verifier {
622 tasks: Vec<VerifierTask>, // non-empty
623 body_hasher: BodyHasher,
624}
625
626impl Verifier {
627 /// Initiates a message verification process by verifying the header of a
628 /// message. Returns a verifier for all signatures in the given header, or
629 /// `None` if the header contains no signatures.
630 ///
631 /// The `resolver` parameter is a reference to a type that implements
632 /// [`LookupTxt`]; the trait `LookupTxt` is an abstraction for DNS
633 /// resolution. The parameter is also `Clone`, in order to share the
634 /// resolver among concurrent key record lookup tasks.
635 pub async fn verify_header<T>(
636 resolver: &T,
637 headers: &HeaderFields,
638 config: &Config,
639 ) -> Option<Self>
640 where
641 T: LookupTxt + Clone + 'static,
642 {
643 let verifier = HeaderVerifier::find_signatures(headers, config)?;
644
645 let verified_tasks = verifier.verify_all(resolver).await;
646
647 let mut tasks = vec![];
648 let mut body_hasher = BodyHasherBuilder::new(config.forbid_unsigned_content);
649
650 for task in verified_tasks {
651 let status = match task.status {
652 VerifyStatus::InProgress => panic!("verification unexpectedly skipped"),
653 VerifyStatus::Failed(e) => VerificationStatus::Failure(e),
654 VerifyStatus::Successful => {
655 // For successfully verified signatures, register a body
656 // hasher request for verification of the body hash.
657 let sig = task.signature.as_ref().unwrap();
658 let (body_len, hash_alg, canon_alg) = body_hasher_key(sig);
659 body_hasher.register_canonicalization(body_len, hash_alg, canon_alg);
660
661 // Mark this task as a (preliminary) success, later body
662 // hash verification can still result in failure.
663 VerificationStatus::Success
664 }
665 };
666
667 tasks.push(VerifierTask {
668 status,
669 index: task.index,
670 signature: task.signature,
671 key_record: task.key_record,
672 });
673 }
674
675 let body_hasher = body_hasher.build();
676
677 Some(Self { tasks, body_hasher })
678 }
679
680 /// Processes a chunk of the message body.
681 ///
682 /// Clients should pass the message body either whole or in chunks of
683 /// arbitrary size to this method in order to calculate the body hash (the
684 /// *bh=* tag). The returned [`BodyHasherStance`] instructs the client how
685 /// to proceed if more chunks are outstanding. Note that the given body
686 /// chunk is canonicalised and hashed, but not otherwise retained in memory.
687 ///
688 /// Remember that email message bodies generally use CRLF line endings; this
689 /// is important for correct body hash calculation.
690 ///
691 /// # Examples
692 ///
693 /// ```
694 /// # use viadkim::verifier::Verifier;
695 /// # fn f(verifier: &mut Verifier) {
696 /// let _ = verifier.process_body_chunk(b"\
697 /// Hello friend!\r
698 /// \r
699 /// How are you?\r
700 /// ");
701 /// # }
702 /// ```
703 pub fn process_body_chunk(&mut self, chunk: &[u8]) -> BodyHasherStance {
704 self.body_hasher.hash_chunk(chunk)
705 }
706
707 /// Finishes the verification process and returns the results.
708 ///
709 /// The returned result vector is never empty.
710 pub fn finish(self) -> Vec<VerificationResult> {
711 let bh_results = self.body_hasher.finish();
712
713 let mut result = vec![];
714
715 for task in self.tasks {
716 // To obtain the final VerificationStatus, those tasks that did
717 // verify successfully, now must have their body hashes verify, too.
718 let final_status = match task.status {
719 VerificationStatus::Success => {
720 let sig = task.signature.as_ref()
721 .expect("successful verification missing signature");
722 verify_body_hash(sig, &bh_results)
723 }
724 status @ VerificationStatus::Failure(_) => status,
725 };
726
727 result.push(VerificationResult {
728 status: final_status,
729 index: task.index,
730 signature: task.signature,
731 key_record: task.key_record,
732 });
733 }
734
735 result
736 }
737}
738
739fn verify_body_hash(sig: &DkimSignature, bh_results: &BodyHashResults) -> VerificationStatus {
740 trace!(domain = %sig.domain, selector = %sig.selector, "checking body hash for signature");
741
742 let key = body_hasher_key(sig);
743
744 let bh_result = bh_results.get(&key)
745 .expect("requested body hash result not available");
746
747 match bh_result {
748 Ok((h, _)) => {
749 if h == &sig.body_hash {
750 trace!("body hash matched");
751 VerificationStatus::Success
752 } else {
753 trace!("body hash did not match");
754 VerificationStatus::Failure(VerificationError::BodyHashMismatch)
755 }
756 }
757 Err(BodyHashError::InsufficientInput) => {
758 trace!("insufficient message body content for body hash");
759 VerificationStatus::Failure(VerificationError::InsufficientContent)
760 }
761 Err(BodyHashError::InputTruncated) => {
762 trace!("unsigned content in message body not allowed due to local policy");
763 VerificationStatus::Failure(VerificationError::Policy(PolicyError::UnsignedContent))
764 }
765 }
766}
767
768/// Verifies the DKIM signatures in an email message.
769///
770/// This is a convenience function around a common usage pattern of
771/// [`Verifier`], when the whole message body is available as a byte slice. Use
772/// `Verifier` to process the message body in chunks, which allows shortcutting
773/// unnecessary body processing. Unlike in `Verifier::finish`, the returned
774/// vector is empty if the header contains no signatures.
775///
776/// See the example for `Verifier` for basic usage.
777pub async fn verify<T>(
778 resolver: &T,
779 header: &HeaderFields,
780 body: &[u8],
781 config: &Config,
782) -> Vec<VerificationResult>
783where
784 T: LookupTxt + Clone + 'static,
785{
786 let mut verifier = match Verifier::verify_header(resolver, header, config).await {
787 Some(verifier) => verifier,
788 None => return vec![],
789 };
790
791 let _ = verifier.process_body_chunk(body);
792
793 verifier.finish()
794}