mail_auth/dkim2/output.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//! Verification results: [`Dkim2Result`], [`Dkim2Output`] and [`ChainLink`].
8
9use super::{Flag, MessageInstance, Signature};
10use crate::{AuthenticatedMessage, DnsError, Error, headers::Header};
11
12/// Outcome of a DKIM2 verification (§14.1).
13///
14/// The error carried by the failure variants is usually an [`Error::Dkim2`]
15/// naming the check that failed.
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub enum Dkim2Result {
18 /// The message is a Validly Signed DKIM2 Message: every signature
19 /// verified, every hash matched, the most recent signature matches the
20 /// SMTP envelope, the chain of custody is unbroken and every `From`
21 /// header field is Authentic. For a provisional verification, the message
22 /// is Provisionally Verified (§13.1).
23 Pass,
24 /// The chain could be evaluated but a signature or a hash was not
25 /// correct.
26 Fail(crate::Error),
27 /// The chain cannot be verified because of an unrecoverable problem, such
28 /// as a malformed or missing header field or tag, a gap in the
29 /// numbering, an envelope or chain of custody mismatch, a timestamp out
30 /// of range, a missing, invalid or revoked public key, or a `From` header
31 /// field that is not Authentic.
32 PermError(crate::Error),
33 /// A public key could not be fetched because of a temporary DNS error; a
34 /// later attempt may succeed.
35 TempError(crate::Error),
36 /// The message has no `DKIM2-Signature` and no `Message-Instance` header
37 /// field, so there was nothing to verify. This follows the `none` result
38 /// of DKIM and ARC; §14.2 itself has no such state.
39 None,
40}
41
42impl From<Error> for Dkim2Result {
43 fn from(err: Error) -> Self {
44 if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
45 Dkim2Result::TempError(err)
46 } else {
47 Dkim2Result::PermError(err)
48 }
49 }
50}
51
52/// Which checks a DKIM2 verification performed.
53#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
54pub enum Verification {
55 /// Every check of §14: the message is a Validly Signed DKIM2 Message
56 /// when the result is [`Dkim2Result::Pass`].
57 #[default]
58 Full,
59 /// Only the checks for a Provisionally Verified message (§13.1): header
60 /// field validity, timestamps, the envelope against the most recent
61 /// signature, that signature and the hashes of the most recent
62 /// `Message-Instance`.
63 Provisional,
64}
65
66/// Result of [`MessageAuthenticator::verify_dkim2`](crate::MessageAuthenticator::verify_dkim2)
67/// and [`MessageAuthenticator::verify_dkim2_provisionally`](crate::MessageAuthenticator::verify_dkim2_provisionally).
68///
69/// Holds the overall [`Dkim2Result`] and, when the chain passes, one
70/// [`ChainLink`] per signature. The links borrow from the verified
71/// [`AuthenticatedMessage`](crate::AuthenticatedMessage). A passing full
72/// verification is the proof of validity that
73/// [`Dkim2Signer`](super::Dkim2Signer) requires to sign a message that
74/// already carries DKIM2 header fields (§12).
75#[derive(Debug, PartialEq, Eq, Clone)]
76pub struct Dkim2Output<'x> {
77 pub(crate) result: Dkim2Result,
78 pub(crate) chain: Vec<ChainLink<'x>>,
79 pub(crate) verification: Verification,
80 pub(crate) raw_message: &'x [u8],
81 pub(crate) signature_fields: &'x [Header<'x, Signature>],
82 pub(crate) instance_fields: &'x [Header<'x, MessageInstance>],
83}
84
85/// One verified hop of a DKIM2 chain, in ascending `i=` order.
86#[derive(Debug, PartialEq, Eq, Clone)]
87pub struct ChainLink<'x> {
88 /// The `DKIM2-Signature` of this hop.
89 pub signature: &'x Signature,
90 /// The `Message-Instance` whose `m=` equals the signature's `m=`, if
91 /// present.
92 pub instance: Option<&'x MessageInstance>,
93 /// Verification result of this hop.
94 pub result: Dkim2Result,
95 /// Whether this hop continues the chain of custody of the previous one.
96 pub custody_ok: bool,
97}
98
99impl<'x> Dkim2Output<'x> {
100 /// Returns the overall verification result.
101 pub fn result(&self) -> &Dkim2Result {
102 &self.result
103 }
104
105 /// Returns which checks were performed.
106 pub fn verification(&self) -> Verification {
107 self.verification
108 }
109
110 /// Returns the verified hops in ascending `i=` order. Empty unless
111 /// [`result`](Self::result) is [`Dkim2Result::Pass`]; after a
112 /// provisional verification it holds only the most recent signature.
113 pub fn chain(&self) -> &[ChainLink<'x>] {
114 &self.chain
115 }
116
117 /// Returns the error carried by a `Fail`, `PermError` or `TempError`
118 /// result, or `None` for `Pass` and `None`.
119 pub fn error(&self) -> Option<&crate::Error> {
120 match &self.result {
121 Dkim2Result::Fail(err) | Dkim2Result::PermError(err) | Dkim2Result::TempError(err) => {
122 Some(err)
123 }
124 Dkim2Result::Pass | Dkim2Result::None => None,
125 }
126 }
127
128 /// Returns true if any signer in the verified chain requested feedback
129 /// with the `feedback` flag (§10.10).
130 pub fn feedback_requested(&self) -> bool {
131 self.chain
132 .iter()
133 .any(|link| link.signature.flags.contains(&Flag::Feedback))
134 }
135
136 /// Returns the signing domains (`d=`) of the signatures that set the
137 /// `feedback` flag, in ascending `i=` order.
138 pub fn feedback_domains(&self) -> impl Iterator<Item = &str> {
139 self.chain
140 .iter()
141 .filter(|link| link.signature.flags.contains(&Flag::Feedback))
142 .map(|link| link.signature.d.as_str())
143 }
144
145 /// Returns the signing domain of the most recent hop that set the
146 /// `feedhere` flag, or `None` if no hop did.
147 ///
148 /// A privacy-conscious Forwarder sets `feedhere` so that feedback is
149 /// relayed through it instead of being sent directly to the requestor
150 /// (§10.10).
151 pub fn feedback_relay(&self) -> Option<&str> {
152 self.chain
153 .iter()
154 .filter(|link| link.signature.flags.contains(&Flag::FeedHere))
155 .max_by_key(|link| link.signature.i)
156 .map(|link| link.signature.d.as_str())
157 }
158
159 pub(crate) fn is_full_pass(&self) -> bool {
160 matches!(self.result, Dkim2Result::Pass) && self.verification == Verification::Full
161 }
162
163 pub(crate) fn is_proof_for(&self, message: &AuthenticatedMessage<'_>) -> bool {
164 self.is_full_pass()
165 && !message.has_dkim2_errors
166 && same_values(self.signature_fields, &message.dkim2_signatures)
167 && same_values(self.instance_fields, &message.dkim2_instances)
168 }
169
170 pub(crate) fn covers(&self, raw_message: &[u8]) -> bool {
171 !self.raw_message.is_empty() && std::ptr::eq(self.raw_message, raw_message)
172 }
173}
174
175fn same_values<T>(proof: &[Header<'_, T>], message: &[Header<'_, T>]) -> bool {
176 proof.len() == message.len()
177 && proof
178 .iter()
179 .zip(message)
180 .all(|(proof, message)| proof.value == message.value)
181}
182
183impl From<Dkim2Result> for Dkim2Output<'_> {
184 fn from(result: Dkim2Result) -> Self {
185 Dkim2Output {
186 result,
187 chain: Vec::new(),
188 verification: Verification::Full,
189 raw_message: &[],
190 signature_fields: &[],
191 instance_fields: &[],
192 }
193 }
194}
195
196#[cfg(test)]
197mod test {
198 use super::*;
199 use crate::dkim2::{Flag, Signature};
200
201 fn signed(i: u32, domain: &str, flags: Vec<Flag>) -> Signature {
202 Signature {
203 i,
204 d: domain.to_string(),
205 flags,
206 ..Default::default()
207 }
208 }
209
210 #[test]
211 fn feedback_accessors() {
212 let sig1 = signed(1, "a.example", vec![Flag::Feedback]);
213 let sig2 = signed(2, "b.example", vec![Flag::Feedback, Flag::FeedHere]);
214 let link = |signature| ChainLink {
215 signature,
216 instance: None,
217 result: Dkim2Result::Pass,
218 custody_ok: true,
219 };
220 let output = Dkim2Output {
221 chain: vec![link(&sig1), link(&sig2)],
222 ..Dkim2Output::from(Dkim2Result::Pass)
223 };
224
225 assert!(output.feedback_requested());
226 assert_eq!(
227 output.feedback_domains().collect::<Vec<_>>(),
228 vec!["a.example", "b.example"]
229 );
230 assert_eq!(output.feedback_relay(), Some("b.example"));
231
232 let none = Dkim2Output::from(Dkim2Result::Pass);
233 assert!(!none.feedback_requested());
234 assert!(none.feedback_domains().next().is_none());
235 assert_eq!(none.feedback_relay(), None);
236 }
237}