Skip to content

Commit

Permalink
WIP: directly support PEM decoding of pki-types
Browse files Browse the repository at this point in the history
WIP: this is incomplete and untested, just two types: private
keys and (plural) certificates, which motivates the trait.
  • Loading branch information
ctz committed Sep 4, 2024
1 parent f600475 commit 5be98ca
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 14 deletions.
94 changes: 93 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@
//! base64-encoded DER, PEM objects are delimited by header and footer lines which indicate the type
//! of object contained in the PEM blob.
//!
//! The [rustls-pemfile](https://docs.rs/rustls-pemfile) crate can be used to parse PEM files.
//! Types here can be created from:
//!
//! - DER using (for example) [`PrivatePkcs8KeyDer::from`].
//! - PEM using (for example) [`PrivatePkcs8KeyDer::from_pem_slice`] via the [`DecodePem`] extension trait.
//!
//! `from_pem_slice` returns the first matching item from the given input.
//! It is usual for given PEM file to contain multiple items: if you wish
//! to examine all of these you can use the iterator-based API in the [`pem`] module.
//!
//! ## Creating new certificates and keys
//!
Expand Down Expand Up @@ -120,6 +127,25 @@ impl<'a> PrivateKeyDer<'a> {
}
}

#[cfg(feature = "alloc")]
impl DecodePem for PrivateKeyDer<'static> {
fn from_pem_items(
iter: &mut impl Iterator<Item = Result<pem::Item, pem::Error>>,
) -> Result<Self, pem::Error> {
for item in iter {
match item {
Ok(pem::Item::Pkcs1Key(pkcs1)) => return Ok(Self::Pkcs1(pkcs1)),
Ok(pem::Item::Pkcs8Key(pkcs8)) => return Ok(Self::Pkcs8(pkcs8)),
Ok(pem::Item::Sec1Key(sec1)) => return Ok(Self::Sec1(sec1)),
Ok(_) => {}
Err(err) => return Err(err),
}
}

Err(pem::Error::NoItemsFound)
}
}

impl<'a> From<PrivatePkcs1KeyDer<'a>> for PrivateKeyDer<'a> {
fn from(key: PrivatePkcs1KeyDer<'a>) -> Self {
Self::Pkcs1(key)
Expand Down Expand Up @@ -481,6 +507,30 @@ impl<'a> CertificateDer<'a> {
}
}

#[cfg(feature = "alloc")]
impl DecodePem for Vec<CertificateDer<'static>> {
/// This returns _all_ certificate items appearing in `pem_slice`.
fn from_pem_items(
iter: &mut impl Iterator<Item = Result<pem::Item, pem::Error>>,
) -> Result<Self, pem::Error> {
let mut out = Self::new();

for item in iter {
match item {
Ok(pem::Item::X509Certificate(x509)) => out.push(x509),
Ok(_) => {}
Err(err) => return Err(err),
}
}

if out.is_empty() {
Err(pem::Error::NoItemsFound)
} else {
Ok(out)
}
}
}

impl AsRef<[u8]> for CertificateDer<'_> {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
Expand Down Expand Up @@ -757,6 +807,48 @@ impl UnixTime {
}
}

/// An extension trait for types we can decode from PEM.
#[cfg(feature = "alloc")]
pub trait DecodePem: Sized {
/// Decode this type from PEM contained in a byte slice.
fn from_pem_slice(pem: &[u8]) -> Result<Self, pem::Error> {
Self::from_pem_items(&mut pem::read_all_from_slice(pem))
}

/// Decode this type from PEM contained in a `str`.
fn from_pem_str(pem: &str) -> Result<Self, pem::Error> {
Self::from_pem_slice(pem.as_bytes())
}

/// Decode this type from PEM present in a buffered reader.
#[cfg(feature = "std")]
fn from_pem_reader(rd: &mut impl std::io::Read) -> Result<Self, pem::Error> {
let mut items = Vec::new();
// iterate over items to slough off io errors
for item in pem::read_all(&mut std::io::BufReader::new(rd)) {
match item {
Ok(item) => items.push(Ok(item)),
Err(err) => return Err(err),
}
}
Self::from_pem_items(&mut items.into_iter())
}

/// Decode this type from PEM contents of the named file.
#[cfg(feature = "std")]
fn from_pem_file(file_name: impl AsRef<std::path::Path>) -> Result<Self, pem::Error> {
Self::from_pem_reader(&mut std::fs::File::open(file_name).map_err(pem::Error::Io)?)
}

/// Underlying conversion to be implemented by target types.
///
/// This is not intended for direct use, instead use [`DecodePem::from_pem_slice()`]
/// and other provided functions in this trait.
fn from_pem_items(
iter: &mut impl Iterator<Item = Result<pem::Item, pem::Error>>,
) -> Result<Self, pem::Error>;
}

