sectxtlib/
pgpcleartextmessage.rs

1use crate::SecurityTxtOptions;
2
3use super::parse_error::ParseError;
4
5use nom::{
6    branch::alt,
7    bytes::complete::{tag, take_while, take_while1},
8    character::complete::{line_ending, none_of, one_of},
9    combinator::{all_consuming, opt, peek, recognize},
10    multi::{many0, many1, many1_count, separated_list1},
11    sequence::{delimited, preceded, separated_pair, terminated, tuple},
12    IResult,
13};
14
15#[derive(Debug, PartialEq)]
16pub(crate) struct PGPSignature<'a> {
17    pub signature: &'a str,
18    pub keys: Vec<(&'a str, &'a str)>,
19}
20
21#[derive(Debug, PartialEq)]
22pub(crate) struct PGPCleartextMessage<'a> {
23    pub hash_armor_headers: Vec<Vec<&'a str>>,
24    pub cleartext: String,
25    pub signature: PGPSignature<'a>,
26}
27
28impl PGPCleartextMessage<'_> {}
29
30pub(crate) struct PGPCleartextMessageParser {
31    options: SecurityTxtOptions,
32}
33
34impl PGPCleartextMessageParser {
35    pub fn new(options: &SecurityTxtOptions) -> Self {
36        Self {
37            options: options.clone(),
38        }
39    }
40
41    pub fn parse<'a>(&'a self, text: &'a str) -> Result<PGPCleartextMessage<'a>, ParseError> {
42        let (_, msg) = self.signed_parser(text)?;
43        Ok(msg)
44    }
45
46    fn lf_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
47        match self.options.strict {
48            true => line_ending(i),
49            false => tag("\n")(i),
50        }
51    }
52
53    // signed           =  cleartext-header
54    //                     1*(hash-header)
55    //                     CRLF
56    //                     cleartext
57    //                     signature
58    fn signed_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, PGPCleartextMessage<'a>> {
59        let (_, (_, hash_armor_headers, _, cleartext, signature)) = all_consuming(tuple((
60            |x| self.cleartext_header_parser(x),
61            many1(|x| self.hash_header_parser(x)),
62            |x| self.lf_parser(x),
63            |x| self.cleartext_parser(x),
64            |x| self.signature_parser(x),
65        )))(i)?;
66
67        Ok((
68            i,
69            PGPCleartextMessage {
70                hash_armor_headers,
71                cleartext,
72                signature,
73            },
74        ))
75    }
76
77    // cleartext-header =  %s"-----BEGIN PGP SIGNED MESSAGE-----" CRLF
78    fn cleartext_header_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
79        terminated(tag("-----BEGIN PGP SIGNED MESSAGE-----"), |x| self.lf_parser(x))(i)
80    }
81
82    // hash-header      =  %s"Hash: " hash-alg *("," hash-alg) CRLF
83    fn hash_header_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, Vec<&'a str>> {
84        delimited(
85            tag("Hash: "),
86            separated_list1(tag(","), |x| self.hash_alg_parser(x)),
87            |x| self.lf_parser(x),
88        )(i)
89    }
90
91    // hash-alg         =  token
92    //                       ; imported from RFC 2045; see RFC 4880 Section
93    //                       ; 10.3.3 for a pointer to the registry of
94    //                       ; valid values
95    fn hash_alg_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
96        self.token_parser(i)
97    }
98
99    // < Section 5.1 of [RFC2045] >
100    // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
101    //             or tspecials>
102    fn token_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
103        take_while1(is_token_char)(i)
104    }
105
106    // cleartext        =  *((line-dash / line-from / line-nodash) [CR] LF)
107    // EOL is handled in branches.
108    fn cleartext_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, String> {
109        let (i, lines) = many0(alt((|x| self.line_dash_parser(x), |x| self.line_nodash_parser(x))))(i)?;
110        Ok((i, lines.join("")))
111    }
112
113    // line-dash        =  ("- ") "-" *UTF8-char-not-cr
114    //                        ; MUST include initial "- "
115    fn line_dash_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
116        preceded(
117            tag("- "),
118            recognize(tuple((
119                one_of("-"),
120                take_while(|x| x != '\r' && x != '\n'),
121                line_ending,
122            ))),
123        )(i)
124    }
125
126    // line-nodash      =  ["- "] *UTF8-char-not-cr
127    //                       ; MAY include initial "- "
128    fn line_nodash_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
129        preceded(
130            opt(tag("- ")),
131            recognize(tuple((
132                peek(none_of("-")),
133                take_while(|x| x != '\r' && x != '\n'),
134                line_ending,
135            ))),
136        )(i)
137    }
138
139    // signature        =  armor-header
140    //                     armor-keys
141    //                     CRLF
142    //                     signature-data
143    //                     armor-tail
144    fn signature_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, PGPSignature<'a>> {
145        let (i, (_, keys, _, signature, _)) = tuple((
146            |x| self.armor_header_parser(x),
147            |x| self.armor_keys_parser(x),
148            |x| self.lf_parser(x),
149            |x| self.signature_data_parser(x),
150            |x| self.armor_tail_parser(x),
151        ))(i)?;
152
153        Ok((i, PGPSignature { signature, keys }))
154    }
155
156    // armor-header     =  %s"-----BEGIN PGP SIGNATURE-----" CRLF
157    fn armor_header_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
158        terminated(tag("-----BEGIN PGP SIGNATURE-----"), |x| self.lf_parser(x))(i)
159    }
160
161    // armor-keys       =  *(token ": " *( VCHAR / WSP ) CRLF)
162    //                       ; Armor Header Keys from RFC 4880
163    fn armor_keys_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, Vec<(&'a str, &'a str)>> {
164        many0(terminated(
165            separated_pair(
166                |x| self.token_parser(x),
167                tag(": "),
168                take_while(|x| is_vchar(x) || is_wsp(x)),
169            ),
170            |x| self.lf_parser(x),
171        ))(i)
172    }
173
174    // armor-tail       =  %s"-----END PGP SIGNATURE-----" CRLF
175    fn armor_tail_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
176        terminated(tag("-----END PGP SIGNATURE-----"), |x| self.lf_parser(x))(i)
177    }
178
179    // signature-data   =  1*(1*(ALPHA / DIGIT / "=" / "+" / "/") CRLF)
180    //                       ; base64; see RFC 4648
181    //                       ; includes RFC 4880 checksum
182    fn signature_data_parser<'a>(&'a self, i: &'a str) -> IResult<&'a str, &'a str> {
183        recognize(many1_count(terminated(take_while1(is_signature_data_char), |x| {
184            self.lf_parser(x)
185        })))(i)
186    }
187}
188
189// < Section 5.1 of [RFC2045] >
190// tspecials :=  "(" / ")" / "<" / ">" / "@" /
191//               "," / ";" / ":" / "\" / <">
192//               "/" / "[" / "]" / "?" / "="
193//               ; Must be in quoted-string,
194//               ; to use within parameter values
195fn is_token_char(i: char) -> bool {
196    let tspecials = "()<>@,;:\\\"/[]?=";
197    i != ' ' && !i.is_ascii_control() && tspecials.find(i).is_none()
198}
199
200fn is_signature_data_char(i: char) -> bool {
201    matches!(i, 'a'..='z' | 'A'..='Z' | '0'..='9' | '=' | '+' | '/')
202}
203
204// VCHAR            =  %x21-7E
205//                       ; visible (printing) characters
206fn is_vchar(i: char) -> bool {
207    matches!(i, '\x21'..='\x7E')
208}
209
210// WSP              =  SP / HTAB
211//                       ; white space
212fn is_wsp(i: char) -> bool {
213    i == ' ' || i == '\t'
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    const SIGNATURE_DATA: &str = "iHUEARYKAB0WIQSsP2kEdoKDVFpSg6u3rK+YCkjapwUCY9qRaQAKCRC3rK+YCkja\r
221pwALAP9LEHSYMDW4h8QRHg4MwCzUdnbjBLIvpq4QTo3dIqCUPwEA31MsEf95OKCh\r
222MTHYHajOzjwpwlQVrjkK419igx4imgk=\r
223=KONn\r
224";
225
226    #[test]
227    fn test_parse() {
228        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
229        let txt = format!(
230            "-----BEGIN PGP SIGNED MESSAGE-----\r
231Hash: SHA512\r
232\r
233Test\r
234- Test\r
235-----BEGIN PGP SIGNATURE-----\r
236\r
237{SIGNATURE_DATA}-----END PGP SIGNATURE-----\r
238"
239        );
240        let msg = PGPCleartextMessage {
241            hash_armor_headers: vec![vec!["SHA512"]],
242            cleartext: "Test\r\nTest\r\n".into(),
243            signature: PGPSignature {
244                signature: SIGNATURE_DATA,
245                keys: vec![],
246            },
247        };
248        assert_eq!(signed_parser.parse(&txt), Ok(msg));
249    }
250
251    #[test]
252    fn test_hash_header_parser() {
253        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
254        let test_vector = vec![
255            ("Hash: SHA512\r\n", vec!["SHA512"]),
256            ("Hash: SHA256,SHA512\r\n", vec!["SHA256", "SHA512"]),
257        ];
258
259        for (input, result) in test_vector {
260            assert_eq!(signed_parser.hash_header_parser(input), Ok(("", result)));
261        }
262    }
263
264    #[test]
265    fn test_token_parser() {
266        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
267        let test_vector = vec![("SHA512\r\n", "SHA512", "\r\n")];
268
269        for (input, result, leftover) in test_vector {
270            assert_eq!(signed_parser.token_parser(input), Ok((leftover, result)));
271        }
272    }
273
274    #[test]
275    fn test_line_dash_parser() {
276        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
277        let test_vector = vec![("- -test\r\n", "-test\r\n")];
278
279        for (input, result) in test_vector {
280            assert_eq!(signed_parser.line_dash_parser(input), Ok(("", result)));
281        }
282    }
283
284    #[test]
285    fn test_line_nodash_parser() {
286        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
287        let test_vector = vec![("test\r\n", "test\r\n")];
288
289        for (input, result) in test_vector {
290            assert_eq!(signed_parser.line_nodash_parser(input), Ok(("", result)));
291        }
292    }
293
294    #[test]
295    fn test_signature_parser() {
296        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
297        let input = format!(
298            "-----BEGIN PGP SIGNATURE-----\r
299\r
300{SIGNATURE_DATA}-----END PGP SIGNATURE-----\r
301"
302        );
303        let signature = PGPSignature {
304            signature: SIGNATURE_DATA,
305            keys: vec![],
306        };
307
308        assert_eq!(signed_parser.signature_parser(&input), Ok(("", signature)));
309    }
310
311    #[test]
312    fn test_armor_header_parser() {
313        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
314        let input = "-----BEGIN PGP SIGNATURE-----\r\n";
315        assert_eq!(
316            signed_parser.armor_header_parser(input),
317            Ok(("", "-----BEGIN PGP SIGNATURE-----"))
318        );
319    }
320
321    #[test]
322    fn test_armor_tail_parser() {
323        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
324        let input = "-----END PGP SIGNATURE-----\r\n";
325        assert_eq!(
326            signed_parser.armor_tail_parser(input),
327            Ok(("", "-----END PGP SIGNATURE-----"))
328        );
329    }
330
331    #[test]
332    fn test_armor_keys_parser() {
333        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
334        let test_vector = vec![
335            ("", vec![]),
336            ("test: \r\n", vec![("test", "")]),
337            ("test: test\r\n", vec![("test", "test")]),
338        ];
339
340        for (input, result) in test_vector {
341            assert_eq!(signed_parser.armor_keys_parser(input), Ok(("", result)));
342        }
343    }
344
345    #[test]
346    fn test_signature_data_parser() {
347        let signed_parser = PGPCleartextMessageParser::new(&Default::default());
348        let test_vector = vec![
349            "iHUEARYKAB0WIQSsP2kEdoKDVFpSg6u3rK+YCkjapwUCY9qRaQAKCRC3rK+YCkja\r\npwALAP9LEHSYMDW4h8QRHg4MwCzUdnbjBLIvpq4QTo3dIqCUPwEA31MsEf95OKCh\r\nMTHYHajOzjwpwlQVrjkK419igx4imgk\r\n=KONn\r\n",
350        ];
351
352        for input in test_vector {
353            assert_eq!(signed_parser.signature_data_parser(input), Ok(("", input)));
354        }
355    }
356}