chore: checkpoint before Python removal

This commit is contained in:
2026-03-26 22:33:59 +00:00
parent 683cec9307
commit e568ddf82a
29972 changed files with 11269302 additions and 2 deletions

270
vendor/pem-rfc7468/src/decoder.rs vendored Normal file
View File

@@ -0,0 +1,270 @@
//! Decoder for PEM encapsulated data.
//!
//! From RFC 7468 Section 2:
//!
//! > Textual encoding begins with a line comprising "-----BEGIN ", a
//! > label, and "-----", and ends with a line comprising "-----END ", a
//! > label, and "-----". Between these lines, or "encapsulation
//! > boundaries", are base64-encoded data according to Section 4 of
//! > [RFC 4648].
//!
//! [RFC 4648]: https://datatracker.ietf.org/doc/html/rfc4648
use crate::{
grammar, Base64Decoder, Error, Result, BASE64_WRAP_WIDTH, POST_ENCAPSULATION_BOUNDARY,
PRE_ENCAPSULATION_BOUNDARY,
};
use core::str;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::io;
/// Decode a PEM document according to RFC 7468's "Strict" grammar.
///
/// On success, writes the decoded document into the provided buffer, returning
/// the decoded label and the portion of the provided buffer containing the
/// decoded message.
pub fn decode<'i, 'o>(pem: &'i [u8], buf: &'o mut [u8]) -> Result<(&'i str, &'o [u8])> {
let mut decoder = Decoder::new(pem).map_err(|e| check_for_headers(pem, e))?;
let type_label = decoder.type_label();
let buf = buf
.get_mut(..decoder.remaining_len())
.ok_or(Error::Length)?;
let decoded = decoder.decode(buf).map_err(|e| check_for_headers(pem, e))?;
if decoder.base64.is_finished() {
Ok((type_label, decoded))
} else {
Err(Error::Length)
}
}
/// Decode a PEM document according to RFC 7468's "Strict" grammar, returning
/// the result as a [`Vec`] upon success.
#[cfg(feature = "alloc")]
pub fn decode_vec(pem: &[u8]) -> Result<(&str, Vec<u8>)> {
let mut decoder = Decoder::new(pem).map_err(|e| check_for_headers(pem, e))?;
let type_label = decoder.type_label();
let mut buf = Vec::new();
decoder
.decode_to_end(&mut buf)
.map_err(|e| check_for_headers(pem, e))?;
Ok((type_label, buf))
}
/// Decode the encapsulation boundaries of a PEM document according to RFC 7468's "Strict" grammar.
///
/// On success, returning the decoded label.
pub fn decode_label(pem: &[u8]) -> Result<&str> {
Ok(Encapsulation::try_from(pem)?.label())
}
/// Buffered PEM decoder.
///
/// Stateful buffered decoder type which decodes an input PEM document according
/// to RFC 7468's "Strict" grammar.
#[derive(Clone)]
pub struct Decoder<'i> {
/// PEM type label.
type_label: &'i str,
/// Buffered Base64 decoder.
base64: Base64Decoder<'i>,
}
impl<'i> Decoder<'i> {
/// Create a new PEM [`Decoder`] with the default options.
///
/// Uses the default 64-character line wrapping.
pub fn new(pem: &'i [u8]) -> Result<Self> {
Self::new_wrapped(pem, BASE64_WRAP_WIDTH)
}
/// Create a new PEM [`Decoder`] which wraps at the given line width.
pub fn new_wrapped(pem: &'i [u8], line_width: usize) -> Result<Self> {
let encapsulation = Encapsulation::try_from(pem)?;
let type_label = encapsulation.label();
let base64 = Base64Decoder::new_wrapped(encapsulation.encapsulated_text, line_width)?;
Ok(Self { type_label, base64 })
}
/// Get the PEM type label for the input document.
pub fn type_label(&self) -> &'i str {
self.type_label
}
/// Decode data into the provided output buffer.
///
/// There must be at least as much remaining Base64 input to be decoded
/// in order to completely fill `buf`.
pub fn decode<'o>(&mut self, buf: &'o mut [u8]) -> Result<&'o [u8]> {
Ok(self.base64.decode(buf)?)
}
/// Decode all of the remaining data in the input buffer into `buf`.
#[cfg(feature = "alloc")]
pub fn decode_to_end<'o>(&mut self, buf: &'o mut Vec<u8>) -> Result<&'o [u8]> {
Ok(self.base64.decode_to_end(buf)?)
}
/// Get the decoded length of the remaining PEM data after Base64 decoding.
pub fn remaining_len(&self) -> usize {
self.base64.remaining_len()
}
/// Are we finished decoding the PEM input?
pub fn is_finished(&self) -> bool {
self.base64.is_finished()
}
}
impl<'i> From<Decoder<'i>> for Base64Decoder<'i> {
fn from(decoder: Decoder<'i>) -> Base64Decoder<'i> {
decoder.base64
}
}
#[cfg(feature = "std")]
impl<'i> io::Read for Decoder<'i> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.base64.read(buf)
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
self.base64.read_to_end(buf)
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
self.base64.read_exact(buf)
}
}
/// PEM encapsulation parser.
///
/// This parser performs an initial pass over the data, locating the
/// pre-encapsulation (`---BEGIN [...]---`) and post-encapsulation
/// (`---END [...]`) boundaries while attempting to avoid branching
/// on the potentially secret Base64-encoded data encapsulated between
/// the two boundaries.
///
/// It only supports a single encapsulated message at present. Future work
/// could potentially include extending it provide an iterator over a series
/// of encapsulated messages.
#[derive(Copy, Clone, Debug)]
struct Encapsulation<'a> {
/// Type label extracted from the pre/post-encapsulation boundaries.
///
/// From RFC 7468 Section 2:
///
/// > The type of data encoded is labeled depending on the type label in
/// > the "-----BEGIN " line (pre-encapsulation boundary). For example,
/// > the line may be "-----BEGIN CERTIFICATE-----" to indicate that the
/// > content is a PKIX certificate (see further below). Generators MUST
/// > put the same label on the "-----END " line (post-encapsulation
/// > boundary) as the corresponding "-----BEGIN " line. 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. Parsers
/// > MAY disregard the label in the post-encapsulation boundary instead of
/// > signaling an error if there is a label mismatch: some extant
/// > implementations require the labels to match; others do not.
label: &'a str,
/// Encapsulated text portion contained between the boundaries.
///
/// This data should be encoded as Base64, however this type performs no
/// validation of it so it can be handled in constant-time.
encapsulated_text: &'a [u8],
}
impl<'a> Encapsulation<'a> {
/// Parse the type label and encapsulated text from between the
/// pre/post-encapsulation boundaries.
pub fn parse(data: &'a [u8]) -> Result<Self> {
// Strip the "preamble": optional text occurring before the pre-encapsulation boundary
let data = grammar::strip_preamble(data)?;
// Parse pre-encapsulation boundary (including label)
let data = data
.strip_prefix(PRE_ENCAPSULATION_BOUNDARY)
.ok_or(Error::PreEncapsulationBoundary)?;
let (label, body) = grammar::split_label(data).ok_or(Error::Label)?;
let mut body = match grammar::strip_trailing_eol(body).unwrap_or(body) {
[head @ .., b'-', b'-', b'-', b'-', b'-'] => head,
_ => return Err(Error::PreEncapsulationBoundary),
};
// Ensure body ends with a properly labeled post-encapsulation boundary
for &slice in [POST_ENCAPSULATION_BOUNDARY, label.as_bytes()].iter().rev() {
// Ensure the input ends with the post encapsulation boundary as
// well as a matching label
if !body.ends_with(slice) {
return Err(Error::PostEncapsulationBoundary);
}
let len = body.len().checked_sub(slice.len()).ok_or(Error::Length)?;
body = body.get(..len).ok_or(Error::PostEncapsulationBoundary)?;
}
let encapsulated_text =
grammar::strip_trailing_eol(body).ok_or(Error::PostEncapsulationBoundary)?;
Ok(Self {
label,
encapsulated_text,
})
}
/// Get the label parsed from the encapsulation boundaries.
pub fn label(self) -> &'a str {
self.label
}
}
impl<'a> TryFrom<&'a [u8]> for Encapsulation<'a> {
type Error = Error;
fn try_from(bytes: &'a [u8]) -> Result<Self> {
Self::parse(bytes)
}
}
/// Check for PEM headers in the input, as they are disallowed by RFC7468.
///
/// Returns `Error::HeaderDisallowed` if headers are encountered.
fn check_for_headers(pem: &[u8], err: Error) -> Error {
if err == Error::Base64(base64ct::Error::InvalidEncoding)
&& pem.iter().any(|&b| b == grammar::CHAR_COLON)
{
Error::HeaderDisallowed
} else {
err
}
}
#[cfg(test)]
mod tests {
use super::Encapsulation;
#[test]
fn pkcs8_example() {
let pem = include_bytes!("../tests/examples/pkcs8.pem");
let encapsulation = Encapsulation::parse(pem).unwrap();
assert_eq!(encapsulation.label, "PRIVATE KEY");
assert_eq!(
encapsulation.encapsulated_text,
&[
77, 67, 52, 67, 65, 81, 65, 119, 66, 81, 89, 68, 75, 50, 86, 119, 66, 67, 73, 69,
73, 66, 102, 116, 110, 72, 80, 112, 50, 50, 83, 101, 119, 89, 109, 109, 69, 111,
77, 99, 88, 56, 86, 119, 73, 52, 73, 72, 119, 97, 113, 100, 43, 57, 76, 70, 80,
106, 47, 49, 53, 101, 113, 70
]
);
}
}

