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