xfcc_parser/
lib.rs

1pub mod error;
2
3use std::{
4    borrow::{Borrow, Cow},
5    str::{FromStr, Utf8Error},
6    string::FromUtf8Error,
7};
8
9pub use error::XfccError;
10use nom::{
11    branch::alt,
12    bytes::complete::{escaped_transform, is_not, tag, take, take_till},
13    character::complete::char,
14    combinator::{eof, map, map_res, value},
15    multi::{separated_list0, separated_list1},
16    sequence::{delimited, tuple},
17    AsChar, IResult,
18};
19use strum::EnumString;
20
21const DOUBLE_QUOTE: u8 = b'"';
22const SLASH: u8 = b'\\';
23const COMMA: u8 = b',';
24const SEMICOLON: u8 = b';';
25const EQUAL: u8 = b'=';
26
27/// Variants of pair keys in XFCC elements
28#[derive(Debug, PartialEq, Eq, EnumString, strum::Display)]
29pub enum PairKey {
30    /// The Subject Alternative Name (URI type) of the current proxy's certificate
31    By,
32    /// The SHA 256 digest of the current client certificate
33    Hash,
34    /// The entire client certificate in URL encoded PEM format
35    Cert,
36    /// The entire client certificate chain (including the leaf certificate) in URL encoded PEM format
37    Chain,
38    /// The Subject field of the current client certificate
39    Subject,
40    /// The URI type Subject Alternative Name field of the current client certificate
41    #[strum(serialize = "URI")]
42    Uri,
43    /// The DNS type Subject Alternative Name field of the current client certificate
44    #[strum(serialize = "DNS")]
45    Dns,
46}
47
48/// A list of key-value pairs representing a raw XFCC element
49pub type ElementRaw<'a> = Vec<(PairKey, Cow<'a, str>)>;
50
51/// An XFCC element
52#[derive(Debug, PartialEq, Eq, Default)]
53pub struct Element<'a> {
54    pub by: Vec<Cow<'a, str>>,
55    pub hash: Option<Cow<'a, str>>,
56    pub cert: Option<Cow<'a, str>>,
57    pub chain: Option<Cow<'a, str>>,
58    pub subject: Option<Cow<'a, str>>,
59    pub uri: Vec<Cow<'a, str>>,
60    pub dns: Vec<Cow<'a, str>>,
61}
62
63impl<'a> TryFrom<ElementRaw<'a>> for Element<'a> {
64    type Error = XfccError<'a>;
65
66    fn try_from(element_raw: ElementRaw<'a>) -> Result<Self, Self::Error> {
67        let mut element = Self::default();
68        for (key, value) in element_raw {
69            if value.is_empty() {
70                continue;
71            }
72            macro_rules! error_if_duplicate {
73                ($key_type:expr, $key_field:ident) => {
74                    if element.$key_field.is_some() {
75                        return Err(XfccError::DuplicatePairKey($key_type));
76                    } else {
77                        element.$key_field = Some(value);
78                    }
79                };
80            }
81            match key {
82                PairKey::By => element.by.push(value),
83                PairKey::Hash => error_if_duplicate!(PairKey::Hash, hash),
84                PairKey::Cert => error_if_duplicate!(PairKey::Cert, cert),
85                PairKey::Chain => error_if_duplicate!(PairKey::Chain, chain),
86                PairKey::Subject => error_if_duplicate!(PairKey::Subject, subject),
87                PairKey::Uri => element.uri.push(value),
88                PairKey::Dns => element.dns.push(value),
89            }
90        }
91        Ok(element)
92    }
93}
94
95fn to_cow_str(s: &[u8]) -> Result<Cow<str>, Utf8Error> {
96    std::str::from_utf8(s).map(Cow::from)
97}
98
99fn to_owned_cow_str(s: Vec<u8>) -> Result<Cow<'static, str>, FromUtf8Error> {
100    String::from_utf8(s).map(Cow::from)
101}
102
103fn empty_quoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
104    map(value("", tag([DOUBLE_QUOTE, DOUBLE_QUOTE])), Cow::from)(s)
105}
106
107fn escaped_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
108    map_res(
109        escaped_transform(
110            is_not(&[DOUBLE_QUOTE, SLASH][..]),
111            SLASH.as_char(),
112            take(1u8),
113        ),
114        to_owned_cow_str,
115    )(s)
116}
117
118fn quoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
119    alt((
120        empty_quoted_value,
121        delimited(
122            char(DOUBLE_QUOTE.as_char()),
123            escaped_value,
124            char(DOUBLE_QUOTE.as_char()),
125        ),
126    ))(s)
127}
128
129fn unquoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
130    map_res(
131        alt((
132            take_till(|c| c == COMMA || c == SEMICOLON || c == EQUAL),
133            eof,
134        )),
135        to_cow_str,
136    )(s)
137}
138
139fn pair_key(s: &[u8]) -> IResult<&[u8], PairKey> {
140    map_res(
141        alt((
142            tag("By"),
143            tag("Hash"),
144            tag("Cert"),
145            tag("Chain"),
146            tag("Subject"),
147            tag("URI"),
148            tag("DNS"),
149        )),
150        |name| {
151            to_cow_str(name).map(|name| match PairKey::from_str(name.borrow()) {
152                Ok(key) => key,
153                Err(_) => unreachable!("Failed to parse PairKey while nom succeeded"),
154            })
155        },
156    )(s)
157}
158
159fn pair(s: &[u8]) -> IResult<&[u8], (PairKey, Cow<str>)> {
160    let (s, (key, _, value)) =
161        tuple((pair_key, char('='), alt((quoted_value, unquoted_value))))(s)?;
162    Ok((s, (key, value)))
163}
164
165fn element(s: &[u8]) -> IResult<&[u8], ElementRaw> {
166    separated_list1(char(';'), pair)(s)
167}
168
169/// Parses an XFCC header to a list of raw XFCC elements, each consists of a list of key-value pairs
170///
171/// # Arguments
172///
173/// * `s` - An XFCC header
174///
175/// # Examples
176///
177/// ```
178/// use std::borrow::Cow;
179/// use xfcc_parser::PairKey;
180///
181/// let input = br#"By=http://frontend.lyft.com;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
182/// let (trailing, elements) = xfcc_parser::element_raw_list(input).unwrap();
183///
184/// assert!(trailing.is_empty());
185/// assert_eq!(elements[0], vec![
186///     (PairKey::By, Cow::from("http://frontend.lyft.com")),
187///     (PairKey::Subject, Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")),
188///     (PairKey::Uri, Cow::from("http://testclient.lyft.com")),
189/// ]);
190/// ```
191pub fn element_raw_list(s: &[u8]) -> IResult<&[u8], Vec<ElementRaw>> {
192    separated_list0(char(','), element)(s)
193}
194
195/// Parses an XFCC header to a list of XFCC elements
196///
197/// # Arguments
198///
199/// * `s` - An XFCC header
200///
201/// # Examples
202///
203/// ```
204/// use std::borrow::Cow;
205/// use xfcc_parser::Element;
206///
207/// let input = br#"By=http://frontend.lyft.com;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
208/// let elements = xfcc_parser::element_list(input).unwrap();
209///
210/// assert_eq!(
211///     elements[0],
212///     Element {
213///         by: vec![Cow::from("http://frontend.lyft.com")],
214///         hash: None,
215///         cert: None,
216///         chain: None,
217///         subject: Some(Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")),
218///         uri: vec![Cow::from("http://testclient.lyft.com")],
219///         dns: vec![],
220///     }
221/// );
222/// ```
223pub fn element_list(s: &[u8]) -> Result<Vec<Element>, XfccError> {
224    let (trailing, raw_list) = element_raw_list(s)?;
225
226    if !trailing.is_empty() {
227        return Err(XfccError::TrailingSequence(trailing));
228    }
229
230    let mut elements = vec![];
231    raw_list
232        .into_iter()
233        .try_for_each(|element_raw| -> Result<(), XfccError> {
234            elements.push(Element::try_from(element_raw)?);
235            Ok(())
236        })?;
237
238    Ok(elements)
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn basic_escaped_value_test() {
247        let input = br#"hello, \"world\"!"#;
248        assert_eq!(
249            escaped_value(input),
250            Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
251        );
252    }
253
254    #[test]
255    fn unnecessarily_escaped_value_test() {
256        let input = br#"\h\e\l\l\o, \"world\"!"#;
257        assert_eq!(
258            escaped_value(input),
259            Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
260        );
261    }
262
263    #[test]
264    fn utf8_escaped_value_test() {
265        let input: Vec<u8> = "こんにちは"
266            .bytes()
267            .flat_map(|b| [b'\\', b].into_iter())
268            .collect();
269        assert_eq!(
270            escaped_value(&input),
271            Ok((&[][..], Cow::from("こんにちは")))
272        );
273    }
274
275    #[test]
276    fn invalid_utf8_escaped_value_test() {
277        let mut input: Vec<u8> = "こんにちは".bytes().collect();
278        input.pop().unwrap();
279        assert_eq!(
280            escaped_value(&input),
281            Err(nom::Err::Error(nom::error::Error {
282                input: &input[..],
283                code: nom::error::ErrorKind::MapRes
284            }))
285        );
286    }
287
288    #[test]
289    fn basic_quoted_value_test() {
290        let input = br#""hello, \"world\"!""#;
291        assert_eq!(
292            quoted_value(input),
293            Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
294        );
295    }
296
297    #[test]
298    fn empty_quoted_value_test() {
299        let input = br#""""#;
300        assert_eq!(empty_quoted_value(input), Ok((&[][..], Cow::from(""))));
301        assert_eq!(quoted_value(input), Ok((&[][..], Cow::from(""))));
302    }
303
304    #[test]
305    fn basic_unquoted_value_test() {
306        let input = b"hello! world!;";
307        let parsed = unquoted_value(input).unwrap();
308        assert_eq!(parsed, (&[SEMICOLON][..], Cow::from("hello! world!")));
309        assert!(matches!(parsed.1, Cow::Borrowed(_)));
310
311        let input = b"hello! world!";
312        let parsed = unquoted_value(input).unwrap();
313        assert_eq!(parsed, (&[][..], Cow::from("hello! world!")));
314        assert!(matches!(parsed.1, Cow::Borrowed(_)));
315    }
316
317    #[test]
318    fn must_be_quoted_in_unquoted_value_test() {
319        let input = b"hello, world!;";
320        assert_eq!(
321            unquoted_value(input),
322            Ok((&b", world!;"[..], Cow::from("hello")))
323        );
324    }
325
326    #[test]
327    fn basic_pair_key_test() {
328        let input = b"Chain";
329        assert_eq!(pair_key(input), Ok((&[][..], PairKey::Chain)));
330    }
331
332    #[test]
333    fn invalid_pair_key_test() {
334        let input = b"Example";
335        assert_eq!(
336            pair_key(input),
337            Err(nom::Err::Error(nom::error::Error {
338                input: &input[..],
339                code: nom::error::ErrorKind::Tag
340            }))
341        );
342    }
343
344    #[test]
345    fn basic_pair_test() {
346        let input = br#"Chain=hello! world!;"#;
347        let parsed = pair(input).unwrap();
348        assert_eq!(
349            parsed,
350            (&b";"[..], (PairKey::Chain, Cow::from("hello! world!")))
351        );
352        assert!(matches!(parsed.1 .1, Cow::Borrowed(_)));
353
354        let input = br#"Chain=hello! world!"#;
355        let parsed = pair(input).unwrap();
356        assert_eq!(
357            parsed,
358            (&[][..], (PairKey::Chain, Cow::from("hello! world!")))
359        );
360        assert!(matches!(parsed.1 .1, Cow::Borrowed(_)));
361    }
362
363    #[test]
364    fn quoted_value_pair_test() {
365        let input = br#"Chain="hello! world!";"#;
366        let parsed = pair(input).unwrap();
367        assert_eq!(
368            parsed,
369            (&b";"[..], (PairKey::Chain, Cow::from("hello! world!")))
370        );
371        assert!(matches!(parsed.1 .1, Cow::Owned(_)));
372
373        let input = br#"Chain="hello! world!""#;
374        let parsed = pair(input).unwrap();
375        assert_eq!(
376            parsed,
377            (&[][..], (PairKey::Chain, Cow::from("hello! world!")))
378        );
379        assert!(matches!(parsed.1 .1, Cow::Owned(_)));
380    }
381
382    #[test]
383    fn basic_element_test() {
384        let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
385        assert_eq!(
386            element(input),
387            Ok((
388                &[][..],
389                (vec![
390                    (PairKey::By, Cow::from("http://frontend.lyft.com")),
391                    (
392                        PairKey::Hash,
393                        Cow::from(
394                            "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
395                        )
396                    ),
397                    (
398                        PairKey::Subject,
399                        Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
400                    ),
401                    (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
402                ])
403            ))
404        );
405    }
406
407    #[test]
408    fn empty_value_in_element_test() {
409        let input = br#"By=;By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
410        assert_eq!(
411            element(input),
412            Ok((
413                &[][..],
414                (vec![
415                    (PairKey::By, Cow::from("")),
416                    (PairKey::By, Cow::from("http://frontend.lyft.com")),
417                    (
418                        PairKey::Hash,
419                        Cow::from(
420                            "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
421                        )
422                    ),
423                    (
424                        PairKey::Subject,
425                        Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
426                    ),
427                    (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
428                ])
429            ))
430        );
431    }
432
433    #[test]
434    fn basic_element_raw_list_test() {
435        let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com,By=http://example.com;By=http://instance.com"#;
436        assert_eq!(
437            element_raw_list(input),
438            Ok((
439                &[][..],
440                vec![
441                    vec![
442                        (PairKey::By, Cow::from("http://frontend.lyft.com")),
443                        (
444                            PairKey::Hash,
445                            Cow::from(
446                                "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
447                            )
448                        ),
449                        (
450                            PairKey::Subject,
451                            Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
452                        ),
453                        (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
454                    ],
455                    vec![
456                        (PairKey::By, Cow::from("http://example.com")),
457                        (PairKey::By, Cow::from("http://instance.com"))
458                    ]
459                ]
460            ))
461        );
462
463        // https://github.com/alecholmes/xfccparser/blob/master/parser_test.go
464        let input = br#"Hash=hash;Cert="-----BEGIN%20CERTIFICATE-----%0cert%0A-----END%20CERTIFICATE-----%0A";Subject="CN=hello,OU=hello,O=Acme\, Inc.";URI=;DNS=hello.west.example.com;DNS=hello.east.example.com,By=spiffe://mesh.example.com/ns/hellons/sa/hellosa;Hash=again;Subject="";URI=spiffe://mesh.example.com/ns/otherns/sa/othersa"#;
465        assert_eq!(
466            element_raw_list(input),
467            Ok((
468                &[][..],
469                vec![
470                    vec![
471                        (PairKey::Hash, Cow::from("hash")),
472                        (
473                            PairKey::Cert,
474                            Cow::from("-----BEGIN%20CERTIFICATE-----%0cert%0A-----END%20CERTIFICATE-----%0A")
475                        ),
476                        (PairKey::Subject, Cow::from("CN=hello,OU=hello,O=Acme, Inc.")),
477                        (PairKey::Uri, Cow::from("")),
478                        (PairKey::Dns, Cow::from("hello.west.example.com")),
479                        (PairKey::Dns, Cow::from("hello.east.example.com"))
480                    ],
481                    vec![
482                        (
483                            PairKey::By,
484                            Cow::from("spiffe://mesh.example.com/ns/hellons/sa/hellosa")
485                        ),
486                        (PairKey::Hash, Cow::from("again")),
487                        (PairKey::Subject, Cow::from("")),
488                        (
489                            PairKey::Uri,
490                            Cow::from("spiffe://mesh.example.com/ns/otherns/sa/othersa")
491                        )
492                    ]
493                ]
494            ))
495        );
496    }
497
498    #[test]
499    fn basic_element_list_test() {
500        let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com,By=http://example.com;By=http://instance.com"#;
501        let certificates = element_list(input).unwrap();
502        assert_eq!(certificates.len(), 2);
503        assert_eq!(
504            certificates[0],
505            Element {
506                by: vec![Cow::from("http://frontend.lyft.com")],
507                hash: Some(Cow::from(
508                    "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
509                )),
510                cert: None,
511                chain: None,
512                subject: Some(Cow::from(
513                    "/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"
514                )),
515                uri: vec![Cow::from("http://testclient.lyft.com")],
516                dns: vec![],
517            }
518        );
519        assert_eq!(
520            certificates[1],
521            Element {
522                by: vec![
523                    Cow::from("http://example.com"),
524                    Cow::from("http://instance.com")
525                ],
526                hash: None,
527                cert: None,
528                chain: None,
529                subject: None,
530                uri: vec![],
531                dns: vec![],
532            }
533        );
534    }
535
536    #[test]
537    fn empty_subject_element_list_test() {
538        let input = br#"By=http://example.com;Subject="""#;
539        let certificates = element_list(input).unwrap();
540        assert_eq!(certificates.len(), 1);
541        assert_eq!(
542            certificates[0],
543            Element {
544                by: vec![Cow::from("http://example.com"),],
545                hash: None,
546                cert: None,
547                chain: None,
548                subject: None,
549                uri: vec![],
550                dns: vec![],
551            }
552        );
553    }
554
555    #[test]
556    fn duplicate_pair_key_test() {
557        let input = br#"By=http://example.com;Hash=hash1;Hash=hash2"#;
558        assert_eq!(
559            element_raw_list(input),
560            Ok((
561                &[][..],
562                vec![vec![
563                    (PairKey::By, Cow::from("http://example.com")),
564                    (PairKey::Hash, Cow::from("hash1")),
565                    (PairKey::Hash, Cow::from("hash2")),
566                ]]
567            ))
568        );
569        assert_eq!(
570            element_list(input),
571            Err(XfccError::DuplicatePairKey(PairKey::Hash))
572        );
573    }
574
575    #[test]
576    fn trailing_characters_test() {
577        let input = br#"By=http://example.com;Hash=hash,URI"#;
578        assert_eq!(
579            element_raw_list(input),
580            Ok((
581                &b",URI"[..],
582                vec![vec![
583                    (PairKey::By, Cow::from("http://example.com")),
584                    (PairKey::Hash, Cow::from("hash")),
585                ]]
586            ))
587        );
588        assert_eq!(
589            element_list(input),
590            Err(XfccError::TrailingSequence(&b",URI"[..]))
591        );
592    }
593}