1use super::{ChainBinding, MessageInstance, Signature, SignatureValue};
8use crate::common::headers::{
9 HEADER_CAPACITY, HeaderFolder, HeaderWriter, Writer, write_base64, write_integer,
10};
11use std::fmt::{Display, Formatter};
12
13impl SignatureValue {
14 fn write(&self, writer: &mut impl Writer, empty: bool) {
15 writer.write(self.selector.as_bytes());
16 writer.write(b":");
17 writer.write(self.a.name().as_bytes());
18 writer.write(b":");
19 if !empty {
20 write_base64(writer, &self.b);
21 }
22 }
23}
24
25impl Signature {
26 pub(crate) fn write_value(&self, writer: &mut impl Writer, empty_signature: bool) {
27 writer.write(b"i=");
28 write_integer(writer, self.i as u64);
29 writer.write(b"; m=");
30 write_integer(writer, self.m as u64);
31 writer.write(b"; t=");
32 write_integer(writer, self.t);
33 writer.write(b"; d=");
34 writer.write(self.d.as_bytes());
35 writer.write(b"; ");
36
37 match &self.chain {
38 ChainBinding::Envelope { mail_from, rcpt_to } => {
39 writer.write(b"mf=");
40 write_base64(writer, mail_from.as_bytes());
41 writer.write(b"; rt=");
42 for (pos, rcpt) in rcpt_to.iter().enumerate() {
43 if pos > 0 {
44 writer.write(b",");
45 }
46 write_base64(writer, rcpt.as_bytes());
47 }
48 }
49 ChainBinding::NextDomain(domain) => {
50 writer.write(b"nd=");
51 writer.write(domain.as_bytes());
52 }
53 }
54
55 writer.write(b"; s=");
56 for (pos, value) in self.s.iter().enumerate() {
57 if pos > 0 {
58 writer.write(b",");
59 }
60 value.write(writer, empty_signature);
61 }
62 writer.write(b";");
63
64 if let Some(nonce) = &self.n {
65 writer.write(b" n=");
66 writer.write(nonce.as_bytes());
67 writer.write(b";");
68 }
69
70 if !self.flags.is_empty() {
71 writer.write(b" f=");
72 for (pos, flag) in self.flags.iter().enumerate() {
73 if pos > 0 {
74 writer.write(b",");
75 }
76 writer.write(flag.as_bytes());
77 }
78 writer.write(b";");
79 }
80 }
81
82 pub fn write(&self, writer: &mut impl Writer) {
83 writer.write(b"DKIM2-Signature: ");
84 self.write_value(writer, false);
85 writer.write(b"\r\n");
86 }
87}
88
89impl MessageInstance {
90 pub(crate) fn write_value(&self, writer: &mut impl Writer) {
91 writer.write(b"m=");
92 write_integer(writer, self.m as u64);
93 writer.write(b"; h=");
94 for (pos, hash) in self.hashes.iter().enumerate() {
95 if pos > 0 {
96 writer.write(b",");
97 }
98 writer.write(hash.name.map(|n| n.name().as_bytes()).unwrap_or(b""));
99 writer.write(b":");
100 write_base64(writer, &hash.header_hash);
101 writer.write(b":");
102 write_base64(writer, &hash.body_hash);
103 }
104
105 if let Some(recipe) = &self.recipe {
106 let mut json = Vec::new();
107 if recipe.to_json(&mut json).is_ok() {
108 writer.write(b"; r=");
109 write_base64(writer, &json);
110 }
111 }
112 writer.write(b";");
113 }
114
115 pub fn write(&self, writer: &mut impl Writer) {
116 writer.write(b"Message-Instance: ");
117 self.write_value(writer);
118 writer.write(b"\r\n");
119 }
120}
121
122impl HeaderWriter for Signature {
123 fn write_header(&self, writer: &mut impl Writer) {
124 self.write(&mut HeaderFolder::new(writer));
125 }
126}
127
128impl HeaderWriter for MessageInstance {
129 fn write_header(&self, writer: &mut impl Writer) {
130 self.write(&mut HeaderFolder::new(writer));
131 }
132}
133
134impl Display for Signature {
135 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136 let mut buf = Vec::with_capacity(HEADER_CAPACITY);
137 self.write_value(&mut buf, false);
138 f.write_str(&String::from_utf8_lossy(&buf))
139 }
140}
141
142impl Display for MessageInstance {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 let mut buf = Vec::with_capacity(HEADER_CAPACITY);
145 self.write_value(&mut buf);
146 f.write_str(&String::from_utf8_lossy(&buf))
147 }
148}
149
150#[cfg(test)]
151mod test {
152 use crate::common::crypto::{Algorithm, HashAlgorithm};
153 use crate::common::headers::HeaderWriter;
154 use crate::dkim2::{
155 ChainBinding, Flag, MessageHash, MessageInstance, Signature, SignatureValue,
156 };
157
158 const MAX_HEADER_LINE_LEN: usize = 76;
159
160 fn strip_all_ws(bytes: &[u8]) -> Vec<u8> {
161 bytes
162 .iter()
163 .copied()
164 .filter(|c| !matches!(c, b' ' | b'\t' | b'\r' | b'\n'))
165 .collect()
166 }
167
168 fn assert_fold_is_transparent(field_name: &[u8], folded: &[u8], unfolded: &[u8]) {
173 assert!(
174 folded.ends_with(b"\r\n"),
175 "a folded header field must end with CRLF: {:?}",
176 String::from_utf8_lossy(folded)
177 );
178
179 for (n, line) in folded
180 .strip_suffix(b"\r\n")
181 .unwrap()
182 .split(|&c| c == b'\n')
183 .enumerate()
184 {
185 let line = line.strip_suffix(b"\r").unwrap_or(line);
186 let content = line.strip_prefix(b"\t").unwrap_or(line);
187 assert!(
188 content.len() <= MAX_HEADER_LINE_LEN,
189 "{}: line {n} is {} bytes, exceeds {MAX_HEADER_LINE_LEN}: {:?}",
190 String::from_utf8_lossy(field_name),
191 content.len(),
192 String::from_utf8_lossy(content),
193 );
194 }
195
196 for (i, &ch) in folded.iter().enumerate() {
197 if ch == b'\n' && i + 1 < folded.len() {
198 assert!(
199 i >= 1 && folded[i - 1] == b'\r' && folded.get(i + 1) == Some(&b'\t'),
200 "every interior LF must be part of a CRLF+TAB fold: {:?}",
201 String::from_utf8_lossy(folded)
202 );
203 }
204 }
205
206 let mut expected = field_name.to_vec();
207 expected.extend_from_slice(unfolded);
208 assert_eq!(
209 strip_all_ws(folded),
210 strip_all_ws(&expected),
211 "folding changed the whitespace-stripped (signed) bytes of {}",
212 String::from_utf8_lossy(field_name)
213 );
214 }
215
216 fn dump_folded(title: &str, folded: &[u8]) {
217 println!("\n===== {title} ({} bytes) =====", folded.len());
218 for (n, line) in folded.split(|&c| c == b'\n').enumerate() {
219 if line.is_empty() {
220 continue;
221 }
222 let line = line.strip_suffix(b"\r").unwrap_or(line);
223 let content = line.strip_prefix(b"\t").unwrap_or(line);
224 let leader = if line.len() != content.len() {
225 "\\t"
226 } else {
227 " "
228 };
229 println!(
230 " line {n:>2} | {:>3} | {leader}{}",
231 content.len(),
232 String::from_utf8_lossy(content)
233 );
234 }
235 println!("----- raw (escaped) -----");
236 println!(" {}", String::from_utf8_lossy(folded).escape_debug());
237 }
238
239 fn value_of<'a>(folded: &'a [u8], field_name: &[u8]) -> &'a [u8] {
240 folded
241 .strip_prefix(field_name)
242 .expect("folded header must start with the field name")
243 }
244
245 fn big_signature() -> Signature {
246 Signature {
247 i: 3,
248 m: 2,
249 t: 1782394336,
250 d: "test.dkim2.eu".to_string(),
251 s: vec![
252 SignatureValue {
253 selector: "rsa2048".to_string(),
254 a: Algorithm::RsaSha256,
255 b: (0u8..=255).cycle().take(256).collect(),
256 },
257 SignatureValue {
258 selector: "ed25519".to_string(),
259 a: Algorithm::Ed25519Sha256,
260 b: (0u8..64).collect(),
261 },
262 ],
263 chain: ChainBinding::Envelope {
264 mail_from: "<sender@test.dkim2.eu>".to_string(),
265 rcpt_to: vec![
266 "<recipient@example.com>".to_string(),
267 "<second-recipient@another-example.org>".to_string(),
268 ],
269 },
270 n: Some("banana".to_string()),
271 flags: vec![Flag::Feedback, Flag::DoNotModify],
272 }
273 }
274
275 fn big_instance() -> MessageInstance {
276 MessageInstance {
277 m: 1,
278 hashes: vec![
279 MessageHash {
280 name: Some(HashAlgorithm::Sha256),
281 header_hash: (0u8..32).collect(),
282 body_hash: (32u8..64).collect(),
283 },
284 MessageHash {
285 name: Some(HashAlgorithm::Sha256),
286 header_hash: (64u8..96).collect(),
287 body_hash: (96u8..128).collect(),
288 },
289 ],
290 recipe: None,
291 }
292 }
293
294 #[test]
295 fn signature_write_header_folds_and_round_trips() {
296 let sig = big_signature();
297
298 let mut folded = Vec::new();
299 sig.write_header(&mut folded);
300
301 let mut unfolded = Vec::new();
302 sig.write_value(&mut unfolded, false);
303 unfolded.extend_from_slice(b"\r\n");
304
305 assert!(
306 folded.windows(3).any(|w| w == b"\r\n\t"),
307 "a large signature should have been folded: {:?}",
308 String::from_utf8_lossy(&folded)
309 );
310 dump_folded(
311 "DKIM2-Signature (large: rsa2048 + ed25519, 2 recipients)",
312 &folded,
313 );
314 assert_fold_is_transparent(b"DKIM2-Signature: ", &folded, &unfolded);
315
316 let value = value_of(&folded, b"DKIM2-Signature: ");
317 assert_eq!(
318 Signature::parse(value).unwrap(),
319 sig,
320 "folded signature must parse back to the original"
321 );
322 }
323
324 #[test]
325 fn message_instance_write_header_folds_and_round_trips() {
326 let instance = big_instance();
327
328 let mut folded = Vec::new();
329 instance.write_header(&mut folded);
330
331 assert!(
332 folded.windows(3).any(|w| w == b"\r\n\t"),
333 "a large message-instance should have been folded: {:?}",
334 String::from_utf8_lossy(&folded)
335 );
336 let mut unfolded = Vec::new();
337 instance.write_value(&mut unfolded);
338 unfolded.extend_from_slice(b"\r\n");
339 dump_folded("Message-Instance (large: 2 sha256 hash sets)", &folded);
340 assert_fold_is_transparent(b"Message-Instance: ", &folded, &unfolded);
341
342 let value = value_of(&folded, b"Message-Instance: ");
343 assert_eq!(
344 MessageInstance::parse(value).unwrap(),
345 instance,
346 "folded message-instance must parse back to the original"
347 );
348 }
349
350 #[test]
351 fn signature_folds_at_every_realistic_size() {
352 for extra_recipients in 0..6 {
353 let mut sig = big_signature();
354 if let ChainBinding::Envelope { rcpt_to, .. } = &mut sig.chain {
355 for n in 0..extra_recipients {
356 rcpt_to.push(format!("<rcpt-{n}@padding-domain-for-length.example>"));
357 }
358 }
359
360 let mut folded = Vec::new();
361 sig.write_header(&mut folded);
362
363 let mut unfolded = Vec::new();
364 sig.write_value(&mut unfolded, false);
365 unfolded.extend_from_slice(b"\r\n");
366
367 dump_folded(
368 &format!("DKIM2-Signature ({} recipients)", extra_recipients + 2),
369 &folded,
370 );
371 assert_fold_is_transparent(b"DKIM2-Signature: ", &folded, &unfolded);
372 assert_eq!(
373 Signature::parse(value_of(&folded, b"DKIM2-Signature: ")).unwrap(),
374 sig
375 );
376 }
377 }
378
379 fn sample() -> Signature {
380 Signature {
381 i: 2,
382 m: 1,
383 t: 1700000000,
384 d: "example.com".to_string(),
385 s: vec![SignatureValue {
386 selector: "sel".to_string(),
387 a: Algorithm::Ed25519Sha256,
388 b: vec![1, 2, 3, 4],
389 }],
390 chain: ChainBinding::Envelope {
391 mail_from: "<a@example.com>".to_string(),
392 rcpt_to: vec!["<b@example.org>".to_string()],
393 },
394 n: None,
395 flags: Vec::new(),
396 }
397 }
398
399 #[test]
400 fn flags_emitted_in_order() {
401 let mut buf = Vec::new();
402 Signature {
403 flags: vec![Flag::DoNotModify, Flag::FeedHere],
404 ..sample()
405 }
406 .write_value(&mut buf, false);
407 let out = String::from_utf8(buf).unwrap();
408 assert!(out.contains("f=donotmodify,feedhere;"), "{out}");
409 }
410
411 #[test]
412 fn next_domain_emits_nd_and_omits_envelope() {
413 let mut buf = Vec::new();
414 Signature {
415 chain: ChainBinding::NextDomain("relay.example".to_string()),
416 ..sample()
417 }
418 .write_value(&mut buf, false);
419 let out = String::from_utf8(buf).unwrap();
420 assert!(out.contains("nd=relay.example"), "{out}");
421 assert!(!out.contains("mf="), "{out}");
422 assert!(!out.contains("rt="), "{out}");
423 }
424
425 #[test]
426 fn write_then_parse_round_trips() {
427 let sig = sample();
428 let mut buf = Vec::new();
429 sig.write_value(&mut buf, false);
430 assert_eq!(Signature::parse(&buf).unwrap(), sig);
431 }
432}