Skip to main content

mail_auth/dkim2/
builder.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//! Typestate builder steps of [`Dkim2Signer`].
8
9use super::{Dkim2Hash, Dkim2Signer, Flag, KeyEntry};
10use crate::{
11    crypto::DkimKey,
12    signer::{NeedDomain, NeedSelector, Ready},
13};
14
15impl Dkim2Signer<NeedDomain> {
16    /// Starts a signer with its first private key (RSA-SHA256,
17    /// Ed25519-SHA256 or ML-DSA-44). Continue with
18    /// [`domain`](Self::domain).
19    pub fn from_key(key: impl Into<DkimKey>) -> Dkim2Signer<NeedDomain> {
20        Dkim2Signer {
21            _state: Default::default(),
22            keys: vec![KeyEntry {
23                key: key.into(),
24                selector: String::new(),
25                group: 0,
26            }],
27            domain: String::new(),
28            hashes: vec![Dkim2Hash::Sha256],
29            flags: Vec::new(),
30            nonce: None,
31            out_of_band: false,
32        }
33    }
34
35    /// Sets the signing domain (`d=`). Its public keys are looked up under
36    /// `<selector>._domainkey.<domain>`. Continue with
37    /// [`selector`](Dkim2Signer::selector).
38    pub fn domain(self, domain: impl Into<String>) -> Dkim2Signer<NeedSelector> {
39        Dkim2Signer {
40            _state: Default::default(),
41            keys: self.keys,
42            domain: domain.into(),
43            hashes: self.hashes,
44            flags: self.flags,
45            nonce: self.nonce,
46            out_of_band: self.out_of_band,
47        }
48    }
49}
50
51impl Dkim2Signer<NeedSelector> {
52    /// Sets the selector of the first key, which completes the signer.
53    pub fn selector(mut self, selector: impl Into<String>) -> Dkim2Signer<Ready> {
54        if let Some(entry) = self.keys.first_mut() {
55            entry.selector = selector.into();
56        }
57        Dkim2Signer {
58            _state: Default::default(),
59            keys: self.keys,
60            domain: self.domain,
61            hashes: self.hashes,
62            flags: self.flags,
63            nonce: self.nonce,
64            out_of_band: self.out_of_band,
65        }
66    }
67}
68
69impl Dkim2Signer<Ready> {
70    /// Adds another signing key under its own selector as an alternative to
71    /// the keys added before it.
72    ///
73    /// The key adds a new `|`-separated group to the `s=` tag of the same
74    /// signature (algorithmic agility, §10.9): the signature passes when
75    /// either this key or an earlier group verifies, so a Verifier that does
76    /// not implement one of the algorithms can still check the others.
77    /// Selectors must be distinct and a signature holds at most eight keys;
78    /// signing fails with
79    /// [`Dkim2Error::DuplicateSelector`](super::Dkim2Error::DuplicateSelector)
80    /// or [`Dkim2Error::TooManySelectors`](super::Dkim2Error::TooManySelectors)
81    /// otherwise.
82    pub fn additional_key(self, key: impl Into<DkimKey>, selector: impl Into<String>) -> Self {
83        let group = self
84            .keys
85            .last()
86            .map_or(0, |entry| entry.group.saturating_add(1));
87        self.push_key(key.into(), selector.into(), group)
88    }
89
90    /// Adds another signing key under its own selector that must verify
91    /// together with the key added before it.
92    ///
93    /// The key joins the last `|`-separated group of the `s=` tag with a `,`
94    /// (a "hybrid" signature, §10.9): that group passes only when every key
95    /// in it verifies. The same limits as
96    /// [`additional_key`](Self::additional_key) apply.
97    pub fn hybrid_key(self, key: impl Into<DkimKey>, selector: impl Into<String>) -> Self {
98        let group = self.keys.last().map_or(0, |entry| entry.group);
99        self.push_key(key.into(), selector.into(), group)
100    }
101
102    fn push_key(mut self, key: DkimKey, selector: String, group: u32) -> Self {
103        self.keys.push(KeyEntry {
104            key,
105            selector,
106            group,
107        });
108        self
109    }
110
111    /// Sets the hash algorithms of the `Message-Instance` header fields this
112    /// signer adds (§3.1 and §9.3). They form a single `,`-separated group,
113    /// so all of them must match. Repeated algorithms are added once; an
114    /// empty list keeps the default, SHA-256 only.
115    pub fn hashes(mut self, hashes: impl IntoIterator<Item = Dkim2Hash>) -> Self {
116        let mut selected = Vec::with_capacity(2);
117        for hash in hashes {
118            if !selected.contains(&hash) {
119                selected.push(hash);
120            }
121        }
122        if !selected.is_empty() {
123            self.hashes = selected;
124        }
125        self
126    }
127
128    /// Adds flags to the `f=` tag of the signature (§10.10). Repeated flags
129    /// are added once. [`Flag::Unknown`] values must be textstrings (letters,
130    /// digits, `-` and `_`), or signing fails with
131    /// [`Dkim2Error::SignatureSyntax`](super::Dkim2Error::SignatureSyntax);
132    /// unknown values that spell a defined flag are treated as that flag.
133    ///
134    /// The `exploded` flag is not allowed on the first signature of a
135    /// message (§12.5). It is added automatically to a later signature
136    /// whose [`Hop`](super::Hop) has more than one `RCPT TO`; a Forwarder that
137    /// sends separate copies to several recipients marks each copy's hop with
138    /// [`Hop::exploded`](super::Hop::exploded) instead.
139    pub fn flags(mut self, flags: impl IntoIterator<Item = Flag>) -> Self {
140        for flag in flags {
141            if !self.flags.contains(&flag) {
142                self.flags.push(flag);
143            }
144        }
145        self
146    }
147
148    /// Sets the nonce (`n=` tag, §10.3): an opaque value meaningful only to
149    /// the signer, for example a key to match a returned DSN. It must be at
150    /// most 64 printable ASCII characters without semicolons; signing fails
151    /// with [`Dkim2Error::SignatureSyntax`](super::Dkim2Error::SignatureSyntax)
152    /// otherwise. An empty nonce means no `n=` tag.
153    pub fn nonce(mut self, nonce: impl Into<String>) -> Self {
154        self.nonce = Some(nonce.into()).filter(|nonce| !nonce.is_empty());
155        self
156    }
157
158    /// Declares an out-of-band arrangement (§12 and §12.3) that allows this
159    /// signer to sign messages that are not Fully Verified DKIM2 Messages,
160    /// such as a message whose most recent signature carries `nd=` for an
161    /// imaginary hop made by the same system. Without it, a message that
162    /// already has DKIM2 header fields is signed only with a passing full
163    /// verification of it. The chain of custody, `mf=`/`d=` and `exploded`
164    /// checks still apply.
165    pub fn out_of_band_arrangement(mut self) -> Self {
166        self.out_of_band = true;
167        self
168    }
169}