/// DER-encoded data, either owned or borrowed
///
/// This wrapper type is used to represent DER-encoded data in a way that is agnostic to whether
Expand Down
48 changes: 35 additions & 13 deletions src/pem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub enum Item {
}

/// Errors that may arise when parsing the contents of a PEM file
#[derive(Debug, PartialEq)]
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// a section is missing its "END marker" line
Expand All @@ -73,19 +73,41 @@ pub enum Error {

/// base64 decode error
Base64Decode(String),
}

/// Errors that may arise from reading from a file-like stream
#[cfg(feature = "std")]
#[derive(Debug)]
pub enum IoError {
/// a `pem::Error` that occurred while parsing the stream contents
Pem(Error),
/// no items found of desired type
NoItemsFound,

/// a `std::io::Error` that occurred while reading from a `io::BufRead`
/// I/O errors, from APIs that accept `std::io` types.
#[cfg(feature = "std")]
Io(io::Error),
}

/// Iterate over all PEM sections by reading `pem_slice`
pub(crate) fn read_all_from_slice(
pem_slice: &[u8],
) -> impl Iterator<Item = Result<Item, Error>> + '_ {
struct SliceIter<'a> {
current: &'a [u8],
}

impl Iterator for SliceIter<'_> {
type Item = Result<Item, Error>;

fn next(&mut self) -> Option<Self::Item> {
match read_one_from_slice(self.current) {
Ok(Some((item, rest))) => {
self.current = rest;
Some(Ok(item))
}
Ok(None) => None,
Err(err) => Some(Err(err)),
}
}
}

SliceIter { current: pem_slice }
}

/// Extract and decode the next PEM section from `input`
///
/// - `Ok(None)` is returned if there is no PEM section to read from `input`
Expand Down Expand Up @@ -121,14 +143,14 @@ pub fn read_one_from_slice(mut input: &[u8]) -> Result<Option<(Item, &[u8])>, Er
/// You can use this function to build an iterator, for example:
/// `for item in iter::from_fn(|| read_one(rd).transpose()) { ... }`
#[cfg(feature = "std")]
pub fn read_one(rd: &mut dyn io::BufRead) -> Result<Option<Item>, IoError> {
pub fn read_one(rd: &mut dyn io::BufRead) -> Result<Option<Item>, Error> {
let mut b64buf = Vec::with_capacity(1024);
let mut section = None::<(Vec<_>, Vec<_>)>;
let mut line = Vec::with_capacity(80);

loop {
line.clear();
let len = read_until_newline(rd, &mut line).map_err(IoError::Io)?;
let len = read_until_newline(rd, &mut line).map_err(Error::Io)?;

let next_line = if len == 0 {
None
Expand All @@ -139,7 +161,7 @@ pub fn read_one(rd: &mut dyn io::BufRead) -> Result<Option<Item>, IoError> {
match read_one_impl(next_line, &mut section, &mut b64buf) {
Ok(ControlFlow::Break(opt)) => return Ok(opt),
Ok(ControlFlow::Continue(())) => continue,
Err(e) => return Err(IoError::Pem(e)),
Err(e) => return Err(e),
}
}
}
Expand Down Expand Up @@ -266,6 +288,6 @@ fn read_until_newline<R: io::BufRead + ?Sized>(r: &mut R, buf: &mut Vec<u8>) ->

/// Extract and return all PEM sections by reading `rd`.
#[cfg(feature = "std")]
pub fn read_all(rd: &mut dyn io::BufRead) -> impl Iterator<Item = Result<Item, IoError>> + '_ {
pub fn read_all(rd: &mut dyn io::BufRead) -> impl Iterator<Item = Result<Item, Error>> + '_ {
iter::from_fn(move || read_one(rd).transpose())
}

0 comments on commit 5be98ca

Please sign in to comment.