Skip to main content

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}