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    ChainTooLong,
214    SignatureExpired(u32),
215    MailFromMismatch(u32),
216    RcptToMismatch(u32),
217    MailFromDomainMismatch(u32),
218    NextDomainMismatch(u32),
219    CustodyBreak(u32),
220    PublicKeyFetch(u32),
221    PublicKeyMissing(u32),
222    PublicKeyMultiple(u32),
223    PublicKeySyntax(u32),
224    PublicKeyAlgorithmMismatch(u32),
225    PublicKeyRevoked(u32),
226    IncorrectSignature(u32),
227    NoValidAlgorithm(u32),
228    HeaderHashMismatch(u32),
229    BodyHashMismatch(u32),
230    Modified,
231    Exploded,
232}
233
234impl std::fmt::Display for Dkim2Error {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Dkim2Error::InstanceMissing(m) => write!(f, "Message-Instance m={m} missing"),
238            Dkim2Error::InstanceSyntax(m) => write!(f, "Message-Instance m={m} syntax error"),
239            Dkim2Error::InstanceTagMissing { m, tag } => {
240                write!(f, "Message-Instance m={m} tag={tag} missing")
241            }
242            Dkim2Error::InstanceNotSigned(m) => write!(f, "Message-Instance m={m} is not signed"),
243            Dkim2Error::InstanceAboveSignature(m) => {
244                write!(
245                    f,
246                    "Message-Instance m={m} is higher than any DKIM2-Signature"
247                )
248            }
249            Dkim2Error::SignatureMissing(i) => write!(f, "DKIM2-Signature i={i} missing"),
250            Dkim2Error::SignatureSyntax(i) => write!(f, "DKIM2-Signature i={i} syntax error"),
251            Dkim2Error::SignatureTagMissing { i, tag } => {
252                write!(f, "DKIM2-Signature i={i} tag={tag} missing")
253            }
254            Dkim2Error::SignatureTagUnexpected { i, tag } => {
255                write!(f, "DKIM2-Signature i={i} tag={tag} was unexpected")
256            }
257            Dkim2Error::SequenceGap => write!(f, "DKIM2 sequence numbering has a gap"),
258            Dkim2Error::SequenceOverflow => {
259                write!(f, "DKIM2 sequence numbering would overflow")
260            }
261            Dkim2Error::ChainTooLong => write!(f, "Too many DKIM2 header fields"),
262            Dkim2Error::SignatureExpired(i) => write!(f, "DKIM2-Signature i={i} signature expired"),
263            Dkim2Error::MailFromMismatch(i) => {
264                write!(f, "DKIM2-Signature i={i} MAIL FROM did not match")
265            }
266            Dkim2Error::RcptToMismatch(i) => {
267                write!(f, "DKIM2-Signature i={i} RCPT TO did not match")
268            }
269            Dkim2Error::MailFromDomainMismatch(i) => {
270                write!(f, "DKIM2-Signature i={i} MAIL FROM and d= do not match")
271            }
272            Dkim2Error::NextDomainMismatch(i) => {
273                write!(f, "DKIM2-Signature i={i} nd= does not match")
274            }
275            Dkim2Error::CustodyBreak(i) => {
276                write!(
277                    f,
278                    "DKIM2-Signature i={i} d= and previous RCPT TO do not match"
279                )
280            }
281            Dkim2Error::PublicKeyFetch(i) => {
282                write!(f, "DKIM2-Signature i={i} public key could not be fetched")
283            }
284            Dkim2Error::PublicKeyMissing(i) => {
285                write!(f, "DKIM2-Signature i={i} public key does not exist")
286            }
287            Dkim2Error::PublicKeyMultiple(i) => {
288                write!(f, "DKIM2-Signature i={i} public key has multiple records")
289            }
290            Dkim2Error::PublicKeySyntax(i) => {
291                write!(f, "DKIM2-Signature i={i} public key has a syntax error")
292            }
293            Dkim2Error::PublicKeyAlgorithmMismatch(i) => {
294                write!(f, "DKIM2-Signature i={i} public key algorithm mismatch")
295            }
296            Dkim2Error::PublicKeyRevoked(i) => {
297                write!(f, "DKIM2-Signature i={i} public key has been revoked")
298            }
299            Dkim2Error::IncorrectSignature(i) => {
300                write!(f, "DKIM2-Signature i={i} incorrect signature")
301            }
302            Dkim2Error::NoValidAlgorithm(i) => {
303                write!(f, "DKIM2-Signature i={i} has no valid signature algorithms")
304            }
305            Dkim2Error::HeaderHashMismatch(m) => {
306                write!(f, "Message-Instance m={m} header hash mismatch")
307            }
308            Dkim2Error::BodyHashMismatch(m) => {
309                write!(f, "Message-Instance m={m} body hash mismatch")
310            }
311            Dkim2Error::Modified => {
312                write!(f, "Message has been modified despite a donotmodify request")
313            }
314            Dkim2Error::Exploded => {
315                write!(
316                    f,
317                    "Message has been exploded despite a donotexplode request"
318                )
319            }
320        }
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use super::*;
327
328    fn signed(i: u32, domain: &str, flags: Vec<Flag>) -> Signature {
329        Signature {
330            i,
331            d: domain.to_string(),
332            flags,
333            ..Default::default()
334        }
335    }
336
337    #[test]
338    fn feedback_accessors() {
339        let sig1 = signed(1, "a.example", vec![Flag::Feedback]);
340        let sig2 = signed(2, "b.example", vec![Flag::Feedback, Flag::FeedHere]);
341        let link = |signature| ChainLink {
342            signature,
343            instance: None,
344            result: Dkim2Result::Pass,
345            custody_ok: true,
346        };
347        let output = Dkim2Output {
348            result: Dkim2Result::Pass,
349            chain: vec![link(&sig1), link(&sig2)],
350        };
351
352        assert!(output.feedback_requested());
353        assert_eq!(output.feedback_domains(), vec!["a.example", "b.example"]);
354        assert_eq!(output.feedback_relay(), Some("b.example"));
355
356        let none = Dkim2Output::from(Dkim2Result::Pass);
357        assert!(!none.feedback_requested());
358        assert!(none.feedback_domains().is_empty());
359        assert_eq!(none.feedback_relay(), None);
360    }
361}