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