msg_auth_status/parser/
dkim_signature.rs

1//! Parsing for DKIM-Signature using Logos
2
3use logos::{Lexer, Logos};
4
5use crate::dkim::*;
6
7use crate::error::{DkimSignatureError, DkimTagValueError};
8
9#[cfg(feature = "mail_parser")]
10use mail_parser::HeaderValue;
11
12#[derive(Debug, Logos)]
13pub enum DkimFieldValueToken<'hdr> {
14    #[regex(r"[^;]+", |lex| lex.slice(), priority = 1)]
15    MaybeValue(&'hdr str),
16
17    #[token(";", priority = 2)]
18    FieldSep,
19}
20
21/// See RFC 6376 s. 3.5 - DKIM Tags
22#[derive(Debug, Logos)]
23#[logos(skip r"[ \t\r\n]+")]
24pub enum DkimFieldKeyToken<'hdr> {
25    #[token("v", priority = 1)]
26    TagV, // Version - RFC 6376 only defines "1"
27
28    #[token("a", priority = 1)]
29    TagA,
30
31    #[token("bh", priority = 1)]
32    TagBh,
33
34    #[token("c", priority = 1)]
35    TagC,
36
37    #[token("d", priority = 1)]
38    TagD,
39
40    #[token("h", priority = 1)]
41    TagH,
42
43    #[token("i", priority = 1)]
44    TagI,
45
46    #[token("l", priority = 1)]
47    TagL,
48
49    #[token("q", priority = 1)]
50    TagQ,
51
52    #[token("s", priority = 1)]
53    TagS,
54
55    #[token("t", priority = 1)]
56    TagT,
57
58    #[token("x", priority = 1)]
59    TagX,
60
61    #[token("z", priority = 1)]
62    TagZ,
63
64    #[token(";", priority = 1)]
65    FieldSep,
66
67    #[token("=", priority = 1)]
68    Equal,
69
70    // Must not conflict with "b"
71    #[token("b", priority = 2)]
72    TagB,
73
74    // Allows everything else alpha than above as unknown tags
75    #[regex(r"([dvacdhilqtxvz][a-z]+|b[a-gi-z]|[efgjkmnopryu][a-z]*)", |lex| lex.slice(), priority = 3)]
76    MaybeTag(&'hdr str),
77}
78
79#[derive(Clone, Debug, PartialEq)]
80pub enum DkimTagChoice<'hdr> {
81    V,
82    A,
83    B,
84    Bh,
85    C,
86    D,
87    H,
88    I,
89    L,
90    Q,
91    S,
92    T,
93    X,
94    Z,
95    // RFC 6376 s. 3.2 Unrecognised tags MUST be ignored
96    Unknown(&'hdr str),
97}
98
99impl<'hdr> DkimTagChoice<'hdr> {
100    fn from_token(token: DkimFieldKeyToken<'hdr>) -> Option<Self> {
101        let ret = match token {
102            DkimFieldKeyToken::TagV => Self::V,
103            DkimFieldKeyToken::TagA => Self::A,
104            DkimFieldKeyToken::TagB => Self::B,
105            DkimFieldKeyToken::TagBh => Self::Bh,
106            DkimFieldKeyToken::TagC => Self::C,
107            DkimFieldKeyToken::TagD => Self::D,
108            DkimFieldKeyToken::TagH => Self::H,
109            DkimFieldKeyToken::TagI => Self::I,
110            DkimFieldKeyToken::TagL => Self::L,
111            DkimFieldKeyToken::TagQ => Self::Q,
112            DkimFieldKeyToken::TagS => Self::S,
113            DkimFieldKeyToken::TagT => Self::T,
114            DkimFieldKeyToken::TagX => Self::X,
115            DkimFieldKeyToken::TagZ => Self::Z,
116            DkimFieldKeyToken::MaybeTag(tag) => Self::Unknown(tag),
117            _ => return None,
118        };
119        Some(ret)
120    }
121}
122
123#[derive(Debug, PartialEq)]
124enum Stage<'hdr> {
125    WantTag,
126    WantEq(DkimTagChoice<'hdr>),
127}
128
129// Intermediary Parsed structure to final DkimSignature
130// Final DkimSignature validates if any missing fields
131#[derive(Debug, Default, PartialEq)]
132struct ParsedDkimSignature<'hdr> {
133    /// Version
134    pub v: Option<DkimVersion<'hdr>>,
135    /// Algorithm
136    pub a: Option<DkimAlgorithm<'hdr>>,
137    /// Signature data (base64)
138    pub b: Option<&'hdr str>,
139    /// Hash of canonicalized body part of the message as limited by the 'l='
140    pub bh: Option<&'hdr str>,
141    /// Message canonicalization informs the verifier of the type of canonicalization used to prepare the message for signing. See s.3.4
142    pub c: Option<DkimCanonicalization<'hdr>>,
143    /// The SDID claiming responsibility for an introduction of a message into the mail stream
144    pub d: Option<&'hdr str>,
145    /// Signed header fields separated by colon ':' - see 'h='
146    pub h: Option<&'hdr str>,
147    /// The Agent or User Identifier (AUID) on behalf of which the SDID is taking responsibility.
148    pub i: Option<&'hdr str>,
149    /// Body length limit - see misuse on RFC 6376 s. 8.2
150    pub l: Option<&'hdr str>,
151    /// Query methods - currently only DnsTxt
152    pub q: Option<&'hdr str>,
153    /// The selector subdividing the namespace for the "d=" (domain) tag
154    pub s: Option<&'hdr str>,
155    /// Recommended - Signature Timestamp
156    pub t: Option<DkimTimestamp<'hdr>>,
157    /// Recommended - Signature Expiration
158    pub x: Option<DkimTimestamp<'hdr>>,
159    /// Copied header fields
160    pub z: Option<&'hdr str>,
161    /// Raw unparsed
162    pub raw: Option<&'hdr str>,
163}
164
165impl<'hdr> ParsedDkimSignature<'hdr> {
166    fn add_tag_value(
167        &mut self,
168        tag: DkimTagChoice<'hdr>,
169        val: &'hdr str,
170    ) -> Result<(), DkimTagValueError> {
171        match tag {
172            DkimTagChoice::V => self.v = Some(val.try_into()?),
173            DkimTagChoice::A => self.a = Some(val.try_into()?),
174            DkimTagChoice::B => self.b = Some(val),
175            DkimTagChoice::Bh => self.bh = Some(val),
176            DkimTagChoice::C => self.c = Some(val.try_into()?),
177            DkimTagChoice::D => self.d = Some(val),
178            DkimTagChoice::H => self.h = Some(val),
179            DkimTagChoice::I => self.i = Some(val),
180            DkimTagChoice::L => self.l = Some(val),
181            DkimTagChoice::Q => self.q = Some(val),
182            DkimTagChoice::S => self.s = Some(val),
183            DkimTagChoice::T => self.t = Some(val.try_into()?),
184            DkimTagChoice::X => self.x = Some(val.try_into()?),
185            DkimTagChoice::Z => self.z = Some(val),
186            // RFC 6376 s. 3.2 Unrecognised tags MUST be ignored
187            DkimTagChoice::Unknown(_) => {}
188        }
189        Ok(())
190    }
191}
192
193// TODO: It would be helpful to highlight all errors
194impl<'hdr> TryFrom<ParsedDkimSignature<'hdr>> for DkimSignature<'hdr> {
195    type Error = DkimSignatureError<'hdr>;
196
197    fn try_from(p: ParsedDkimSignature<'hdr>) -> Result<Self, Self::Error> {
198        // Required fields must be present
199        let version = match p.v {
200            Some(val) => val,
201            None => return Err(DkimSignatureError::MissingVersion),
202        };
203        let algorithm = match p.a {
204            Some(val) => val,
205            None => return Err(DkimSignatureError::MissingAlgorithm),
206        };
207        let signature = match p.b {
208            Some(val) => val,
209            None => return Err(DkimSignatureError::MissingSignature),
210        };
211        let body_hash = match p.bh {
212            Some(val) => val,
213            None => return Err(DkimSignatureError::MissingBodyHash),
214        };
215        let responsible_sdid = match p.d {
216            Some(val) => val,
217            None => return Err(DkimSignatureError::MissingResponsibleSdid),
218        };
219        let signed_header_fields = match p.h {
220            Some(val) => val,
221            None => return Err(DkimSignatureError::MissingSignedHeaderFields),
222        };
223        let selector = match p.s {
224            Some(val) => val,
225            None => return Err(DkimSignatureError::MissingSelector),
226        };
227        // Optional c, i, l, q, s, t, x, z,
228        let c = p.c;
229        let i = p.i;
230        let l = p.l;
231        let q = p.q;
232        let t = p.t;
233        let x = p.x;
234        let z = p.z;
235        let raw = p.raw;
236        Ok(Self {
237            v: version,
238            a: algorithm,
239            b: signature,
240            bh: body_hash,
241            d: responsible_sdid,
242            h: signed_header_fields,
243            s: selector,
244            c,
245            i,
246            l,
247            q,
248            t,
249            x,
250            z,
251            raw,
252        })
253    }
254}
255
256impl<'hdr> TryFrom<&'hdr HeaderValue<'hdr>> for DkimSignature<'hdr> {
257    type Error = DkimSignatureError<'hdr>;
258
259    fn try_from(hval: &'hdr HeaderValue<'hdr>) -> Result<Self, Self::Error> {
260        let text = match hval.as_text() {
261            None => return Err(DkimSignatureError::NoTagFound),
262            Some(text) => text,
263        };
264
265        let mut tag_lexer = DkimFieldKeyToken::lexer(text);
266        let mut stage = Stage::WantTag;
267        let mut res = ParsedDkimSignature {
268            raw: Some(text),
269            ..Default::default()
270        };
271
272        while let Some(token) = tag_lexer.next() {
273            match token {
274                Ok(DkimFieldKeyToken::Equal) if stage != Stage::WantTag => {
275                    stage = match stage {
276                        Stage::WantEq(ref key_tag) => {
277                            let mut value_lexer: Lexer<'hdr, DkimFieldValueToken<'hdr>> =
278                                tag_lexer.morph();
279
280                            for value_token in value_lexer.by_ref() {
281                                match value_token {
282                                    Ok(DkimFieldValueToken::MaybeValue(value)) => {
283                                        res.add_tag_value(key_tag.clone(), value)?;
284                                    }
285                                    Ok(DkimFieldValueToken::FieldSep) => {
286                                        break;
287                                    }
288                                    Err(_) => return Err(DkimSignatureError::ParseValueUnmatch),
289                                }
290                            }
291                            tag_lexer = value_lexer.morph();
292                            Stage::WantTag
293                        }
294                        _ => return Err(DkimSignatureError::UnexpectedEqual),
295                    };
296                }
297                Ok(maybe_tag_token) if stage == Stage::WantTag => {
298                    let current_tag = DkimTagChoice::from_token(maybe_tag_token);
299                    stage = match current_tag {
300                        None => return Err(DkimSignatureError::NoTagFound),
301                        Some(tag) => Stage::WantEq(tag),
302                    };
303                }
304                _ => {
305                    let cut_slice = &tag_lexer.source()[tag_lexer.span().start..];
306                    let cut_span =
307                        &tag_lexer.source()[tag_lexer.span().start..tag_lexer.span().end];
308
309                    let detail = crate::error::ParsingDetail {
310                        component: "parse_dkim_signature",
311                        span_start: tag_lexer.span().start,
312                        span_end: tag_lexer.span().end,
313                        source: tag_lexer.source(),
314                        clipped_span: cut_span,
315                        clipped_remaining: cut_slice,
316                    };
317
318                    return Err(DkimSignatureError::ParsingDetailed(detail));
319                }
320            }
321        }
322        res.try_into()
323    }
324}