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