299
vendor/pem-rfc7468/src/encoder.rs vendored Normal file
View File

@@ -0,0 +1,299 @@
//! PEM encoder.
use crate::{
grammar, Base64Encoder, Error, LineEnding, Result, BASE64_WRAP_WIDTH,
ENCAPSULATION_BOUNDARY_DELIMITER, POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY,
};
use base64ct::{Base64, Encoding};
use core::str;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "std")]
use std::io;
/// Compute the length of a PEM encoded document which encapsulates a
/// Base64-encoded body including line endings every 64 characters.
///
/// The `input_len` parameter specifies the length of the raw input
/// bytes prior to Base64 encoding.
///
/// Note that the current implementation of this function computes an upper
/// bound of the length and the actual encoded document may be slightly shorter
/// (typically 1-byte). Downstream consumers of this function should check the
/// actual encoded length and potentially truncate buffers allocated using this
/// function to estimate the encapsulated size.
///
/// Use [`encoded_len`] (when possible) to obtain a precise length.
///
/// ## Returns
/// - `Ok(len)` on success
/// - `Err(Error::Length)` on length overflow
pub fn encapsulated_len(label: &str, line_ending: LineEnding, input_len: usize) -> Result<usize> {
encapsulated_len_wrapped(label, BASE64_WRAP_WIDTH, line_ending, input_len)
}
/// Compute the length of a PEM encoded document with the Base64 body
/// line wrapped at the specified `width`.
///
/// This is the same as [`encapsulated_len`], which defaults to a width of 64.
///
/// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides
/// 64 is technically non-compliant:
///
/// > Generators MUST wrap the base64-encoded lines so that each line
/// > consists of exactly 64 characters except for the final line, which
/// > will encode the remainder of the data (within the 64-character line
/// > boundary)
///
/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2
pub fn encapsulated_len_wrapped(
label: &str,
line_width: usize,
line_ending: LineEnding,
input_len: usize,
) -> Result<usize> {
if line_width < 4 {
return Err(Error::Length);
}
let base64_len = input_len
.checked_mul(4)
.and_then(|n| n.checked_div(3))
.and_then(|n| n.checked_add(3))
.ok_or(Error::Length)?
& !3;
let base64_len_wrapped = base64_len_wrapped(base64_len, line_width, line_ending)?;
encapsulated_len_inner(label, line_ending, base64_len_wrapped)
}
/// Get the length of a PEM encoded document with the given bytes and label.
///
/// This function computes a precise length of the PEM encoding of the given
/// `input` data.
///
/// ## Returns
/// - `Ok(len)` on success
/// - `Err(Error::Length)` on length overflow
pub fn encoded_len(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<usize> {
let base64_len = Base64::encoded_len(input);
let base64_len_wrapped = base64_len_wrapped(base64_len, BASE64_WRAP_WIDTH, line_ending)?;
encapsulated_len_inner(label, line_ending, base64_len_wrapped)
}
/// Encode a PEM document according to RFC 7468's "Strict" grammar.
pub fn encode<'o>(
type_label: &str,
line_ending: LineEnding,
input: &[u8],
buf: &'o mut [u8],
) -> Result<&'o str> {
let mut encoder = Encoder::new(type_label, line_ending, buf)?;
encoder.encode(input)?;
let encoded_len = encoder.finish()?;
let output = &buf[..encoded_len];
// Sanity check
debug_assert!(str::from_utf8(output).is_ok());
// Ensure `output` contains characters from the lower 7-bit ASCII set
if output.iter().fold(0u8, |acc, &byte| acc | (byte & 0x80)) == 0 {
// Use unchecked conversion to avoid applying UTF-8 checks to potentially
// secret PEM documents (and therefore introducing a potential timing
// sidechannel)
//
// SAFETY: contents of this buffer are controlled entirely by the encoder,
// which ensures the contents are always a valid (ASCII) subset of UTF-8.
// It's also additionally sanity checked by two assertions above to ensure
// the validity (with the always-on runtime check implemented in a
// constant time-ish manner.
#[allow(unsafe_code)]
Ok(unsafe { str::from_utf8_unchecked(output) })
} else {
Err(Error::CharacterEncoding)
}
}
/// Encode a PEM document according to RFC 7468's "Strict" grammar, returning
/// the result as a [`String`].
#[cfg(feature = "alloc")]
pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<String> {
let expected_len = encoded_len(label, line_ending, input)?;
let mut buf = vec![0u8; expected_len];
let actual_len = encode(label, line_ending, input, &mut buf)?.len();
debug_assert_eq!(expected_len, actual_len);
String::from_utf8(buf).map_err(|_| Error::CharacterEncoding)
}
/// Compute the encapsulated length of Base64 data of the given length.
fn encapsulated_len_inner(
label: &str,
line_ending: LineEnding,
base64_len: usize,
) -> Result<usize> {
[
PRE_ENCAPSULATION_BOUNDARY.len(),
label.as_bytes().len(),
ENCAPSULATION_BOUNDARY_DELIMITER.len(),
line_ending.len(),
base64_len,
line_ending.len(),
POST_ENCAPSULATION_BOUNDARY.len(),
label.as_bytes().len(),
ENCAPSULATION_BOUNDARY_DELIMITER.len(),
line_ending.len(),
]
.into_iter()
.try_fold(0usize, |acc, len| acc.checked_add(len))
.ok_or(Error::Length)
}
/// Compute Base64 length line-wrapped at the specified width with the given
/// line ending.
fn base64_len_wrapped(
base64_len: usize,
line_width: usize,
line_ending: LineEnding,
) -> Result<usize> {
base64_len
.saturating_sub(1)
.checked_div(line_width)
.and_then(|lines| lines.checked_mul(line_ending.len()))
.and_then(|len| len.checked_add(base64_len))
.ok_or(Error::Length)
}
/// Buffered PEM encoder.
///
/// Stateful buffered encoder type which encodes an input PEM document according
/// to RFC 7468's "Strict" grammar.
pub struct Encoder<'l, 'o> {
/// PEM type label.
type_label: &'l str,
/// Line ending used to wrap Base64.
line_ending: LineEnding,
/// Buffered Base64 encoder.
base64: Base64Encoder<'o>,
}
impl<'l, 'o> Encoder<'l, 'o> {
/// Create a new PEM [`Encoder`] with the default options which
/// writes output into the provided buffer.
///
/// Uses the default 64-character line wrapping.
pub fn new(type_label: &'l str, line_ending: LineEnding, out: &'o mut [u8]) -> Result<Self> {
Self::new_wrapped(type_label, BASE64_WRAP_WIDTH, line_ending, out)
}
/// Create a new PEM [`Encoder`] which wraps at the given line width.
///
/// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides
/// 64 is technically non-compliant:
///
/// > Generators MUST wrap the base64-encoded lines so that each line
/// > consists of exactly 64 characters except for the final line, which
/// > will encode the remainder of the data (within the 64-character line
/// > boundary)
///
/// This method is provided with the intended purpose of implementing the
/// OpenSSH private key format, which uses a non-standard wrap width of 70.
///
/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2
pub fn new_wrapped(
type_label: &'l str,
line_width: usize,
line_ending: LineEnding,
mut out: &'o mut [u8],
) -> Result<Self> {
grammar::validate_label(type_label.as_bytes())?;
for boundary_part in [
PRE_ENCAPSULATION_BOUNDARY,
type_label.as_bytes(),
ENCAPSULATION_BOUNDARY_DELIMITER,
line_ending.as_bytes(),
] {
if out.len() < boundary_part.len() {
return Err(Error::Length);
}
let (part, rest) = out.split_at_mut(boundary_part.len());
out = rest;
part.copy_from_slice(boundary_part);
}
let base64 = Base64Encoder::new_wrapped(out, line_width, line_ending)?;
Ok(Self {
type_label,
line_ending,
base64,
})
}
/// Get the PEM type label used for this document.
pub fn type_label(&self) -> &'l str {
self.type_label
}
/// Encode the provided input data.
///
/// This method can be called as many times as needed with any sized input
/// to write data encoded data into the output buffer, so long as there is
/// sufficient space in the buffer to handle the resulting Base64 encoded
/// data.
pub fn encode(&mut self, input: &[u8]) -> Result<()> {
self.base64.encode(input)?;
Ok(())
}
/// Borrow the inner [`Base64Encoder`].
pub fn base64_encoder(&mut self) -> &mut Base64Encoder<'o> {
&mut self.base64
}
/// Finish encoding PEM, writing the post-encapsulation boundary.
///
/// On success, returns the total number of bytes written to the output
/// buffer.
pub fn finish(self) -> Result<usize> {
let (base64, mut out) = self.base64.finish_with_remaining()?;
for boundary_part in [
self.line_ending.as_bytes(),
POST_ENCAPSULATION_BOUNDARY,
self.type_label.as_bytes(),
ENCAPSULATION_BOUNDARY_DELIMITER,
self.line_ending.as_bytes(),
] {
if out.len() < boundary_part.len() {
return Err(Error::Length);
}
let (part, rest) = out.split_at_mut(boundary_part.len());
out = rest;
part.copy_from_slice(boundary_part);
}
encapsulated_len_inner(self.type_label, self.line_ending, base64.len())
}
}
#[cfg(feature = "std")]
impl<'l, 'o> io::Write for Encoder<'l, 'o> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.encode(buf)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
// TODO(tarcieri): return an error if there's still data remaining in the buffer?
Ok(())
}
}

