Skip to main content

mail_auth/dkim2/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use crate::{
8    Dkim2Result,
9    common::crypto::{Algorithm, DkimKey, HashAlgorithm},
10};
11
12pub mod builder;
13pub mod canonicalize;
14pub mod dsn;
15pub mod headers;
16pub mod parse;
17pub mod recipe;
18mod recipe_serializer;
19pub mod sign;
20pub mod verify;
21
22#[cfg(test)]
23mod interop_test;
24
25pub use dsn::{Dkim2Dsn, Dkim2DsnFailure, Dkim2DsnOutput};
26pub use recipe::{BodyRecipe, HeaderRecipe, Recipe, Step};
27pub use sign::{Dkim2Signed, Envelope, Hop};
28
29pub struct NeedDomain;
30pub struct NeedSelector;
31pub struct Done;
32
33pub struct Dkim2Signer<State = NeedDomain> {
34    _state: std::marker::PhantomData<State>,
35    pub keys: Vec<KeyEntry>,
36    pub domain: String,
37    pub flags: Vec<Flag>,
38    pub nonce: Option<String>,
39}
40
41pub struct KeyEntry {
42    pub key: DkimKey,
43    pub selector: String,
44}
45
46#[derive(Debug, PartialEq, Eq, Clone, Default)]
47pub struct Signature {
48    pub i: u32,
49    pub m: u32,
50    pub t: u64,
51    pub d: String,
52    pub s: Vec<SignatureValue>,
53    pub chain: ChainBinding,
54    pub n: Option<String>,
55    pub flags: Vec<Flag>,
56}
57
58#[derive(Debug, PartialEq, Eq, Clone)]
59pub struct SignatureValue {
60    pub selector: String,
61    pub a: Algorithm,
62    pub b: Vec<u8>,
63}
64
65#[derive(Debug, PartialEq, Eq, Clone)]
66pub enum ChainBinding {
67    Envelope {
68        mail_from: String,
69        rcpt_to: Vec<String>,
70    },
71    NextDomain(String),
72}
73
74impl Default for ChainBinding {
75    fn default() -> Self {
76        ChainBinding::Envelope {
77            mail_from: String::new(),
78            rcpt_to: Vec::new(),
79        }
80    }
81}
82
83#[derive(Debug, PartialEq, Eq, Clone)]
84pub enum Flag {
85    DoNotModify,
86    DoNotExplode,
87    Feedback,
88    FeedHere,
89    Exploded,
90    Unknown(String),
91}
92
93impl Flag {
94    pub fn as_bytes(&self) -> &[u8] {
95        match self {
96            Flag::DoNotModify => b"donotmodify",
97            Flag::DoNotExplode => b"donotexplode",
98            Flag::Feedback => b"feedback",
99            Flag::FeedHere => b"feedhere",
100            Flag::Exploded => b"exploded",
101            Flag::Unknown(value) => value.as_bytes(),
102        }
103    }
104}
105
106#[derive(Debug, PartialEq, Eq, Clone, Default)]
107pub struct MessageInstance {
108    pub m: u32,
109    pub hashes: Vec<MessageHash>,
110    pub recipe: Option<Recipe>,
111}
112
113#[derive(Debug, PartialEq, Eq, Clone)]
114pub struct MessageHash {
115    pub name: Option<HashAlgorithm>,
116    pub header_hash: Vec<u8>,
117    pub body_hash: Vec<u8>,
118}
119
120#[derive(Debug, PartialEq, Eq, Clone)]
121pub struct Dkim2Output<'x> {
122    pub(crate) result: Dkim2Result,
123    pub(crate) chain: Vec<ChainLink<'x>>,
124}
125
126#[derive(Debug, PartialEq, Eq, Clone)]
127pub struct ChainLink<'x> {
128    pub signature: &'x Signature,
129    pub instance: Option<&'x MessageInstance>,
130    pub result: Dkim2Result,
131    pub custody_ok: bool,
132}
133
134impl<'x> Dkim2Output<'x> {
135    pub fn result(&self) -> &Dkim2Result {
136        &self.result
137    }
138
139    pub fn chain(&self) -> &[ChainLink<'x>] {
140        &self.chain
141    }
142
143    pub fn error(&self) -> Option<&crate::Error> {
144        match &self.result {
145            Dkim2Result::Fail(err) | Dkim2Result::PermError(err) | Dkim2Result::TempError(err) => {
146                Some(err)
147            }
148            Dkim2Result::Pass | Dkim2Result::None => None,
149        }
150    }
151
152    pub fn failure_reason(&self) -> Option<String> {
153        self.error().map(|err| err.to_string())
154    }
155
156    /// Returns true if any signer in the verified chain requested feedback
157    /// (the `feedback` flag, draft-ietf-dkim-dkim2-spec-03 §8.10).
158    pub fn feedback_requested(&self) -> bool {
159        self.chain
160            .iter()
161            .any(|link| link.signature.flags.contains(&Flag::Feedback))
162    }
163
164    /// Domains (`d=`) of the signatures that requested feedback.
165    pub fn feedback_domains(&self) -> Vec<&str> {
166        self.chain
167            .iter()
168            .filter(|link| link.signature.flags.contains(&Flag::Feedback))
169            .map(|link| link.signature.d.as_str())
170            .collect()
171    }
172
173    /// If a privacy-conscious Forwarder set the `feedhere` flag, returns the
174    /// domain of the most recent such hop, via which feedback should be relayed
175    /// instead of being sent directly to the requestor (§8.10). Otherwise `None`.
176    pub fn feedback_relay(&self) -> Option<&str> {
177        self.chain
178            .iter()
179            .filter(|link| link.signature.flags.contains(&Flag::FeedHere))
180            .max_by_key(|link| link.signature.i)
181            .map(|link| link.signature.d.as_str())
182    }
183}
184
185impl From<Dkim2Result> for Dkim2Output<'_> {
186    fn from(result: Dkim2Result) -> Self {
187        Dkim2Output {
188            result,
189            chain: Vec::new(),
190        }
191    }
192}
193
194impl Signature {
195    pub fn domain(&self) -> &str {
196        &self.d
197    }
198}
199
200#[derive(Debug, PartialEq, Eq, Clone)]
201pub enum Dkim2Error {
202    InstanceMissing(u32),
203    InstanceSyntax(u32),
204    InstanceTagMissing { m: u32, tag: &'static str },
205    InstanceNotSigned(u32),
206    InstanceAboveSignature(u32),
207    SignatureMissing(u32),
208    SignatureSyntax(u32),
209    SignatureTagMissing { i: u32, tag: &'static str },
210    SignatureTagUnexpected { i: u32, tag: &'static str },
211    SequenceGap,
212    SequenceOverflow,
213    SignatureExpired(u32),
214    MailFromMismatch(u32),
215    RcptToMismatch(u32),
216    MailFromDomainMismatch(u32),
217    NextDomainMismatch(u32),
218    PublicKeyFetch(u32),
219    PublicKeyMissing(u32),
220    PublicKeyMultiple(u32),
221    PublicKeySyntax(u32),
222    PublicKeyAlgorithmMismatch(u32),
223    PublicKeyRevoked(u32),
224    IncorrectSignature(u32),
225    NoValidAlgorithm(u32),
226    HeaderHashMismatch(u32),
227    BodyHashMismatch(u32),
228    Modified,
229    Exploded,
230}
231
232impl std::fmt::Display for Dkim2Error {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            Dkim2Error::InstanceMissing(m) => write!(f, "Message-Instance m={m} missing"),
236            Dkim2Error::InstanceSyntax(m) => write!(f, "Message-Instance m={m} syntax error"),
237            Dkim2Error::InstanceTagMissing { m, tag } => {
238                write!(f, "Message-Instance m={m} tag={tag} missing")
239            }
240            Dkim2Error::InstanceNotSigned(m) => write!(f, "Message-Instance m={m} is not signed"),
241            Dkim2Error::InstanceAboveSignature(m) => {
242                write!(
243                    f,
244                    "Message-Instance m={m} is higher than any DKIM2-Signature"
245                )
246            }
247            Dkim2Error::SignatureMissing(i) => write!(f, "DKIM2-Signature i={i} missing"),
248            Dkim2Error::SignatureSyntax(i) => write!(f, "DKIM2-Signature i={i} syntax error"),
249            Dkim2Error::SignatureTagMissing { i, tag } => {
250                write!(f, "DKIM2-Signature i={i} tag={tag} missing")
251            }
252            Dkim2Error::SignatureTagUnexpected { i, tag } => {
253                write!(f, "DKIM2-Signature i={i} tag={tag} was unexpected")
254            }
255            Dkim2Error::SequenceGap => write!(f, "DKIM2 sequence numbering has a gap"),
256            Dkim2Error::SequenceOverflow => {
257                write!(f, "DKIM2 sequence numbering would overflow")
258            }
259            Dkim2Error::SignatureExpired(i) => write!(f, "DKIM2-Signature i={i} signature expired"),
260            Dkim2Error::MailFromMismatch(i) => {
261                write!(f, "DKIM2-Signature i={i} MAIL FROM did not match")
262            }
263            Dkim2Error::RcptToMismatch(i) => {
264                write!(f, "DKIM2-Signature i={i} RCPT TO did not match")
265            }
266            Dkim2Error::MailFromDomainMismatch(i) => {
267                write!(f, "DKIM2-Signature i={i} MAIL FROM and d= do not match")
268            }
269            Dkim2Error::NextDomainMismatch(i) => {
270                write!(f, "DKIM2-Signature i={i} nd= does not match")
271            }
272            Dkim2Error::PublicKeyFetch(i) => {
273                write!(f, "DKIM2-Signature i={i} public key could not be fetched")
274            }
275            Dkim2Error::PublicKeyMissing(i) => {
276                write!(f, "DKIM2-Signature i={i} public key does not exist")
277            }
278            Dkim2Error::PublicKeyMultiple(i) => {
279                write!(f, "DKIM2-Signature i={i} public key has multiple records")
280            }
281            Dkim2Error::PublicKeySyntax(i) => {
282                write!(f, "DKIM2-Signature i={i} public key has a syntax error")
283            }
284            Dkim2Error::PublicKeyAlgorithmMismatch(i) => {
285                write!(f, "DKIM2-Signature i={i} public key algorithm mismatch")
286            }
287            Dkim2Error::PublicKeyRevoked(i) => {
288                write!(f, "DKIM2-Signature i={i} public key has been revoked")
289            }
290            Dkim2Error::IncorrectSignature(i) => {
291                write!(f, "DKIM2-Signature i={i} incorrect signature")
292            }
293            Dkim2Error::NoValidAlgorithm(i) => {
294                write!(f, "DKIM2-Signature i={i} has no valid signature algorithms")
295            }
296            Dkim2Error::HeaderHashMismatch(m) => {
297                write!(f, "Message-Instance m={m} header hash mismatch")
298            }
299            Dkim2Error::BodyHashMismatch(m) => {
300                write!(f, "Message-Instance m={m} body hash mismatch")
301            }
302            Dkim2Error::Modified => {
303                write!(f, "Message has been modified despite a donotmodify request")
304            }
305            Dkim2Error::Exploded => {
306                write!(
307                    f,
308                    "Message has been exploded despite a donotexplode request"
309                )
310            }
311        }
312    }
313}
314
315#[cfg(test)]
316mod test {
317    use super::*;
318
319    fn signed(i: u32, domain: &str, flags: Vec<Flag>) -> Signature {
320        Signature {
321            i,
322            d: domain.to_string(),
323            flags,
324            ..Default::default()
325        }
326    }
327
328    #[test]
329    fn feedback_accessors() {
330        let sig1 = signed(1, "a.example", vec![Flag::Feedback]);
331        let sig2 = signed(2, "b.example", vec![Flag::Feedback, Flag::FeedHere]);
332        let link = |signature| ChainLink {
333            signature,
334            instance: None,
335            result: Dkim2Result::Pass,
336            custody_ok: true,
337        };
338        let output = Dkim2Output {
339            result: Dkim2Result::Pass,
340            chain: vec![link(&sig1), link(&sig2)],
341        };
342
343        assert!(output.feedback_requested());
344        assert_eq!(output.feedback_domains(), vec!["a.example", "b.example"]);
345        assert_eq!(output.feedback_relay(), Some("b.example"));
346
347        let none = Dkim2Output::from(Dkim2Result::Pass);
348        assert!(!none.feedback_requested());
349        assert!(none.feedback_domains().is_empty());
350        assert_eq!(none.feedback_relay(), None);
351    }
352}