Commit
Author: Mauro D [mauro@stalw.art]
Hash: 93270c2cf2a1863a93a33f58e9e374603f296c12
Timestamp: Thu, 03 Nov 2022 17:22:21 +0000 (2 years ago)

+1707 -0 +/-9 browse
DKIM signing and parsing.
1diff --git a/Cargo.toml b/Cargo.toml
2new file mode 100644
3index 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
21new file mode 100644
22index 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
165new file mode 100644
166index 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
173new file mode 100644
174index 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
386new file mode 100644
387index 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
644new file mode 100644
645index 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
801new file mode 100644
802index 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
1395new file mode 100644
1396index 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
1729new file mode 100644
1730index 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+ }