Commit
+1707 -0 +/-9 browse
1 | diff --git a/Cargo.toml b/Cargo.toml |
2 | new file mode 100644 |
3 | index 0000000..b41646b |
4 | --- /dev/null |
5 | +++ b/Cargo.toml |
6 | @@ -0,0 +1,13 @@ |
7 | + [package] |
8 | + name = "mail-auth" |
9 | + version = "0.1.0" |
10 | + edition = "2021" |
11 | + |
12 | + |
13 | + [dependencies] |
14 | + #mail-parser = { version = "0.7", git = "https://github.com/stalwartlabs/mail-parser", features = ["ludicrous_mode", "full_encoding"] |
15 | + mail-parser = { path = "/home/vagrant/code/mail-parser", features = ["ludicrous_mode", "full_encoding"] } |
16 | + mail-builder = { version = "0.2.4", git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } |
17 | + rsa = {version = "0.7.0"} |
18 | + sha1 = {version = "0.10", features = ["oid"]} |
19 | + sha2 = {version = "0.10.6", features = ["oid"]} |
20 | diff --git a/src/common/headers.rs b/src/common/headers.rs |
21 | new file mode 100644 |
22 | index 0000000..889b215 |
23 | --- /dev/null |
24 | +++ b/src/common/headers.rs |
25 | @@ -0,0 +1,138 @@ |
26 | + use std::{ |
27 | + iter::{Enumerate, Peekable}, |
28 | + slice::Iter, |
29 | + }; |
30 | + |
31 | + #[derive(Clone, Copy)] |
32 | + enum State { |
33 | + Name { start: usize }, |
34 | + Value { start: usize, colon: usize }, |
35 | + } |
36 | + |
37 | + pub(crate) struct HeaderIterator<'x> { |
38 | + message: &'x [u8], |
39 | + iter: Peekable<Enumerate<Iter<'x, u8>>>, |
40 | + state: State, |
41 | + } |
42 | + |
43 | + impl<'x> HeaderIterator<'x> { |
44 | + pub fn new(message: &'x [u8]) -> Self { |
45 | + HeaderIterator { |
46 | + message, |
47 | + iter: message.iter().enumerate().peekable(), |
48 | + state: State::Name { start: 0 }, |
49 | + } |
50 | + } |
51 | + |
52 | + pub fn body_offset(&mut self) -> Option<usize> { |
53 | + self.iter.peek().map(|(pos, _)| *pos) |
54 | + } |
55 | + } |
56 | + |
57 | + impl<'x> Iterator for HeaderIterator<'x> { |
58 | + type Item = (&'x [u8], &'x [u8]); |
59 | + |
60 | + fn next(&mut self) -> Option<Self::Item> { |
61 | + let mut last_ch = 0; |
62 | + while let Some((pos, &ch)) = self.iter.next() { |
63 | + if ch == b':' { |
64 | + if let State::Name { start } = &self.state { |
65 | + self.state = State::Value { |
66 | + start: *start, |
67 | + colon: pos, |
68 | + }; |
69 | + } |
70 | + } else if ch == b'\n' { |
71 | + match self.state { |
72 | + State::Value { start, colon } => { |
73 | + if self |
74 | + .iter |
75 | + .peek() |
76 | + .map_or(true, |(_, next_byte)| ![b' ', b'\t'].contains(next_byte)) |
77 | + { |
78 | + let header_name = self.message.get(start..colon).unwrap_or_default(); |
79 | + let header_value = |
80 | + self.message.get(colon + 1..pos + 1).unwrap_or_default(); |
81 | + self.state = State::Name { start: pos + 1 }; |
82 | + return Some((header_name, header_value)); |
83 | + } |
84 | + } |
85 | + State::Name { start } => { |
86 | + if last_ch == b'\r' || start == pos { |
87 | + // End of headers |
88 | + return None; |
89 | + } else if self |
90 | + .iter |
91 | + .peek() |
92 | + .map_or(true, |(_, next_byte)| ![b' ', b'\t'].contains(next_byte)) |
93 | + { |
94 | + // Invalid header, return anyway. |
95 | + let header_name = self.message.get(start..pos + 1).unwrap_or_default(); |
96 | + self.state = State::Name { start: pos + 1 }; |
97 | + return Some((header_name, b"")); |
98 | + } |
99 | + } |
100 | + } |
101 | + } |
102 | + |
103 | + last_ch = ch; |
104 | + } |
105 | + |
106 | + None |
107 | + } |
108 | + } |
109 | + |
110 | + #[cfg(test)] |
111 | + mod test { |
112 | + use super::HeaderIterator; |
113 | + |
114 | + #[test] |
115 | + fn header_iterator() { |
116 | + for (message, headers) in [ |
117 | + ( |
118 | + "From: a\nTo: b\nEmpty:\nMulti: 1\n 2\nSubject: c\n\nNot-header: ignore\n", |
119 | + vec![ |
120 | + ("From", " a\n"), |
121 | + ("To", " b\n"), |
122 | + ("Empty", "\n"), |
123 | + ("Multi", " 1\n 2\n"), |
124 | + ("Subject", " c\n"), |
125 | + ], |
126 | + ), |
127 | + ( |
128 | + ": a\nTo: b\n \n \nc\n:\nFrom : d\nSubject: e\n\nNot-header: ignore\n", |
129 | + vec![ |
130 | + ("", " a\n"), |
131 | + ("To", " b\n \n \n"), |
132 | + ("c\n", ""), |
133 | + ("", "\n"), |
134 | + ("From ", " d\n"), |
135 | + ("Subject", " e\n"), |
136 | + ], |
137 | + ), |
138 | + ( |
139 | + concat!( |
140 | + "A: X\r\n", |
141 | + "B : Y\t\r\n", |
142 | + "\tZ \r\n", |
143 | + "\r\n", |
144 | + " C \r\n", |
145 | + "D \t E\r\n" |
146 | + ), |
147 | + vec![("A", " X\r\n"), ("B ", " Y\t\r\n\tZ \r\n")], |
148 | + ), |
149 | + ] { |
150 | + assert_eq!( |
151 | + HeaderIterator::new(message.as_bytes()) |
152 | + .map(|(h, v)| { |
153 | + ( |
154 | + std::str::from_utf8(h).unwrap(), |
155 | + std::str::from_utf8(v).unwrap(), |
156 | + ) |
157 | + }) |
158 | + .collect::<Vec<_>>(), |
159 | + headers |
160 | + ); |
161 | + } |
162 | + } |
163 | + } |
164 | diff --git a/src/common/mod.rs b/src/common/mod.rs |
165 | new file mode 100644 |
166 | index 0000000..af83e80 |
167 | --- /dev/null |
168 | +++ b/src/common/mod.rs |
169 | @@ -0,0 +1,2 @@ |
170 | + pub mod headers; |
171 | + pub mod parse; |
172 | diff --git a/src/common/parse.rs b/src/common/parse.rs |
173 | new file mode 100644 |
174 | index 0000000..514b786 |
175 | --- /dev/null |
176 | +++ b/src/common/parse.rs |
177 | @@ -0,0 +1,207 @@ |
178 | + use std::slice::Iter; |
179 | + |
180 | + use mail_parser::decoders::quoted_printable::quoted_printable_decode_char; |
181 | + |
182 | + pub(crate) trait TagParser: Sized { |
183 | + fn match_bytes(&mut self, bytes: &[u8]) -> bool; |
184 | + fn get_tag(&mut self) -> Vec<u8>; |
185 | + fn get_tag_qp(&mut self) -> Vec<u8>; |
186 | + fn get_headers_qp(&mut self) -> Vec<Vec<u8>>; |
187 | + fn get_number(&mut self) -> u64; |
188 | + fn get_items<T: ItemParser>(&mut self, separator: u8) -> Vec<T>; |
189 | + fn ignore(&mut self); |
190 | + fn skip_whitespaces(&mut self) -> bool; |
191 | + } |
192 | + |
193 | + pub(crate) trait ItemParser: Sized { |
194 | + fn parse(bytes: &[u8]) -> Option<Self>; |
195 | + } |
196 | + |
197 | + impl TagParser for Iter<'_, u8> { |
198 | + #[inline(always)] |
199 | + fn match_bytes(&mut self, bytes: &[u8]) -> bool { |
200 | + let mut pos = 0; |
201 | + |
202 | + for ch in self { |
203 | + if !ch.is_ascii_whitespace() { |
204 | + if bytes[pos].eq_ignore_ascii_case(ch) { |
205 | + if pos < bytes.len() - 1 { |
206 | + pos += 1; |
207 | + } else { |
208 | + break; |
209 | + } |
210 | + } else { |
211 | + return false; |
212 | + } |
213 | + } |
214 | + } |
215 | + |
216 | + pos == bytes.len() - 1 |
217 | + } |
218 | + |
219 | + #[inline(always)] |
220 | + fn get_tag(&mut self) -> Vec<u8> { |
221 | + let mut tag = Vec::with_capacity(20); |
222 | + for &ch in self { |
223 | + if ch == b';' { |
224 | + break; |
225 | + } else if !ch.is_ascii_whitespace() { |
226 | + tag.push(ch); |
227 | + } |
228 | + } |
229 | + tag |
230 | + } |
231 | + |
232 | + #[inline(always)] |
233 | + #[allow(clippy::while_let_on_iterator)] |
234 | + fn get_tag_qp(&mut self) -> Vec<u8> { |
235 | + let mut tag = Vec::with_capacity(20); |
236 | + 'outer: while let Some(&ch) = self.next() { |
237 | + if ch == b';' { |
238 | + break; |
239 | + } else if ch == b'=' { |
240 | + let mut hex1 = 0; |
241 | + |
242 | + while let Some(&ch) = self.next() { |
243 | + if ch.is_ascii_digit() { |
244 | + if hex1 != 0 { |
245 | + if let Some(ch) = quoted_printable_decode_char(hex1, ch) { |
246 | + tag.push(ch); |
247 | + } |
248 | + break; |
249 | + } else { |
250 | + hex1 = ch; |
251 | + } |
252 | + } else if ch == b';' { |
253 | + break 'outer; |
254 | + } else if !ch.is_ascii_whitespace() { |
255 | + break; |
256 | + } |
257 | + } |
258 | + } else if !ch.is_ascii_whitespace() { |
259 | + tag.push(ch); |
260 | + } |
261 | + } |
262 | + tag |
263 | + } |
264 | + |
265 | + #[inline(always)] |
266 | + #[allow(clippy::while_let_on_iterator)] |
267 | + fn get_headers_qp(&mut self) -> Vec<Vec<u8>> { |
268 | + let mut tags = Vec::new(); |
269 | + let mut tag = Vec::with_capacity(20); |
270 | + |
271 | + 'outer: while let Some(&ch) = self.next() { |
272 | + if ch == b';' { |
273 | + break; |
274 | + } else if ch == b'|' { |
275 | + if !tag.is_empty() { |
276 | + tags.push(tag.to_vec()); |
277 | + tag.clear(); |
278 | + } |
279 | + } else if ch == b'=' { |
280 | + let mut hex1 = 0; |
281 | + |
282 | + while let Some(&ch) = self.next() { |
283 | + if ch.is_ascii_digit() { |
284 | + if hex1 != 0 { |
285 | + if let Some(ch) = quoted_printable_decode_char(hex1, ch) { |
286 | + tag.push(ch); |
287 | + } |
288 | + break; |
289 | + } else { |
290 | + hex1 = ch; |
291 | + } |
292 | + } else if ch == b'|' { |
293 | + if !tag.is_empty() { |
294 | + tags.push(tag.to_vec()); |
295 | + tag.clear(); |
296 | + } |
297 | + break; |
298 | + } else if ch == b';' { |
299 | + break 'outer; |
300 | + } else if !ch.is_ascii_whitespace() { |
301 | + break; |
302 | + } |
303 | + } |
304 | + } else if !ch.is_ascii_whitespace() { |
305 | + tag.push(ch); |
306 | + } |
307 | + } |
308 | + |
309 | + if !tag.is_empty() { |
310 | + tags.push(tag); |
311 | + } |
312 | + |
313 | + tags |
314 | + } |
315 | + |
316 | + #[inline(always)] |
317 | + fn get_number(&mut self) -> u64 { |
318 | + let mut num: u64 = 0; |
319 | + |
320 | + for &ch in self { |
321 | + if ch == b';' { |
322 | + break; |
323 | + } else if ch.is_ascii_digit() { |
324 | + num = (num.saturating_mul(10)) + (ch - b'0') as u64; |
325 | + } else if !ch.is_ascii_whitespace() { |
326 | + return 0; |
327 | + } |
328 | + } |
329 | + |
330 | + num |
331 | + } |
332 | + |
333 | + #[inline(always)] |
334 | + fn ignore(&mut self) { |
335 | + for &ch in self { |
336 | + if ch == b';' { |
337 | + break; |
338 | + } |
339 | + } |
340 | + } |
341 | + |
342 | + #[inline(always)] |
343 | + fn skip_whitespaces(&mut self) -> bool { |
344 | + for &ch in self { |
345 | + if ch == b';' { |
346 | + return true; |
347 | + } else if !ch.is_ascii_whitespace() { |
348 | + return false; |
349 | + } |
350 | + } |
351 | + true |
352 | + } |
353 | + |
354 | + fn get_items<T: ItemParser>(&mut self, separator: u8) -> Vec<T> { |
355 | + let mut buf = Vec::with_capacity(10); |
356 | + let mut items = Vec::new(); |
357 | + for &ch in self { |
358 | + if ch == separator { |
359 | + if !buf.is_empty() { |
360 | + if let Some(item) = T::parse(&buf) { |
361 | + items.push(item); |
362 | + } |
363 | + buf.clear(); |
364 | + } |
365 | + } else if ch == b';' { |
366 | + break; |
367 | + } else if !ch.is_ascii_whitespace() { |
368 | + buf.push(ch); |
369 | + } |
370 | + } |
371 | + if !buf.is_empty() { |
372 | + if let Some(item) = T::parse(&buf) { |
373 | + items.push(item); |
374 | + } |
375 | + } |
376 | + items |
377 | + } |
378 | + } |
379 | + |
380 | + impl ItemParser for Vec<u8> { |
381 | + fn parse(bytes: &[u8]) -> Option<Self> { |
382 | + Some(bytes.to_vec()) |
383 | + } |
384 | + } |
385 | diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs |
386 | new file mode 100644 |
387 | index 0000000..ef2ce74 |
388 | --- /dev/null |
389 | +++ b/src/dkim/canonicalize.rs |
390 | @@ -0,0 +1,252 @@ |
391 | + /* |
392 | + * Copyright Stalwart Labs Ltd. See the COPYING |
393 | + * file at the top-level directory of this distribution. |
394 | + * |
395 | + * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
396 | + * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
397 | + * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
398 | + * option. This file may not be copied, modified, or distributed |
399 | + * except according to those terms. |
400 | + */ |
401 | + |
402 | + use std::io::Write; |
403 | + |
404 | + use crate::common::headers::HeaderIterator; |
405 | + |
406 | + use super::{Canonicalization, DKIMSigner}; |
407 | + |
408 | + impl Canonicalization { |
409 | + pub fn canonicalize_body(&self, message: &[u8], mut hasher: impl Write) -> std::io::Result<()> { |
410 | + let mut crlf_seq = 0; |
411 | + |
412 | + match self { |
413 | + Canonicalization::Relaxed => { |
414 | + let mut last_ch = 0; |
415 | + |
416 | + for &ch in message { |
417 | + match ch { |
418 | + b' ' | b'\t' => { |
419 | + while crlf_seq > 0 { |
420 | + let _ = hasher.write(b"\r\n")?; |
421 | + crlf_seq -= 1; |
422 | + } |
423 | + } |
424 | + b'\n' => { |
425 | + crlf_seq += 1; |
426 | + } |
427 | + b'\r' => {} |
428 | + _ => { |
429 | + while crlf_seq > 0 { |
430 | + let _ = hasher.write(b"\r\n")?; |
431 | + crlf_seq -= 1; |
432 | + } |
433 | + |
434 | + if last_ch == b' ' || last_ch == b'\t' { |
435 | + let _ = hasher.write(b" ")?; |
436 | + } |
437 | + |
438 | + let _ = hasher.write(&[ch])?; |
439 | + } |
440 | + } |
441 | + |
442 | + last_ch = ch; |
443 | + } |
444 | + } |
445 | + Canonicalization::Simple => { |
446 | + for &ch in message { |
447 | + match ch { |
448 | + b'\n' => { |
449 | + crlf_seq += 1; |
450 | + } |
451 | + b'\r' => {} |
452 | + _ => { |
453 | + while crlf_seq > 0 { |
454 | + let _ = hasher.write(b"\r\n")?; |
455 | + crlf_seq -= 1; |
456 | + } |
457 | + let _ = hasher.write(&[ch])?; |
458 | + } |
459 | + } |
460 | + } |
461 | + } |
462 | + } |
463 | + |
464 | + hasher.write_all(b"\r\n") |
465 | + } |
466 | + |
467 | + pub fn canonicalize_headers<'x>( |
468 | + &self, |
469 | + headers: impl Iterator<Item = (&'x [u8], &'x [u8])>, |
470 | + mut hasher: impl Write, |
471 | + ) -> std::io::Result<()> { |
472 | + match self { |
473 | + Canonicalization::Relaxed => { |
474 | + for (name, value) in headers { |
475 | + for &ch in name { |
476 | + if !ch.is_ascii_whitespace() { |
477 | + let _ = hasher.write(&[ch.to_ascii_lowercase()])?; |
478 | + } |
479 | + } |
480 | + let _ = hasher.write(b":")?; |
481 | + let mut bytes_written = 0; |
482 | + let mut last_ch = 0; |
483 | + |
484 | + for &ch in value { |
485 | + if !ch.is_ascii_whitespace() { |
486 | + if [b' ', b'\t'].contains(&last_ch) && bytes_written > 0 { |
487 | + bytes_written += hasher.write(b" ")?; |
488 | + } |
489 | + bytes_written += hasher.write(&[ch])?; |
490 | + } |
491 | + last_ch = ch; |
492 | + } |
493 | + let _ = hasher.write(b"\r\n"); |
494 | + } |
495 | + } |
496 | + Canonicalization::Simple => { |
497 | + for (name, value) in headers { |
498 | + let _ = hasher.write(name)?; |
499 | + let _ = hasher.write(b":")?; |
500 | + let _ = hasher.write(value)?; |
501 | + } |
502 | + } |
503 | + } |
504 | + |
505 | + Ok(()) |
506 | + } |
507 | + |
508 | + pub fn serialize_name(&self, mut writer: impl Write) -> std::io::Result<()> { |
509 | + writer.write_all(match self { |
510 | + Canonicalization::Relaxed => b"relaxed", |
511 | + Canonicalization::Simple => b"simple", |
512 | + }) |
513 | + } |
514 | + } |
515 | + |
516 | + impl<'x> DKIMSigner<'x> { |
517 | + #[allow(clippy::while_let_on_iterator)] |
518 | + pub(crate) fn canonicalize( |
519 | + &self, |
520 | + message: &[u8], |
521 | + header_hasher: impl Write, |
522 | + body_hasher: impl Write, |
523 | + ) -> super::Result<(usize, Vec<Vec<u8>>)> { |
524 | + let mut headers_it = HeaderIterator::new(message); |
525 | + let mut headers = Vec::with_capacity(self.sign_headers.len()); |
526 | + let mut found_headers = vec![false; self.sign_headers.len()]; |
527 | + let mut signed_headers = Vec::with_capacity(self.sign_headers.len()); |
528 | + |
529 | + for (name, value) in &mut headers_it { |
530 | + if let Some(pos) = self |
531 | + .sign_headers |
532 | + .iter() |
533 | + .position(|header| header.eq_ignore_ascii_case(name)) |
534 | + { |
535 | + headers.push((name, value)); |
536 | + found_headers[pos] = true; |
537 | + signed_headers.push(name.to_vec()); |
538 | + } |
539 | + } |
540 | + |
541 | + let body = headers_it |
542 | + .body_offset() |
543 | + .and_then(|pos| message.get(pos..)) |
544 | + .unwrap_or_default(); |
545 | + let body_len = body.len(); |
546 | + self.ch |
547 | + .canonicalize_headers(headers.into_iter().rev(), header_hasher)?; |
548 | + self.cb.canonicalize_body(body, body_hasher)?; |
549 | + |
550 | + // Add any missing headers |
551 | + signed_headers.reverse(); |
552 | + for (header, found) in self.sign_headers.iter().zip(found_headers) { |
553 | + if !found { |
554 | + signed_headers.push(header.to_vec()); |
555 | + } |
556 | + } |
557 | + |
558 | + Ok((body_len, signed_headers)) |
559 | + } |
560 | + } |
561 | + |
562 | + #[cfg(test)] |
563 | + mod test { |
564 | + use crate::{common::headers::HeaderIterator, dkim::Canonicalization}; |
565 | + |
566 | + #[test] |
567 | + #[allow(clippy::needless_collect)] |
568 | + fn dkim_canonicalize() { |
569 | + for (message, (relaxed_headers, relaxed_body), (simple_headers, simple_body)) in [ |
570 | + ( |
571 | + concat!( |
572 | + "A: X\r\n", |
573 | + "B : Y\t\r\n", |
574 | + "\tZ \r\n", |
575 | + "\r\n", |
576 | + " C \r\n", |
577 | + "D \t E\r\n" |
578 | + ), |
579 | + ( |
580 | + concat!("a:X\r\n", "b:Y Z\r\n",), |
581 | + concat!(" C\r\n", "D E\r\n"), |
582 | + ), |
583 | + ("A: X\r\nB : Y\t\r\n\tZ \r\n", " C \r\nD \t E\r\n"), |
584 | + ), |
585 | + ( |
586 | + concat!( |
587 | + " From : John\tdoe <jdoe@domain.com>\t\r\n", |
588 | + "SUB JECT:\ttest \t \r\n\r\n", |
589 | + " body \t \r\n", |
590 | + "\r\n", |
591 | + "\r\n", |
592 | + ), |
593 | + ( |
594 | + concat!("from:John doe <jdoe@domain.com>\r\n", "subject:test\r\n"), |
595 | + concat!(" body\r\n"), |
596 | + ), |
597 | + ( |
598 | + concat!( |
599 | + " From : John\tdoe <jdoe@domain.com>\t\r\n", |
600 | + "SUB JECT:\ttest \t \r\n" |
601 | + ), |
602 | + concat!(" body \t \r\n"), |
603 | + ), |
604 | + ), |
605 | + ( |
606 | + concat!("H: value\t\r\n\r\n",), |
607 | + (concat!("h:value\r\n"), concat!("\r\n")), |
608 | + (concat!("H: value\t\r\n"), concat!("\r\n")), |
609 | + ), |
610 | + ( |
611 | + concat!("\tx\t: \t\t\tz\r\n\r\nabc",), |
612 | + (concat!("x:z\r\n"), concat!("abc\r\n")), |
613 | + ("\tx\t: \t\t\tz\r\n", concat!("abc\r\n")), |
614 | + ), |
615 | + ] { |
616 | + let mut header_iterator = HeaderIterator::new(message.as_bytes()); |
617 | + let parsed_headers = (&mut header_iterator).collect::<Vec<_>>(); |
618 | + let raw_body = header_iterator |
619 | + .body_offset() |
620 | + .map(|pos| &message.as_bytes()[pos..]) |
621 | + .unwrap_or_default(); |
622 | + |
623 | + for (canonicalization, expected_headers, expected_body) in [ |
624 | + (Canonicalization::Relaxed, relaxed_headers, relaxed_body), |
625 | + (Canonicalization::Simple, simple_headers, simple_body), |
626 | + ] { |
627 | + let mut headers = Vec::new(); |
628 | + let mut body = Vec::new(); |
629 | + |
630 | + canonicalization |
631 | + .canonicalize_headers(parsed_headers.clone().into_iter(), &mut headers) |
632 | + .unwrap(); |
633 | + canonicalization |
634 | + .canonicalize_body(raw_body, &mut body) |
635 | + .unwrap(); |
636 | + |
637 | + assert_eq!(expected_headers, String::from_utf8(headers).unwrap()); |
638 | + assert_eq!(expected_body, String::from_utf8(body).unwrap()); |
639 | + } |
640 | + } |
641 | + } |
642 | + } |
643 | diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs |
644 | new file mode 100644 |
645 | index 0000000..2cc6e43 |
646 | --- /dev/null |
647 | +++ b/src/dkim/mod.rs |
648 | @@ -0,0 +1,151 @@ |
649 | + /* |
650 | + * Copyright Stalwart Labs Ltd. See the COPYING |
651 | + * file at the top-level directory of this distribution. |
652 | + * |
653 | + * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
654 | + * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
655 | + * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
656 | + * option. This file may not be copied, modified, or distributed |
657 | + * except according to those terms. |
658 | + */ |
659 | + |
660 | + use std::{borrow::Cow, fmt::Display}; |
661 | + |
662 | + use rsa::{RsaPrivateKey, RsaPublicKey}; |
663 | + |
664 | + pub mod canonicalize; |
665 | + pub mod parse; |
666 | + pub mod sign; |
667 | + |
668 | + #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
669 | + pub enum Canonicalization { |
670 | + Relaxed, |
671 | + Simple, |
672 | + } |
673 | + |
674 | + #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
675 | + pub enum Algorithm { |
676 | + Sha1, |
677 | + Sha256, |
678 | + } |
679 | + |
680 | + #[derive(Debug)] |
681 | + pub enum Error { |
682 | + ParseError, |
683 | + MissingParameters, |
684 | + NoHeadersFound, |
685 | + RSA(rsa::errors::Error), |
686 | + PKCS(rsa::pkcs1::Error), |
687 | + SPKI(rsa::pkcs8::spki::Error), |
688 | + |
689 | + /// I/O error |
690 | + Io(std::io::Error), |
691 | + |
692 | + /// Base64 decode/encode error |
693 | + Base64, |
694 | + |
695 | + UnsupportedVersion, |
696 | + UnsupportedAlgorithm, |
697 | + UnsupportedCanonicalization, |
698 | + |
699 | + UnsupportedRecordVersion, |
700 | + UnsupportedKeyType, |
701 | + } |
702 | + |
703 | + pub type Result<T> = std::result::Result<T, Error>; |
704 | + |
705 | + #[derive(Clone)] |
706 | + pub struct DKIMSigner<'x> { |
707 | + private_key: RsaPrivateKey, |
708 | + sign_headers: Vec<Cow<'x, [u8]>>, |
709 | + a: Algorithm, |
710 | + d: Cow<'x, [u8]>, |
711 | + s: Cow<'x, [u8]>, |
712 | + i: Cow<'x, [u8]>, |
713 | + l: bool, |
714 | + x: u64, |
715 | + ch: Canonicalization, |
716 | + cb: Canonicalization, |
717 | + } |
718 | + |
719 | + #[derive(Debug, PartialEq, Eq, Clone)] |
720 | + pub struct Signature<'x> { |
721 | + v: u32, |
722 | + a: Algorithm, |
723 | + d: Cow<'x, [u8]>, |
724 | + s: Cow<'x, [u8]>, |
725 | + b: Vec<u8>, |
726 | + bh: Vec<u8>, |
727 | + h: Vec<Vec<u8>>, |
728 | + z: Vec<Vec<u8>>, |
729 | + i: Cow<'x, [u8]>, |
730 | + l: u64, |
731 | + x: u64, |
732 | + t: u64, |
733 | + ch: Canonicalization, |
734 | + cb: Canonicalization, |
735 | + } |
736 | + |
737 | + #[derive(Debug, PartialEq, Eq, Clone)] |
738 | + pub(crate) struct Record { |
739 | + v: Version, |
740 | + h: Vec<Algorithm>, |
741 | + p: Key, |
742 | + s: Vec<Service>, |
743 | + t: Vec<Flag>, |
744 | + } |
745 | + |
746 | + #[derive(Debug, PartialEq, Eq, Clone)] |
747 | + pub(crate) enum Version { |
748 | + Dkim1, |
749 | + } |
750 | + |
751 | + #[derive(Debug, PartialEq, Eq, Clone)] |
752 | + pub(crate) enum Service { |
753 | + All, |
754 | + Email, |
755 | + } |
756 | + |
757 | + #[derive(Debug, PartialEq, Eq, Clone)] |
758 | + pub(crate) enum Flag { |
759 | + Testing, |
760 | + MatchDomain, |
761 | + } |
762 | + |
763 | + #[derive(Debug, PartialEq, Eq, Clone)] |
764 | + pub(crate) enum Key { |
765 | + Rsa(RsaPublicKey), |
766 | + Revoked, |
767 | + } |
768 | + |
769 | + impl Display for Error { |
770 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
771 | + match self { |
772 | + Error::ParseError => write!(f, "Parse error"), |
773 | + Error::MissingParameters => write!(f, "Missing parameters"), |
774 | + Error::NoHeadersFound => write!(f, "No headers found"), |
775 | + Error::RSA(err) => write!(f, "RSA error: {}", err), |
776 | + Error::PKCS(err) => write!(f, "PKCS error: {}", err), |
777 | + Error::SPKI(err) => write!(f, "SPKI error: {}", err), |
778 | + Error::Io(e) => write!(f, "I/O error: {}", e), |
779 | + Error::Base64 => write!(f, "Base64 encode or decode error."), |
780 | + Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature."), |
781 | + Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature."), |
782 | + Error::UnsupportedCanonicalization => { |
783 | + write!(f, "Unsupported canonicalization method in DKIM Signature.") |
784 | + } |
785 | + Error::UnsupportedRecordVersion => { |
786 | + write!(f, "Unsupported version in DKIM DNS record.") |
787 | + } |
788 | + Error::UnsupportedKeyType => { |
789 | + write!(f, "Unsupported key type in DKIM DNS record.") |
790 | + } |
791 | + } |
792 | + } |
793 | + } |
794 | + |
795 | + impl From<std::io::Error> for Error { |
796 | + fn from(err: std::io::Error) -> Self { |
797 | + Error::Io(err) |
798 | + } |
799 | + } |
800 | diff --git a/src/dkim/parse.rs b/src/dkim/parse.rs |
801 | new file mode 100644 |
802 | index 0000000..e569f93 |
803 | --- /dev/null |
804 | +++ b/src/dkim/parse.rs |
805 | @@ -0,0 +1,588 @@ |
806 | + use std::slice::Iter; |
807 | + |
808 | + use mail_parser::decoders::base64::base64_decode_stream; |
809 | + use rsa::{pkcs8::DecodePublicKey, RsaPublicKey}; |
810 | + |
811 | + use crate::common::parse::{ItemParser, TagParser}; |
812 | + |
813 | + use super::{Algorithm, Canonicalization, Error, Flag, Record, Service, Signature, Version}; |
814 | + |
815 | + enum Key { |
816 | + V, |
817 | + A, |
818 | + B, |
819 | + BH, |
820 | + C, |
821 | + D, |
822 | + H, |
823 | + I, |
824 | + L, |
825 | + Q, |
826 | + S, |
827 | + T, |
828 | + X, |
829 | + Z, |
830 | + Unknown, |
831 | + } |
832 | + |
833 | + impl<'x> Signature<'x> { |
834 | + #[allow(clippy::while_let_on_iterator)] |
835 | + pub fn parse(header: &'x [u8]) -> super::Result<Self> { |
836 | + let mut signature = Signature { |
837 | + v: 0, |
838 | + a: Algorithm::Sha256, |
839 | + d: (b""[..]).into(), |
840 | + s: (b""[..]).into(), |
841 | + i: (b""[..]).into(), |
842 | + b: Vec::with_capacity(0), |
843 | + bh: Vec::with_capacity(0), |
844 | + h: Vec::with_capacity(0), |
845 | + z: Vec::with_capacity(0), |
846 | + l: 0, |
847 | + x: 0, |
848 | + t: 0, |
849 | + ch: Canonicalization::Simple, |
850 | + cb: Canonicalization::Simple, |
851 | + }; |
852 | + let header_len = header.len(); |
853 | + let mut header = header.iter(); |
854 | + |
855 | + while let Some(key) = Key::parse(&mut header) { |
856 | + match key { |
857 | + Key::V => { |
858 | + while let Some(&ch) = header.next() { |
859 | + match ch { |
860 | + b'1' if signature.v == 0 => { |
861 | + signature.v = 1; |
862 | + } |
863 | + b';' => { |
864 | + break; |
865 | + } |
866 | + _ => { |
867 | + if !ch.is_ascii_whitespace() { |
868 | + return Err(Error::UnsupportedVersion); |
869 | + } |
870 | + } |
871 | + } |
872 | + } |
873 | + if signature.v != 1 { |
874 | + return Err(Error::UnsupportedVersion); |
875 | + } |
876 | + } |
877 | + Key::A => { |
878 | + if header.match_bytes(b"rsa-sha") { |
879 | + let mut algo = 0; |
880 | + |
881 | + while let Some(ch) = header.next() { |
882 | + match ch { |
883 | + b'1' if algo == 0 => algo = 1, |
884 | + b'2' if algo == 0 => algo = 2, |
885 | + b'5' if algo == 2 => algo = 25, |
886 | + b'6' if algo == 25 => algo = 256, |
887 | + b';' => { |
888 | + break; |
889 | + } |
890 | + _ => { |
891 | + if !ch.is_ascii_whitespace() { |
892 | + return Err(Error::UnsupportedAlgorithm); |
893 | + } |
894 | + } |
895 | + } |
896 | + } |
897 | + |
898 | + signature.a = match algo { |
899 | + 256 => Algorithm::Sha256, |
900 | + 1 => Algorithm::Sha1, |
901 | + _ => return Err(Error::UnsupportedAlgorithm), |
902 | + }; |
903 | + } else { |
904 | + return Err(Error::UnsupportedAlgorithm); |
905 | + } |
906 | + } |
907 | + Key::B => { |
908 | + signature.b = |
909 | + base64_decode_stream(&mut header, header_len, b';').ok_or(Error::Base64)? |
910 | + } |
911 | + Key::BH => { |
912 | + signature.bh = |
913 | + base64_decode_stream(&mut header, header_len, b';').ok_or(Error::Base64)? |
914 | + } |
915 | + Key::C => { |
916 | + let mut has_header = false; |
917 | + let mut c = None; |
918 | + |
919 | + while let Some(ch) = header.next() { |
920 | + match (ch, c) { |
921 | + (b's' | b'S', None) => { |
922 | + if header.match_bytes(b"imple") { |
923 | + c = Canonicalization::Simple.into(); |
924 | + } else { |
925 | + return Err(Error::UnsupportedAlgorithm); |
926 | + } |
927 | + } |
928 | + (b'r' | b'R', None) => { |
929 | + if header.match_bytes(b"elaxed") { |
930 | + c = Canonicalization::Relaxed.into(); |
931 | + } else { |
932 | + return Err(Error::UnsupportedAlgorithm); |
933 | + } |
934 | + } |
935 | + (b'/', Some(c_)) => { |
936 | + signature.ch = c_; |
937 | + c = None; |
938 | + has_header = true; |
939 | + } |
940 | + (b';', _) => { |
941 | + break; |
942 | + } |
943 | + (_, _) => { |
944 | + if !ch.is_ascii_whitespace() { |
945 | + return Err(Error::UnsupportedCanonicalization); |
946 | + } |
947 | + } |
948 | + } |
949 | + } |
950 | + |
951 | + if let Some(c) = c { |
952 | + if has_header { |
953 | + signature.cb = c; |
954 | + } else { |
955 | + signature.ch = c; |
956 | + } |
957 | + } |
958 | + } |
959 | + Key::D => signature.d = header.get_tag().into(), |
960 | + Key::H => signature.h = header.get_items(b':'), |
961 | + Key::I => signature.i = header.get_tag_qp().into(), |
962 | + Key::L => signature.l = header.get_number(), |
963 | + Key::S => signature.s = header.get_tag().into(), |
964 | + Key::T => signature.t = header.get_number(), |
965 | + Key::X => signature.x = header.get_number(), |
966 | + Key::Z => signature.z = header.get_headers_qp(), |
967 | + Key::Q | Key::Unknown => header.ignore(), |
968 | + } |
969 | + } |
970 | + |
971 | + if !signature.d.is_empty() |
972 | + && !signature.d.is_empty() |
973 | + && !signature.b.is_empty() |
974 | + && !signature.bh.is_empty() |
975 | + && !signature.h.is_empty() |
976 | + { |
977 | + Ok(signature) |
978 | + } else { |
979 | + Err(Error::MissingParameters) |
980 | + } |
981 | + } |
982 | + } |
983 | + |
984 | + impl Key { |
985 | + #[allow(clippy::while_let_on_iterator)] |
986 | + fn parse(header: &mut Iter<'_, u8>) -> Option<Self> { |
987 | + let mut key = Key::Unknown; |
988 | + |
989 | + while let Some(ch) = header.next() { |
990 | + key = match ch { |
991 | + b'v' | b'V' => Key::V, |
992 | + b'a' | b'A' => Key::A, |
993 | + b'b' | b'B' => Key::B, |
994 | + b'c' | b'C' => Key::C, |
995 | + b'd' | b'D' => Key::D, |
996 | + b'h' | b'H' => Key::H, |
997 | + b'i' | b'I' => Key::I, |
998 | + b'l' | b'L' => Key::L, |
999 | + b'q' | b'Q' => Key::Q, |
1000 | + b's' | b'S' => Key::S, |
1001 | + b't' | b'T' => Key::T, |
1002 | + b'x' | b'X' => Key::X, |
1003 | + b'z' | b'Z' => Key::Z, |
1004 | + b' ' | b'\t' | b'\r' | b'\n' | b';' => continue, |
1005 | + _ => Key::Unknown, |
1006 | + }; |
1007 | + break; |
1008 | + } |
1009 | + |
1010 | + while let Some(ch) = header.next() { |
1011 | + match ch { |
1012 | + b'=' => { |
1013 | + return key.into(); |
1014 | + } |
1015 | + b' ' | b'\t' | b'\r' | b'\n' => (), |
1016 | + b'h' | b'H' if matches!(key, Key::B) => { |
1017 | + key = Key::BH; |
1018 | + } |
1019 | + _ => { |
1020 | + key = Key::Unknown; |
1021 | + } |
1022 | + } |
1023 | + } |
1024 | + |
1025 | + None |
1026 | + } |
1027 | + } |
1028 | + |
1029 | + impl Record { |
1030 | + #[allow(clippy::while_let_on_iterator)] |
1031 | + pub fn parse(header: &[u8]) -> super::Result<Self> { |
1032 | + let header_len = header.len(); |
1033 | + let mut header = header.iter(); |
1034 | + let mut key = 0; |
1035 | + let mut record = Record { |
1036 | + v: Version::Dkim1, |
1037 | + h: Vec::new(), |
1038 | + p: super::Key::Revoked, |
1039 | + s: Vec::new(), |
1040 | + t: Vec::new(), |
1041 | + }; |
1042 | + let mut public_key = Vec::new(); |
1043 | + |
1044 | + while let Some(&ch) = header.next() { |
1045 | + match ch { |
1046 | + b' ' | b'\t' | b'\r' | b'\n' => (), |
1047 | + b'=' => { |
1048 | + match key { |
1049 | + b'v' | b'V' => { |
1050 | + if !header.match_bytes(b"DKIM1") || !header.skip_whitespaces() { |
1051 | + return Err(Error::UnsupportedRecordVersion); |
1052 | + } |
1053 | + } |
1054 | + b'h' | b'H' => record.h = header.get_items(b':'), |
1055 | + b'p' | b'P' => { |
1056 | + public_key = base64_decode_stream(&mut header, header_len, b';') |
1057 | + .unwrap_or_default() |
1058 | + } |
1059 | + b's' | b'S' => record.s = header.get_items(b':'), |
1060 | + b't' | b'T' => record.t = header.get_items(b':'), |
1061 | + b'k' | b'K' => { |
1062 | + if !header.match_bytes(b"rsa") || !header.skip_whitespaces() { |
1063 | + return Err(Error::UnsupportedKeyType); |
1064 | + } |
1065 | + } |
1066 | + _ => { |
1067 | + header.ignore(); |
1068 | + } |
1069 | + } |
1070 | + key = 0; |
1071 | + } |
1072 | + b';' => { |
1073 | + key = 0; |
1074 | + } |
1075 | + _ => { |
1076 | + if key == 0 { |
1077 | + key = ch; |
1078 | + } else { |
1079 | + key = u8::MAX; |
1080 | + } |
1081 | + } |
1082 | + } |
1083 | + } |
1084 | + |
1085 | + if !public_key.is_empty() { |
1086 | + record.p = super::Key::Rsa( |
1087 | + RsaPublicKey::from_public_key_der(&public_key).map_err(Error::SPKI)?, |
1088 | + ) |
1089 | + } |
1090 | + |
1091 | + Ok(record) |
1092 | + } |
1093 | + } |
1094 | + |
1095 | + impl ItemParser for Algorithm { |
1096 | + fn parse(bytes: &[u8]) -> Option<Self> { |
1097 | + if bytes.eq_ignore_ascii_case(b"sha256") { |
1098 | + Algorithm::Sha256.into() |
1099 | + } else if bytes.eq_ignore_ascii_case(b"sha1") { |
1100 | + Algorithm::Sha1.into() |
1101 | + } else { |
1102 | + None |
1103 | + } |
1104 | + } |
1105 | + } |
1106 | + |
1107 | + impl ItemParser for Flag { |
1108 | + fn parse(bytes: &[u8]) -> Option<Self> { |
1109 | + if bytes.eq_ignore_ascii_case(b"y") { |
1110 | + Flag::Testing.into() |
1111 | + } else if bytes.eq_ignore_ascii_case(b"s") { |
1112 | + Flag::MatchDomain.into() |
1113 | + } else { |
1114 | + None |
1115 | + } |
1116 | + } |
1117 | + } |
1118 | + |
1119 | + impl ItemParser for Service { |
1120 | + fn parse(bytes: &[u8]) -> Option<Self> { |
1121 | + if bytes.eq(b"*") { |
1122 | + Service::All.into() |
1123 | + } else if bytes.eq_ignore_ascii_case(b"email") { |
1124 | + Service::Email.into() |
1125 | + } else { |
1126 | + None |
1127 | + } |
1128 | + } |
1129 | + } |
1130 | + |
1131 | + #[cfg(test)] |
1132 | + mod test { |
1133 | + use mail_parser::decoders::base64::base64_decode; |
1134 | + use rsa::{pkcs8::DecodePublicKey, RsaPublicKey}; |
1135 | + |
1136 | + use crate::dkim::{ |
1137 | + Algorithm, Canonicalization, Flag, Key, Record, Service, Signature, Version, |
1138 | + }; |
1139 | + |
1140 | + #[test] |
1141 | + fn dkim_signature_parse() { |
1142 | + for (signature, expected_result) in [ |
1143 | + ( |
1144 | + concat!( |
1145 | + "v=1; a=rsa-sha256; s=default; d=stalw.art; c=relaxed/relaxed; ", |
1146 | + "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=; ", |
1147 | + "b=Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv\n", |
1148 | + " eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR\n", |
1149 | + "\t9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=;", |
1150 | + "h=Subject:To:From; t=311923920", |
1151 | + ), |
1152 | + Signature { |
1153 | + v: 1, |
1154 | + a: Algorithm::Sha256, |
1155 | + d: (b"stalw.art"[..]).into(), |
1156 | + s: (b"default"[..]).into(), |
1157 | + i: (b""[..]).into(), |
1158 | + bh: base64_decode(b"QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=").unwrap(), |
1159 | + b: base64_decode( |
1160 | + concat!( |
1161 | + "Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv", |
1162 | + "eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR", |
1163 | + "9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=" |
1164 | + ) |
1165 | + .as_bytes(), |
1166 | + ) |
1167 | + .unwrap(), |
1168 | + h: vec![b"Subject".to_vec(), b"To".to_vec(), b"From".to_vec()], |
1169 | + z: vec![], |
1170 | + l: 0, |
1171 | + x: 0, |
1172 | + t: 311923920, |
1173 | + ch: Canonicalization::Relaxed, |
1174 | + cb: Canonicalization::Relaxed, |
1175 | + }, |
1176 | + ), |
1177 | + ( |
1178 | + concat!( |
1179 | + "v=1; a=rsa-sha1; d=example.net; s=brisbane;\r\n", |
1180 | + " c=simple; q=dns/txt; i=@eng.example.net;\r\n", |
1181 | + " t=1117574938; x=1118006938;\r\n", |
1182 | + " h=from:to:subject:date;\r\n", |
1183 | + " z=From:foo@eng.example.net|To:joe@example.com|\r\n", |
1184 | + " Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;\r\n", |
1185 | + " bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n", |
1186 | + " b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR", |
1187 | + ), |
1188 | + Signature { |
1189 | + v: 1, |
1190 | + a: Algorithm::Sha1, |
1191 | + d: (b"example.net"[..]).into(), |
1192 | + s: (b"brisbane"[..]).into(), |
1193 | + i: (b"@eng.example.net"[..]).into(), |
1194 | + bh: base64_decode(b"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=").unwrap(), |
1195 | + b: base64_decode( |
1196 | + concat!( |
1197 | + "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGe", |
1198 | + "eruD00lszZVoG4ZHRNiYzR" |
1199 | + ) |
1200 | + .as_bytes(), |
1201 | + ) |
1202 | + .unwrap(), |
1203 | + h: vec![ |
1204 | + b"from".to_vec(), |
1205 | + b"to".to_vec(), |
1206 | + b"subject".to_vec(), |
1207 | + b"date".to_vec(), |
1208 | + ], |
1209 | + z: vec![ |
1210 | + b"From:foo@eng.example.net".to_vec(), |
1211 | + b"To:joe@example.com".to_vec(), |
1212 | + b"Subject:demo run".to_vec(), |
1213 | + b"Date:July 5, 2005 3:44:08 PM -0700".to_vec(), |
1214 | + ], |
1215 | + l: 0, |
1216 | + x: 1118006938, |
1217 | + t: 1117574938, |
1218 | + ch: Canonicalization::Simple, |
1219 | + cb: Canonicalization::Simple, |
1220 | + }, |
1221 | + ), |
1222 | + ( |
1223 | + concat!( |
1224 | + "v=1; a = rsa - sha256; s = brisbane; d = example.com; \r\n", |
1225 | + "c = simple / relaxed; q=dns/txt; i = \r\n joe=20@\r\n", |
1226 | + " football.example.com; \r\n", |
1227 | + "h=Received : From : To :\r\n Subject : : Date : Message-ID::;;;; \r\n", |
1228 | + "bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; \r\n", |
1229 | + "b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB \r\n", |
1230 | + "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut \r\n", |
1231 | + "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV \r\n", |
1232 | + "4bmp/YzhwvcubU4=; l = 123", |
1233 | + ), |
1234 | + Signature { |
1235 | + v: 1, |
1236 | + a: Algorithm::Sha256, |
1237 | + d: (b"example.com"[..]).into(), |
1238 | + s: (b"brisbane"[..]).into(), |
1239 | + i: (b"joe @football.example.com"[..]).into(), |
1240 | + bh: base64_decode(b"2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=").unwrap(), |
1241 | + b: base64_decode( |
1242 | + concat!( |
1243 | + "AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB", |
1244 | + "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut", |
1245 | + "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV", |
1246 | + "4bmp/YzhwvcubU4=" |
1247 | + ) |
1248 | + .as_bytes(), |
1249 | + ) |
1250 | + .unwrap(), |
1251 | + h: vec![ |
1252 | + b"Received".to_vec(), |
1253 | + b"From".to_vec(), |
1254 | + b"To".to_vec(), |
1255 | + b"Subject".to_vec(), |
1256 | + b"Date".to_vec(), |
1257 | + b"Message-ID".to_vec(), |
1258 | + ], |
1259 | + z: vec![], |
1260 | + l: 123, |
1261 | + x: 0, |
1262 | + t: 0, |
1263 | + ch: Canonicalization::Simple, |
1264 | + cb: Canonicalization::Relaxed, |
1265 | + }, |
1266 | + ), |
1267 | + ] { |
1268 | + let result = Signature::parse(signature.as_bytes()).unwrap(); |
1269 | + assert_eq!(result.v, expected_result.v, "{:?}", signature); |
1270 | + assert_eq!(result.a, expected_result.a, "{:?}", signature); |
1271 | + assert_eq!(result.d, expected_result.d, "{:?}", signature); |
1272 | + assert_eq!(result.s, expected_result.s, "{:?}", signature); |
1273 | + assert_eq!(result.i, expected_result.i, "{:?}", signature); |
1274 | + assert_eq!(result.b, expected_result.b, "{:?}", signature); |
1275 | + assert_eq!(result.bh, expected_result.bh, "{:?}", signature); |
1276 | + assert_eq!(result.h, expected_result.h, "{:?}", signature); |
1277 | + assert_eq!(result.z, expected_result.z, "{:?}", signature); |
1278 | + assert_eq!(result.l, expected_result.l, "{:?}", signature); |
1279 | + assert_eq!(result.x, expected_result.x, "{:?}", signature); |
1280 | + assert_eq!(result.t, expected_result.t, "{:?}", signature); |
1281 | + assert_eq!(result.ch, expected_result.ch, "{:?}", signature); |
1282 | + assert_eq!(result.cb, expected_result.cb, "{:?}", signature); |
1283 | + } |
1284 | + } |
1285 | + |
1286 | + #[test] |
1287 | + fn dkim_record_parse() { |
1288 | + for (record, expected_result) in [ |
1289 | + ( |
1290 | + concat!( |
1291 | + "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ", |
1292 | + "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt", |
1293 | + "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v", |
1294 | + "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi", |
1295 | + "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB", |
1296 | + ), |
1297 | + Record { |
1298 | + v: Version::Dkim1, |
1299 | + h: Vec::new(), |
1300 | + p: Key::Rsa( |
1301 | + RsaPublicKey::from_public_key_der( |
1302 | + &base64_decode( |
1303 | + concat!( |
1304 | + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ", |
1305 | + "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt", |
1306 | + "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v", |
1307 | + "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi", |
1308 | + "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB", |
1309 | + ) |
1310 | + .as_bytes(), |
1311 | + ) |
1312 | + .unwrap(), |
1313 | + ) |
1314 | + .unwrap(), |
1315 | + ), |
1316 | + s: Vec::new(), |
1317 | + t: Vec::new(), |
1318 | + }, |
1319 | + ), |
1320 | + ( |
1321 | + concat!( |
1322 | + "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC", |
1323 | + "AQ8AMIIBCgKCAQEAvzwKQIIWzQXv0nihasFTT3+JO23hXCg", |
1324 | + "e+ESWNxCJdVLxKL5edxrumEU3DnrPeGD6q6E/vjoXwBabpm", |
1325 | + "8F5o96MEPm7v12O5IIK7wx7gIJiQWvexwh+GJvW4aFFa0g1", |
1326 | + "3Ai75UdZjGFNKHAEGeLmkQYybK/EHW5ymRlSg3g8zydJGEc", |
1327 | + "I/melLCiBoShHjfZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W", |
1328 | + "7qnq6t9puZYXMWj6tEaf6ORWgb7DOXZSTJJjAJPBWa2+Urx", |
1329 | + "XX6Ro7L7Xy1zzeYFCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9", |
1330 | + "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;", |
1331 | + "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes " |
1332 | + ), |
1333 | + Record { |
1334 | + v: Version::Dkim1, |
1335 | + h: vec![Algorithm::Sha1, Algorithm::Sha256], |
1336 | + p: Key::Rsa( |
1337 | + RsaPublicKey::from_public_key_der( |
1338 | + &base64_decode( |
1339 | + concat!( |
1340 | + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvz", |
1341 | + "wKQIIWzQXv0nihasFTT3+JO23hXCge+ESWNxCJdVLxKL5e", |
1342 | + "dxrumEU3DnrPeGD6q6E/vjoXwBabpm8F5o96MEPm7v12O5", |
1343 | + "IIK7wx7gIJiQWvexwh+GJvW4aFFa0g13Ai75UdZjGFNKHA", |
1344 | + "EGeLmkQYybK/EHW5ymRlSg3g8zydJGEcI/melLCiBoShHjf", |
1345 | + "ZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W7qnq6t9puZYXMWj", |
1346 | + "6tEaf6ORWgb7DOXZSTJJjAJPBWa2+UrxXX6Ro7L7Xy1zzeY", |
1347 | + "FCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9p5wMedWasaPS74", |
1348 | + "TZ1b7tI39ncp6QIDAQAB", |
1349 | + ) |
1350 | + .as_bytes(), |
1351 | + ) |
1352 | + .unwrap(), |
1353 | + ) |
1354 | + .unwrap(), |
1355 | + ), |
1356 | + s: vec![Service::All, Service::Email], |
1357 | + t: vec![Flag::Testing, Flag::MatchDomain], |
1358 | + }, |
1359 | + ), |
1360 | + ( |
1361 | + concat!( |
1362 | + "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS", |
1363 | + "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT", |
1364 | + "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk", |
1365 | + "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB" |
1366 | + ), |
1367 | + Record { |
1368 | + v: Version::Dkim1, |
1369 | + h: vec![], |
1370 | + p: Key::Rsa( |
1371 | + RsaPublicKey::from_public_key_der( |
1372 | + &base64_decode( |
1373 | + concat!( |
1374 | + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS", |
1375 | + "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT", |
1376 | + "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk", |
1377 | + "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB" |
1378 | + ) |
1379 | + .as_bytes(), |
1380 | + ) |
1381 | + .unwrap(), |
1382 | + ) |
1383 | + .unwrap(), |
1384 | + ), |
1385 | + s: vec![], |
1386 | + t: vec![], |
1387 | + }, |
1388 | + ), |
1389 | + ] { |
1390 | + assert_eq!(Record::parse(record.as_bytes()).unwrap(), expected_result); |
1391 | + } |
1392 | + } |
1393 | + } |
1394 | diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs |
1395 | new file mode 100644 |
1396 | index 0000000..fbd5f3b |
1397 | --- /dev/null |
1398 | +++ b/src/dkim/sign.rs |
1399 | @@ -0,0 +1,328 @@ |
1400 | + /* |
1401 | + * Copyright Stalwart Labs Ltd. See the COPYING |
1402 | + * file at the top-level directory of this distribution. |
1403 | + * |
1404 | + * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
1405 | + * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
1406 | + * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
1407 | + * option. This file may not be copied, modified, or distributed |
1408 | + * except according to those terms. |
1409 | + */ |
1410 | + |
1411 | + use std::{ |
1412 | + borrow::Cow, |
1413 | + fmt::{Display, Formatter}, |
1414 | + io::Write, |
1415 | + path::Path, |
1416 | + time::SystemTime, |
1417 | + }; |
1418 | + |
1419 | + use mail_builder::encoders::base64::base64_encode; |
1420 | + use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::AssociatedOid, PaddingScheme, RsaPrivateKey}; |
1421 | + use sha1::Sha1; |
1422 | + use sha2::{Digest, Sha256}; |
1423 | + |
1424 | + use super::{Algorithm, Canonicalization, DKIMSigner, Error, Signature}; |
1425 | + |
1426 | + impl<'x> DKIMSigner<'x> { |
1427 | + /// Creates a new DKIM signer from a PKCS1 PEM file. |
1428 | + pub fn from_pkcs1_pem_file(path: &str) -> super::Result<Self> { |
1429 | + DKIMSigner::from_rsa_pkey( |
1430 | + RsaPrivateKey::read_pkcs1_pem_file(Path::new(path)).map_err(Error::PKCS)?, |
1431 | + ) |
1432 | + } |
1433 | + |
1434 | + /// Creates a new DKIM signer from a PKCS1 PEM string. |
1435 | + pub fn from_pkcs1_pem(pem: &str) -> super::Result<Self> { |
1436 | + DKIMSigner::from_rsa_pkey(RsaPrivateKey::from_pkcs1_pem(pem).map_err(Error::PKCS)?) |
1437 | + } |
1438 | + |
1439 | + /// Creates a new DKIM signer from a PKCS1 binary file. |
1440 | + pub fn from_pkcs1_der_file(path: &str) -> super::Result<Self> { |
1441 | + DKIMSigner::from_rsa_pkey( |
1442 | + RsaPrivateKey::read_pkcs1_der_file(Path::new(path)).map_err(Error::PKCS)?, |
1443 | + ) |
1444 | + } |
1445 | + |
1446 | + /// Creates a new DKIM signer from a PKCS1 binary slice. |
1447 | + pub fn from_pkcs1_der(bytes: &[u8]) -> super::Result<Self> { |
1448 | + DKIMSigner::from_rsa_pkey(RsaPrivateKey::from_pkcs1_der(bytes).map_err(Error::PKCS)?) |
1449 | + } |
1450 | + |
1451 | + /// Creates a new DKIM signer from an RsaPrivateKey. |
1452 | + pub fn from_rsa_pkey(private_key: RsaPrivateKey) -> super::Result<Self> { |
1453 | + Ok(DKIMSigner { |
1454 | + private_key, |
1455 | + sign_headers: Vec::with_capacity(0), |
1456 | + cb: Canonicalization::Relaxed, |
1457 | + ch: Canonicalization::Relaxed, |
1458 | + a: Algorithm::Sha256, |
1459 | + d: (b""[..]).into(), |
1460 | + s: (b""[..]).into(), |
1461 | + i: (b""[..]).into(), |
1462 | + l: false, |
1463 | + x: 0, |
1464 | + }) |
1465 | + } |
1466 | + |
1467 | + /// Sets the headers to sign. |
1468 | + pub fn headers(mut self, headers: impl IntoIterator<Item = &'x str>) -> Self { |
1469 | + self.sign_headers = headers |
1470 | + .into_iter() |
1471 | + .map(|h| Cow::Borrowed(h.as_bytes())) |
1472 | + .collect(); |
1473 | + self |
1474 | + } |
1475 | + |
1476 | + /// Sets the domain to use for signing. |
1477 | + pub fn domain(mut self, domain: impl Into<Cow<'x, str>>) -> Self { |
1478 | + self.d = match domain.into() { |
1479 | + Cow::Borrowed(v) => v.as_bytes().into(), |
1480 | + Cow::Owned(v) => v.into_bytes().into(), |
1481 | + }; |
1482 | + self |
1483 | + } |
1484 | + |
1485 | + /// Sets the selector to use for signing. |
1486 | + pub fn selector(mut self, selector: impl Into<Cow<'x, str>>) -> Self { |
1487 | + self.s = match selector.into() { |
1488 | + Cow::Borrowed(v) => v.as_bytes().into(), |
1489 | + Cow::Owned(v) => v.into_bytes().into(), |
1490 | + }; |
1491 | + self |
1492 | + } |
1493 | + |
1494 | + /// Sets the selector to use for signing. |
1495 | + pub fn agent_user_identifier(mut self, auid: impl Into<Cow<'x, str>>) -> Self { |
1496 | + self.i = match auid.into() { |
1497 | + Cow::Borrowed(v) => v.as_bytes().into(), |
1498 | + Cow::Owned(v) => v.into_bytes().into(), |
1499 | + }; |
1500 | + self |
1501 | + } |
1502 | + |
1503 | + /// Sets the number of seconds from now to use for the signature expiration. |
1504 | + pub fn expiration(mut self, expiration: u64) -> Self { |
1505 | + self.x = expiration; |
1506 | + self |
1507 | + } |
1508 | + |
1509 | + /// Include the body length in the signature. |
1510 | + pub fn body_length(mut self, body_length: bool) -> Self { |
1511 | + self.l = body_length; |
1512 | + self |
1513 | + } |
1514 | + |
1515 | + /// Sets header canonicalization algorithm. |
1516 | + pub fn header_canonicalization(mut self, ch: Canonicalization) -> Self { |
1517 | + self.ch = ch; |
1518 | + self |
1519 | + } |
1520 | + |
1521 | + /// Sets header canonicalization algorithm. |
1522 | + pub fn body_canonicalization(mut self, cb: Canonicalization) -> Self { |
1523 | + self.cb = cb; |
1524 | + self |
1525 | + } |
1526 | + |
1527 | + /// Signs a message. |
1528 | + #[inline(always)] |
1529 | + pub fn sign(&self, message: &[u8]) -> super::Result<Signature> { |
1530 | + if !self.d.is_empty() && !self.s.is_empty() { |
1531 | + let now = SystemTime::now() |
1532 | + .duration_since(SystemTime::UNIX_EPOCH) |
1533 | + .map(|d| d.as_secs()) |
1534 | + .unwrap_or(0); |
1535 | + match self.a { |
1536 | + Algorithm::Sha256 => self.sign_::<Sha256>(message, now), |
1537 | + Algorithm::Sha1 => self.sign_::<Sha1>(message, now), |
1538 | + } |
1539 | + } else { |
1540 | + Err(Error::MissingParameters) |
1541 | + } |
1542 | + } |
1543 | + |
1544 | + fn sign_<T>(&self, message: &[u8], now: u64) -> super::Result<Signature> |
1545 | + where |
1546 | + T: Digest + AssociatedOid + std::io::Write, |
1547 | + { |
1548 | + let mut body_hasher = T::new(); |
1549 | + let mut header_hasher = T::new(); |
1550 | + |
1551 | + // Canonicalize headers and body |
1552 | + let (body_len, signed_headers) = |
1553 | + self.canonicalize(message, &mut header_hasher, &mut body_hasher)?; |
1554 | + |
1555 | + if signed_headers.is_empty() { |
1556 | + return Err(Error::NoHeadersFound); |
1557 | + } |
1558 | + |
1559 | + // Create Signature |
1560 | + let mut signature = Signature { |
1561 | + d: self.d.as_ref().into(), |
1562 | + s: self.s.as_ref().into(), |
1563 | + i: self.i.as_ref().into(), |
1564 | + b: Vec::new(), |
1565 | + bh: base64_encode(&body_hasher.finalize())?, |
1566 | + h: signed_headers, |
1567 | + t: now, |
1568 | + x: if self.x > 0 { now + self.x } else { 0 }, |
1569 | + cb: self.cb, |
1570 | + ch: self.ch, |
1571 | + v: 1, |
1572 | + a: self.a, |
1573 | + z: Vec::new(), |
1574 | + l: if self.l { body_len as u64 } else { 0 }, |
1575 | + }; |
1576 | + |
1577 | + // Add signature to hash |
1578 | + header_hasher.write_all(b"dkim-signature:")?; |
1579 | + signature.write(&mut header_hasher, false)?; |
1580 | + |
1581 | + // RSA Sign |
1582 | + signature.b = base64_encode( |
1583 | + &self |
1584 | + .private_key |
1585 | + .sign( |
1586 | + PaddingScheme::new_pkcs1v15_sign::<T>(), |
1587 | + &header_hasher.finalize(), |
1588 | + ) |
1589 | + .map_err(Error::RSA)?, |
1590 | + )?; |
1591 | + |
1592 | + Ok(signature) |
1593 | + } |
1594 | + } |
1595 | + |
1596 | + impl<'x> Signature<'x> { |
1597 | + pub(crate) fn write(&self, mut writer: impl Write, as_header: bool) -> std::io::Result<()> { |
1598 | + if as_header { |
1599 | + writer.write_all(b"DKIM-Signature: ")?; |
1600 | + }; |
1601 | + writer.write_all(b"v=1; a=")?; |
1602 | + writer.write_all(match self.a { |
1603 | + Algorithm::Sha256 => b"rsa-sha256", |
1604 | + Algorithm::Sha1 => b"rsa-sha1", |
1605 | + })?; |
1606 | + writer.write_all(b"; s=")?; |
1607 | + writer.write_all(&self.s)?; |
1608 | + writer.write_all(b"; d=")?; |
1609 | + writer.write_all(&self.d)?; |
1610 | + writer.write_all(b"; c=")?; |
1611 | + |
1612 | + self.ch.serialize_name(&mut writer)?; |
1613 | + writer.write_all(b"/")?; |
1614 | + self.cb.serialize_name(&mut writer)?; |
1615 | + |
1616 | + writer.write_all(b"; h=")?; |
1617 | + for (num, h) in self.h.iter().enumerate() { |
1618 | + if num > 0 { |
1619 | + writer.write_all(b":")?; |
1620 | + } |
1621 | + writer.write_all(h)?; |
1622 | + } |
1623 | + writer.write_all(b"; t=")?; |
1624 | + writer.write_all(self.t.to_string().as_bytes())?; |
1625 | + if self.x > 0 { |
1626 | + writer.write_all(b"; x=")?; |
1627 | + writer.write_all(self.x.to_string().as_bytes())?; |
1628 | + } |
1629 | + writer.write_all(b"; bh=")?; |
1630 | + writer.write_all(&self.bh)?; |
1631 | + writer.write_all(b"; b=")?; |
1632 | + writer.write_all(&self.b)?; |
1633 | + if !self.i.is_empty() { |
1634 | + writer.write_all(b"; i=")?; |
1635 | + for &ch in self.i.iter() { |
1636 | + match ch { |
1637 | + 0..=0x20 | b';' | 0x7f..=u8::MAX => { |
1638 | + writer.write_all(format!("={:02X}", ch).as_bytes())?; |
1639 | + } |
1640 | + _ => { |
1641 | + writer.write_all(&[ch])?; |
1642 | + } |
1643 | + } |
1644 | + } |
1645 | + } |
1646 | + if self.l > 0 { |
1647 | + writer.write_all(b"; l=")?; |
1648 | + writer.write_all(self.l.to_string().as_bytes())?; |
1649 | + } |
1650 | + writer.write_all(b";")?; |
1651 | + if as_header { |
1652 | + writer.write_all(b"\r\n")?; |
1653 | + } |
1654 | + Ok(()) |
1655 | + } |
1656 | + |
1657 | + pub fn write_header(&self, writer: impl Write) -> std::io::Result<()> { |
1658 | + self.write(writer, true) |
1659 | + } |
1660 | + |
1661 | + pub fn to_header(&self) -> String { |
1662 | + let mut buf = Vec::new(); |
1663 | + self.write(&mut buf, true).unwrap(); |
1664 | + String::from_utf8(buf).unwrap() |
1665 | + } |
1666 | + } |
1667 | + |
1668 | + impl<'x> Display for Signature<'x> { |
1669 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
1670 | + let mut buf = Vec::new(); |
1671 | + self.write(&mut buf, false).map_err(|_| std::fmt::Error)?; |
1672 | + f.write_str(&String::from_utf8(buf).map_err(|_| std::fmt::Error)?) |
1673 | + } |
1674 | + } |
1675 | + |
1676 | + #[cfg(test)] |
1677 | + mod test { |
1678 | + use sha2::Sha256; |
1679 | + |
1680 | + const TEST_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY----- |
1681 | + MIICXwIBAAKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFC |
1682 | + jxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gb |
1683 | + to/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB |
1684 | + AoGBALmn+XwWk7akvkUlqb+dOxyLB9i5VBVfje89Teolwc9YJT36BGN/l4e0l6QX |
1685 | + /1//6DWUTB3KI6wFcm7TWJcxbS0tcKZX7FsJvUz1SbQnkS54DJck1EZO/BLa5ckJ |
1686 | + gAYIaqlA9C0ZwM6i58lLlPadX/rtHb7pWzeNcZHjKrjM461ZAkEA+itss2nRlmyO |
1687 | + n1/5yDyCluST4dQfO8kAB3toSEVc7DeFeDhnC1mZdjASZNvdHS4gbLIA1hUGEF9m |
1688 | + 3hKsGUMMPwJBAPW5v/U+AWTADFCS22t72NUurgzeAbzb1HWMqO4y4+9Hpjk5wvL/ |
1689 | + eVYizyuce3/fGke7aRYw/ADKygMJdW8H/OcCQQDz5OQb4j2QDpPZc0Nc4QlbvMsj |
1690 | + 7p7otWRO5xRa6SzXqqV3+F0VpqvDmshEBkoCydaYwc2o6WQ5EBmExeV8124XAkEA |
1691 | + qZzGsIxVP+sEVRWZmW6KNFSdVUpk3qzK0Tz/WjQMe5z0UunY9Ax9/4PVhp/j61bf |
1692 | + eAYXunajbBSOLlx4D+TunwJBANkPI5S9iylsbLs6NkaMHV6k5ioHBBmgCak95JGX |
1693 | + GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc= |
1694 | + -----END RSA PRIVATE KEY-----"#; |
1695 | + |
1696 | + #[test] |
1697 | + fn dkim_sign() { |
1698 | + let dkim = super::DKIMSigner::from_pkcs1_pem(TEST_KEY) |
1699 | + .unwrap() |
1700 | + .headers(["From", "To", "Subject"]) |
1701 | + .domain("stalw.art") |
1702 | + .selector("default"); |
1703 | + let signature = dkim |
1704 | + .sign_::<Sha256>( |
1705 | + concat!( |
1706 | + "From: hello@stalw.art\r\n", |
1707 | + "To: dkim@stalw.art\r\n", |
1708 | + "Subject: Testing DKIM!\r\n\r\n", |
1709 | + "Here goes the test\r\n\r\n" |
1710 | + ) |
1711 | + .as_bytes(), |
1712 | + 311923920, |
1713 | + ) |
1714 | + .unwrap(); |
1715 | + assert_eq!( |
1716 | + concat!( |
1717 | + "v=1; a=rsa-sha256; s=default; d=stalw.art; c=relaxed/relaxed; ", |
1718 | + "h=Subject:To:From; t=311923920; ", |
1719 | + "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=; ", |
1720 | + "b=Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv", |
1721 | + "eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR", |
1722 | + "9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=;", |
1723 | + ), |
1724 | + signature.to_string() |
1725 | + ); |
1726 | + } |
1727 | + } |
1728 | diff --git a/src/lib.rs b/src/lib.rs |
1729 | new file mode 100644 |
1730 | index 0000000..9788c93 |
1731 | --- /dev/null |
1732 | +++ b/src/lib.rs |
1733 | @@ -0,0 +1,28 @@ |
1734 | + /* |
1735 | + * Copyright Stalwart Labs Ltd. See the COPYING |
1736 | + * file at the top-level directory of this distribution. |
1737 | + * |
1738 | + * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
1739 | + * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
1740 | + * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
1741 | + * option. This file may not be copied, modified, or distributed |
1742 | + * except according to those terms. |
1743 | + */ |
1744 | + |
1745 | + pub mod common; |
1746 | + pub mod dkim; |
1747 | + |
1748 | + pub fn add(left: usize, right: usize) -> usize { |
1749 | + left + right |
1750 | + } |
1751 | + |
1752 | + #[cfg(test)] |
1753 | + mod tests { |
1754 | + use super::*; |
1755 | + |
1756 | + #[test] |
1757 | + fn it_works() { |
1758 | + let result = add(2, 2); |
1759 | + assert_eq!(result, 4); |
1760 | + } |
1761 | + } |