107
vendor/pem-rfc7468/src/error.rs vendored Normal file
View File

@@ -0,0 +1,107 @@
//! Error types
use core::fmt;
/// Result type with the `pem-rfc7468` crate's [`Error`] type.
pub type Result<T> = core::result::Result<T, Error>;
/// PEM errors.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
/// Base64-related errors.
Base64(base64ct::Error),
/// Character encoding-related errors.
CharacterEncoding,
/// Errors in the encapsulated text (which aren't specifically Base64-related).
EncapsulatedText,
/// Header detected in the encapsulated text.
HeaderDisallowed,
/// Invalid label.
Label,
/// Invalid length.
Length,
/// "Preamble" (text before pre-encapsulation boundary) contains invalid data.
Preamble,
/// Errors in the pre-encapsulation boundary.
PreEncapsulationBoundary,
/// Errors in the post-encapsulation boundary.
PostEncapsulationBoundary,
/// Unexpected PEM type label.
UnexpectedTypeLabel {
/// Type label that was expected.
expected: &'static str,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Base64(err) => write!(f, "PEM Base64 error: {}", err),
Error::CharacterEncoding => f.write_str("PEM character encoding error"),
Error::EncapsulatedText => f.write_str("PEM error in encapsulated text"),
Error::HeaderDisallowed => f.write_str("PEM headers disallowed by RFC7468"),
Error::Label => f.write_str("PEM type label invalid"),
Error::Length => f.write_str("PEM length invalid"),
Error::Preamble => f.write_str("PEM preamble contains invalid data (NUL byte)"),
Error::PreEncapsulationBoundary => {
f.write_str("PEM error in pre-encapsulation boundary")
}
Error::PostEncapsulationBoundary => {
f.write_str("PEM error in post-encapsulation boundary")
}
Error::UnexpectedTypeLabel { expected } => {
write!(f, "unexpected PEM type label: expecting \"{}\"", expected)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl From<base64ct::Error> for Error {
fn from(err: base64ct::Error) -> Error {
Error::Base64(err)
}
}
impl From<base64ct::InvalidLengthError> for Error {
fn from(_: base64ct::InvalidLengthError) -> Error {
Error::Length
}
}
impl From<core::str::Utf8Error> for Error {
fn from(_: core::str::Utf8Error) -> Error {
Error::CharacterEncoding
}
}
#[cfg(feature = "std")]
impl From<Error> for std::io::Error {
fn from(err: Error) -> std::io::Error {
let kind = match err {
Error::Base64(err) => return err.into(), // Use existing conversion
Error::CharacterEncoding
| Error::EncapsulatedText
| Error::Label
| Error::Preamble
| Error::PreEncapsulationBoundary
| Error::PostEncapsulationBoundary => std::io::ErrorKind::InvalidData,
Error::Length => std::io::ErrorKind::UnexpectedEof,
_ => std::io::ErrorKind::Other,
};
std::io::Error::new(kind, err)
}
}

232
vendor/pem-rfc7468/src/grammar.rs vendored Normal file
View File

@@ -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));
}
}

