kumo_dkim/
parser.rs

1use crate::{canonicalization, hash, DKIMError};
2use nom::bytes::complete::{tag, take_while1};
3use nom::character::complete::alpha1;
4use nom::combinator::opt;
5use nom::multi::fold_many0;
6use nom::sequence::{delimited, pair, preceded, terminated};
7use nom::{IResult, Parser};
8
9#[derive(Clone, Debug, PartialEq)]
10/// DKIM signature tag
11pub struct Tag {
12    /// Name of the tag (v, i, a, h, ...)
13    pub name: String,
14    /// Value of the tag with spaces removed
15    pub value: String,
16    /// Value of the tag as seen in the text
17    pub raw_value: String,
18}
19
20/// Main entrypoint of the parser. Parses the DKIM signature tag list
21/// as specified <https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1>.
22/// tag-list  =  tag-spec *( ";" tag-spec ) [ ";" ]
23pub fn tag_list(input: &str) -> IResult<&str, Vec<Tag>> {
24    let (input, start) = tag_spec.parse(input)?;
25
26    terminated(
27        fold_many0(
28            preceded(tag(";"), tag_spec),
29            move || vec![start.clone()],
30            |mut acc: Vec<Tag>, item| {
31                acc.push(item);
32                acc
33            },
34        ),
35        opt(tag(";")),
36    )
37    .parse(input)
38}
39
40/// tag-spec  =  [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS]
41fn tag_spec(input: &str) -> IResult<&str, Tag> {
42    let (input, name) = delimited(opt(fws), tag_name, opt(fws)).parse(input)?;
43    let (input, _) = tag("=").parse(input)?;
44
45    // Parse the twice to keep the original text
46    let value_input = input;
47    let (_, raw_value) = delimited(opt(fws), raw_tag_value, opt(fws)).parse(value_input)?;
48    let (input, value) = delimited(opt(fws), tag_value, opt(fws)).parse(value_input)?;
49
50    Ok((
51        input,
52        Tag {
53            name: name.to_owned(),
54            value,
55            raw_value,
56        },
57    ))
58}
59
60/// tag-name  =  ALPHA *ALNUMPUNC
61/// ALNUMPUNC =  ALPHA / DIGIT / "_"
62fn tag_name(input: &str) -> IResult<&str, &str> {
63    alpha1.parse(input)
64}
65
66/// tag-value =  [ tval *( 1*(WSP / FWS) tval ) ]
67/// tval      =  1*VALCHAR
68/// VALCHAR   =  %x21-3A / %x3C-7E
69fn tag_value(input: &str) -> IResult<&str, String> {
70    let is_valchar = |c| ('!'..=':').contains(&c) || ('<'..='~').contains(&c);
71    match opt(take_while1(is_valchar)).parse(input)? {
72        (input, Some(start)) => fold_many0(
73            preceded(fws, take_while1(is_valchar)),
74            || start.to_owned(),
75            |mut acc: String, item| {
76                acc += item;
77                acc
78            },
79        )
80        .parse(input),
81        (input, None) => Ok((input, "".to_string())),
82    }
83}
84
85fn raw_tag_value(input: &str) -> IResult<&str, String> {
86    let is_valchar = |c| ('!'..=':').contains(&c) || ('<'..='~').contains(&c);
87    match opt(take_while1(is_valchar)).parse(input)? {
88        (input, Some(start)) => fold_many0(
89            pair(fws, take_while1(is_valchar)),
90            || start.to_owned(),
91            |mut acc: String, item| {
92                acc += &(item.0.to_owned() + item.1);
93                acc
94            },
95        )
96        .parse(input),
97        (input, None) => Ok((input, "".to_string())),
98    }
99}
100
101/// FWS is folding whitespace.  It allows multiple lines separated by CRLF followed by at least one whitespace, to be joined.
102fn fws(input: &str) -> IResult<&str, &str> {
103    take_while1(|c| c == ' ' || c == '\t' || c == '\r' || c == '\n').parse(input)
104}
105
106pub(crate) fn parse_hash_algo(value: &str) -> Result<hash::HashAlgo, DKIMError> {
107    use hash::HashAlgo;
108    match value {
109        "rsa-sha1" => Ok(HashAlgo::RsaSha1),
110        "rsa-sha256" => Ok(HashAlgo::RsaSha256),
111        "ed25519-sha256" => Ok(HashAlgo::Ed25519Sha256),
112        e => Err(DKIMError::UnsupportedHashAlgorithm(e.to_string())),
113    }
114}
115
116/// Parses the canonicalization value (passed in c=) and returns canonicalization
117/// for (Header, Body)
118pub(crate) fn parse_canonicalization(
119    value: Option<&str>,
120) -> Result<(canonicalization::Type, canonicalization::Type), DKIMError> {
121    use canonicalization::Type::{Relaxed, Simple};
122    match value {
123        None => Ok((Simple, Simple)),
124        Some(s) => match s {
125            "simple/simple" => Ok((Simple, Simple)),
126            "relaxed/simple" => Ok((Relaxed, Simple)),
127            "simple/relaxed" => Ok((Simple, Relaxed)),
128            "relaxed/relaxed" => Ok((Relaxed, Relaxed)),
129            "relaxed" => Ok((Relaxed, Simple)),
130            "simple" => Ok((Simple, Simple)),
131            v => Err(DKIMError::UnsupportedCanonicalizationType(v.to_owned())),
132        },
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_canonicalization_empty() {
142        use canonicalization::Type::Simple;
143        assert_eq!(parse_canonicalization(None).unwrap(), (Simple, Simple));
144    }
145
146    #[test]
147    fn test_canonicalization_one_algo() {
148        use canonicalization::Type::{Relaxed, Simple};
149
150        assert_eq!(
151            parse_canonicalization(Some("simple")).unwrap(),
152            (Simple, Simple)
153        );
154        assert_eq!(
155            parse_canonicalization(Some("relaxed")).unwrap(),
156            (Relaxed, Simple)
157        );
158    }
159
160    #[test]
161    fn test_tag_list() {
162        assert_eq!(
163            tag_list("a = a/1@.-:= ").unwrap(),
164            (
165                "",
166                vec![Tag {
167                    name: "a".to_string(),
168                    value: "a/1@.-:=".to_string(),
169                    raw_value: "a/1@.-:=".to_string()
170                }]
171            )
172        );
173        assert_eq!(
174            tag_list("a= a ; b = a\n    bc").unwrap(),
175            (
176                "",
177                vec![
178                    Tag {
179                        name: "a".to_string(),
180                        value: "a".to_string(),
181                        raw_value: "a".to_string()
182                    },
183                    Tag {
184                        name: "b".to_string(),
185                        value: "abc".to_string(),
186                        raw_value: "a\n    bc".to_string()
187                    }
188                ]
189            )
190        );
191    }
192
193    #[test]
194    fn test_tag_spec() {
195        assert_eq!(
196            tag_spec("a=b").unwrap(),
197            (
198                "",
199                Tag {
200                    name: "a".to_string(),
201                    value: "b".to_string(),
202                    raw_value: "b".to_string()
203                }
204            )
205        );
206        assert_eq!(
207            tag_spec("a=b c d e f").unwrap(),
208            (
209                "",
210                Tag {
211                    name: "a".to_string(),
212                    value: "bcdef".to_string(),
213                    raw_value: "b c d e f".to_string()
214                }
215            )
216        );
217    }
218
219    #[test]
220    fn test_tag_list_dns() {
221        assert_eq!(
222            tag_list("k=rsa; p=kEy+/").unwrap(),
223            (
224                "",
225                vec![
226                    Tag {
227                        name: "k".to_string(),
228                        value: "rsa".to_string(),
229                        raw_value: "rsa".to_string()
230                    },
231                    Tag {
232                        name: "p".to_string(),
233                        value: "kEy+/".to_string(),
234                        raw_value: "kEy+/".to_string()
235                    }
236                ]
237            )
238        );
239    }
240}