diff options
Diffstat (limited to 'vendor/pem-rfc7468/src/grammar.rs')
-rw-r--r-- | vendor/pem-rfc7468/src/grammar.rs | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/vendor/pem-rfc7468/src/grammar.rs b/vendor/pem-rfc7468/src/grammar.rs new file mode 100644 index 0000000..91085fe --- /dev/null +++ b/vendor/pem-rfc7468/src/grammar.rs @@ -0,0 +1,232 @@ +//! Helper functions and rules for enforcing the ABNF grammar for +//! RFC 7468-flavored PEM as described in Section 3. +//! +//! The grammar described below is intended to follow the "ABNF (Strict)" +//! subset of the grammar as described in Section 3 Figure 3. + +use crate::{Error, Result, PRE_ENCAPSULATION_BOUNDARY}; +use core::str; + +/// NUL char +pub(crate) const CHAR_NUL: u8 = 0x00; + +/// Horizontal tab +pub(crate) const CHAR_HT: u8 = 0x09; + +/// Space +pub(crate) const CHAR_SP: u8 = 0x20; + +/// Carriage return +pub(crate) const CHAR_CR: u8 = 0x0d; + +/// Line feed +pub(crate) const CHAR_LF: u8 = 0x0a; + +/// Colon ':' +pub(crate) const CHAR_COLON: u8 = 0x3A; + +/// Any printable character except hyphen-minus, as defined in the +/// 'labelchar' production in the RFC 7468 ABNF grammar +pub(crate) fn is_labelchar(char: u8) -> bool { + matches!(char, 0x21..=0x2C | 0x2E..=0x7E) +} + +/// Does the provided byte match a character allowed in a label? +// TODO: allow hyphen-minus to match the 'label' production in the ABNF grammar +pub(crate) fn is_allowed_in_label(char: u8) -> bool { + is_labelchar(char) || matches!(char, CHAR_HT | CHAR_SP) +} + +/// Does the provided byte match the "WSP" ABNF production from Section 3? +/// +/// > The common ABNF production WSP is congruent with "blank"; +/// > a new production W is used for "whitespace" +pub(crate) fn is_wsp(char: u8) -> bool { + matches!(char, CHAR_HT | CHAR_SP) +} + +/// Strip the "preamble", i.e. data that appears before the PEM +/// pre-encapsulation boundary. +/// +/// Presently no attempt is made to ensure the preamble decodes successfully +/// under any particular character encoding. The only byte which is disallowed +/// is the NUL byte. This restriction does not appear in RFC7468, but rather +/// is inspired by the OpenSSL PEM decoder. +/// +/// Returns a slice which starts at the beginning of the encapsulated text. +/// +/// From RFC7468: +/// > Data before the encapsulation boundaries are permitted, and +/// > parsers MUST NOT malfunction when processing such data. +pub(crate) fn strip_preamble(mut bytes: &[u8]) -> Result<&[u8]> { + if bytes.starts_with(PRE_ENCAPSULATION_BOUNDARY) { + return Ok(bytes); + } + + while let Some((byte, remaining)) = bytes.split_first() { + match *byte { + CHAR_NUL => { + return Err(Error::Preamble); + } + CHAR_LF if remaining.starts_with(PRE_ENCAPSULATION_BOUNDARY) => { + return Ok(remaining); + } + _ => (), + } + + bytes = remaining; + } + + Err(Error::Preamble) +} + +/// Strip a newline (`eol`) from the beginning of the provided byte slice. +/// +/// The newline is considered mandatory and a decoding error will occur if it +/// is not present. +/// +/// From RFC 7468 Section 3: +/// > lines are divided with CRLF, CR, or LF. +pub(crate) fn strip_leading_eol(bytes: &[u8]) -> Option<&[u8]> { + match bytes { + [CHAR_LF, rest @ ..] => Some(rest), + [CHAR_CR, CHAR_LF, rest @ ..] => Some(rest), + [CHAR_CR, rest @ ..] => Some(rest), + _ => None, + } +} + +/// Strip a newline (`eol`) from the end of the provided byte slice. +/// +/// The newline is considered mandatory and a decoding error will occur if it +/// is not present. +/// +/// From RFC 7468 Section 3: +/// > lines are divided with CRLF, CR, or LF. +pub(crate) fn strip_trailing_eol(bytes: &[u8]) -> Option<&[u8]> { + match bytes { + [head @ .., CHAR_CR, CHAR_LF] => Some(head), + [head @ .., CHAR_LF] => Some(head), + [head @ .., CHAR_CR] => Some(head), + _ => None, + } +} + +/// Split a slice beginning with a type label as located in an encapsulation +/// boundary. Returns the label as a `&str`, and slice beginning with the +/// encapsulated text with leading `-----` and newline removed. +/// +/// This implementation follows the rules put forth in Section 2, which are +/// stricter than those found in the ABNF grammar: +/// +/// > Labels are formally case-sensitive, uppercase, and comprised of zero or more +/// > characters; they do not contain consecutive spaces or hyphen-minuses, +/// > nor do they contain spaces or hyphen-minuses at either end. +/// +/// We apply a slightly stricter interpretation: +/// - Labels MAY be empty +/// - Non-empty labels MUST start with an upper-case letter: `'A'..='Z'` +/// - The only allowable characters subsequently are `'A'..='Z'` or WSP. +/// (NOTE: this is an overly strict initial implementation and should be relaxed) +/// - Whitespace MUST NOT contain more than one consecutive WSP character +// TODO(tarcieri): evaluate whether this is too strict; support '-' +pub(crate) fn split_label(bytes: &[u8]) -> Option<(&str, &[u8])> { + let mut n = 0usize; + + // TODO(tarcieri): handle hyphens in labels as well as spaces + let mut last_was_wsp = false; + + for &char in bytes { + // Validate character + if is_labelchar(char) { + last_was_wsp = false; + } else if char == b'-' { + // Possible start of encapsulation boundary delimiter + break; + } else if n != 0 && is_wsp(char) { + // Repeated whitespace disallowed + if last_was_wsp { + return None; + } + + last_was_wsp = true; + } else { + return None; + } + + n = n.checked_add(1)?; + } + + let (raw_label, rest) = bytes.split_at(n); + let label = str::from_utf8(raw_label).ok()?; + + match rest { + [b'-', b'-', b'-', b'-', b'-', body @ ..] => Some((label, strip_leading_eol(body)?)), + _ => None, + } +} + +/// Validate that the given bytes are allowed as a PEM type label, i.e. the +/// label encoded in the `BEGIN` and `END` encapsulation boundaries. +pub(crate) fn validate_label(label: &[u8]) -> Result<()> { + // TODO(tarcieri): handle hyphens in labels as well as spaces + let mut last_was_wsp = false; + + for &char in label { + if !is_allowed_in_label(char) { + return Err(Error::Label); + } + + if is_wsp(char) { + // Double sequential whitespace characters disallowed + if last_was_wsp { + return Err(Error::Label); + } + + last_was_wsp = true; + } else { + last_was_wsp = false; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Empty label is OK. + #[test] + fn split_label_empty() { + let (label, body) = split_label(b"-----\nBODY").unwrap(); + assert_eq!(label, ""); + assert_eq!(body, b"BODY"); + } + + /// Label containing text. + #[test] + fn split_label_with_text() { + let (label, body) = split_label(b"PRIVATE KEY-----\nBODY").unwrap(); + assert_eq!(label, "PRIVATE KEY"); + assert_eq!(body, b"BODY"); + } + + /// Reject labels containing repeated spaces + #[test] + fn split_label_with_repeat_wsp_is_err() { + assert!(split_label(b"PRIVATE KEY-----\nBODY").is_none()); + } + + /// Basic validation of a label + #[test] + fn validate_private_key_label() { + assert_eq!(validate_label(b"PRIVATE KEY"), Ok(())); + } + + /// Reject labels with double spaces + #[test] + fn validate_private_key_label_reject_double_space() { + assert_eq!(validate_label(b"PRIVATE KEY"), Err(Error::Label)); + } +} |