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