122
vendor/pem-rfc7468/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,122 @@
#![no_std]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg",
html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
)]
#![deny(unsafe_code)]
#![warn(
clippy::integer_arithmetic,
clippy::mod_module_files,
clippy::panic,
clippy::panic_in_result_fn,
clippy::unwrap_used,
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
//! # Usage
//!
#![cfg_attr(feature = "std", doc = " ```")]
#![cfg_attr(not(feature = "std"), doc = " ```ignore")]
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! /// Example PEM document
//! /// NOTE: do not actually put private key literals into your source code!!!
//! let example_pem = "\
//! -----BEGIN PRIVATE KEY-----
//! MC4CAQAwBQYDK2VwBCIEIBftnHPp22SewYmmEoMcX8VwI4IHwaqd+9LFPj/15eqF
//! -----END PRIVATE KEY-----
//! ";
//!
//! // Decode PEM
//! let (type_label, data) = pem_rfc7468::decode_vec(example_pem.as_bytes())?;
//! assert_eq!(type_label, "PRIVATE KEY");
//! assert_eq!(
//! data,
//! &[
//! 48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32, 23, 237, 156, 115, 233, 219,
//! 100, 158, 193, 137, 166, 18, 131, 28, 95, 197, 112, 35, 130, 7, 193, 170, 157, 251,
//! 210, 197, 62, 63, 245, 229, 234, 133
//! ]
//! );
//!
//! // Encode PEM
//! use pem_rfc7468::LineEnding;
//! let encoded_pem = pem_rfc7468::encode_string(type_label, LineEnding::default(), &data)?;
//! assert_eq!(&encoded_pem, example_pem);
//! # Ok(())
//! # }
//! ```
#[cfg(feature = "alloc")]
#[macro_use]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
mod decoder;
mod encoder;
mod error;
mod grammar;
pub use crate::{
decoder::{decode, decode_label, Decoder},
encoder::{encapsulated_len, encapsulated_len_wrapped, encode, encoded_len, Encoder},
error::{Error, Result},
};
pub use base64ct::LineEnding;
#[cfg(feature = "alloc")]
pub use crate::{decoder::decode_vec, encoder::encode_string};
/// The pre-encapsulation boundary appears before the encapsulated text.
///
/// From RFC 7468 Section 2:
/// > There are exactly five hyphen-minus (also known as dash) characters ("-")
/// > on both ends of the encapsulation boundaries, no more, no less.
const PRE_ENCAPSULATION_BOUNDARY: &[u8] = b"-----BEGIN ";
/// The post-encapsulation boundary appears immediately after the encapsulated text.
const POST_ENCAPSULATION_BOUNDARY: &[u8] = b"-----END ";
/// Delimiter of encapsulation boundaries.
const ENCAPSULATION_BOUNDARY_DELIMITER: &[u8] = b"-----";
/// Width at which the Base64 body of RFC7468-compliant PEM is wrapped.
///
/// From [RFC7468 § 2]:
///
/// > Generators MUST wrap the base64-encoded lines so that each line
/// > consists of exactly 64 characters except for the final line, which
/// > will encode the remainder of the data (within the 64-character line
/// > boundary), and they MUST NOT emit extraneous whitespace. Parsers MAY
/// > handle other line sizes.
///
/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2
pub const BASE64_WRAP_WIDTH: usize = 64;
/// Buffered Base64 decoder type.
pub type Base64Decoder<'i> = base64ct::Decoder<'i, base64ct::Base64>;
/// Buffered Base64 encoder type.
pub type Base64Encoder<'o> = base64ct::Encoder<'o, base64ct::Base64>;
/// Marker trait for types with an associated PEM type label.
pub trait PemLabel {
/// Expected PEM type label for a given document, e.g. `"PRIVATE KEY"`
const PEM_LABEL: &'static str;
/// Validate that a given label matches the expected label.
fn validate_pem_label(actual: &str) -> Result<()> {
if Self::PEM_LABEL == actual {
Ok(())
} else {
Err(Error::UnexpectedTypeLabel {
expected: Self::PEM_LABEL,
})
}
}
}