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