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}