From d1636168b26cc842bc0766235c8a4f2da9663f20 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Tue, 5 Mar 2024 10:46:29 +0100 Subject: [PATCH] rust/pv: Support for writing data in PEM format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use existing OpenSSL functionalities to create PEM files containing arbitrary data. Acked-by: Marc Hartmayer Acked-by: Christoph Schlameuss Signed-off-by: Steffen Eiden Signed-off-by: Jan Höppner --- rust/pv/src/error.rs | 3 + rust/pv/src/lib.rs | 6 + rust/pv/src/openssl_extensions/bio.rs | 85 +++++++ rust/pv/src/openssl_extensions/mod.rs | 2 + .../src/openssl_extensions/stackable_crl.rs | 41 +--- rust/pv/src/pem_utils.rs | 222 ++++++++++++++++++ 6 files changed, 321 insertions(+), 38 deletions(-) create mode 100644 rust/pv/src/openssl_extensions/bio.rs create mode 100644 rust/pv/src/pem_utils.rs diff --git a/rust/pv/src/error.rs b/rust/pv/src/error.rs index af85e93e..3ba808f2 100644 --- a/rust/pv/src/error.rs +++ b/rust/pv/src/error.rs @@ -106,6 +106,9 @@ pub enum Error { )] AddDataMissing(&'static str), + #[error("An ASCII string was expected, but non-ASCII characters were received.")] + NonAscii, + // errors from other crates #[error(transparent)] PvCore(#[from] pv_core::Error), diff --git a/rust/pv/src/lib.rs b/rust/pv/src/lib.rs index 7a33210c..ec31b9a4 100644 --- a/rust/pv/src/lib.rs +++ b/rust/pv/src/lib.rs @@ -37,6 +37,7 @@ mod brcb; mod crypto; mod error; mod openssl_extensions; +mod pem_utils; mod req; mod utils; mod uvattest; @@ -71,6 +72,11 @@ pub mod attest { }; } +/// Definitions and functions to write objects in PEM format +pub mod pem { + pub use crate::pem_utils::Pem; +} + /// Miscellaneous functions and definitions pub mod misc { pub use pv_core::misc::*; diff --git a/rust/pv/src/openssl_extensions/bio.rs b/rust/pv/src/openssl_extensions/bio.rs new file mode 100644 index 00000000..73528eed --- /dev/null +++ b/rust/pv/src/openssl_extensions/bio.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use core::slice; +use openssl::error::ErrorStack; +use openssl_sys::BIO_new_mem_buf; +use std::ffi::c_int; +use std::{marker::PhantomData, ptr}; + +pub struct BioMem(*mut openssl_sys::BIO); + +impl Drop for BioMem { + fn drop(&mut self) { + // SAFETY: Pointer is valid. The pointer value is dropped after the free. + unsafe { + openssl_sys::BIO_free_all(self.0); + } + } +} + +impl BioMem { + pub fn new() -> Result { + openssl_sys::init(); + + // SAFETY: Returns a valid pointer or null. null-case is tested right after this. + let bio = unsafe { openssl_sys::BIO_new(openssl_sys::BIO_s_mem()) }; + match bio.is_null() { + true => Err(ErrorStack::get()), + false => Ok(Self(bio)), + } + } + + pub fn as_ptr(&self) -> *mut openssl_sys::BIO { + self.0 + } + + /// Copies the content of this slice into a Vec + pub fn to_vec(&self) -> Vec { + let buf; + // SAFTEY: BIO provides a continuous memory that can be used to build a slice. + unsafe { + let mut ptr = ptr::null_mut(); + let len = openssl_sys::BIO_get_mem_data(self.0, &mut ptr); + buf = slice::from_raw_parts(ptr as *const _ as *const _, len as usize) + } + buf.to_vec() + } +} + +pub struct BioMemSlice<'a>(*mut openssl_sys::BIO, PhantomData<&'a [u8]>); +impl Drop for BioMemSlice<'_> { + fn drop(&mut self) { + // SAFETY: Pointer is valid. The pointer value is dropped after the free. + unsafe { + openssl_sys::BIO_free_all(self.0); + } + } +} + +impl<'a> BioMemSlice<'a> { + pub fn new(buf: &'a [u8]) -> Result, ErrorStack> { + openssl_sys::init(); + + // SAFETY: `buf` is a slice (i.e. pointer+size) pointing to a valid memory region. + // So the resulting bio is valid. Lifetime of the slice is connected by this Rust + // structure. + assert!(buf.len() <= c_int::MAX as usize); + let bio = unsafe { + { + let r = BIO_new_mem_buf(buf.as_ptr() as *const _, buf.len() as c_int); + match r.is_null() { + true => Err(ErrorStack::get()), + false => Ok(r), + } + }? + }; + + Ok(BioMemSlice(bio, PhantomData)) + } + + pub fn as_ptr(&self) -> *mut openssl_sys::BIO { + self.0 + } +} diff --git a/rust/pv/src/openssl_extensions/mod.rs b/rust/pv/src/openssl_extensions/mod.rs index fab26638..f6234e5d 100644 --- a/rust/pv/src/openssl_extensions/mod.rs +++ b/rust/pv/src/openssl_extensions/mod.rs @@ -6,8 +6,10 @@ /// Extensions to the rust-openssl crate mod akid; +mod bio; mod crl; mod stackable_crl; pub use akid::*; +pub use bio::*; pub use crl::*; diff --git a/rust/pv/src/openssl_extensions/stackable_crl.rs b/rust/pv/src/openssl_extensions/stackable_crl.rs index aef7cf86..12a9f9de 100644 --- a/rust/pv/src/openssl_extensions/stackable_crl.rs +++ b/rust/pv/src/openssl_extensions/stackable_crl.rs @@ -2,16 +2,14 @@ // // Copyright IBM Corp. 2023 -use std::{marker::PhantomData, ptr}; - +use crate::openssl_extensions::bio::BioMemSlice; use foreign_types::{ForeignType, ForeignTypeRef}; use openssl::{ error::ErrorStack, stack::Stackable, x509::{X509Crl, X509CrlRef}, }; -use openssl_sys::BIO_new_mem_buf; -use std::ffi::c_int; +use std::ptr; #[derive(Debug)] pub struct StackableX509Crl(*mut openssl_sys::X509_CRL); @@ -62,44 +60,11 @@ impl Stackable for StackableX509Crl { type StackType = openssl_sys::stack_st_X509_CRL; } -pub struct MemBioSlice<'a>(*mut openssl_sys::BIO, PhantomData<&'a [u8]>); -impl Drop for MemBioSlice<'_> { - fn drop(&mut self) { - unsafe { - openssl_sys::BIO_free_all(self.0); - } - } -} - -impl<'a> MemBioSlice<'a> { - pub fn new(buf: &'a [u8]) -> Result, ErrorStack> { - openssl_sys::init(); - - assert!(buf.len() <= c_int::MAX as usize); - let bio = unsafe { - { - let r = BIO_new_mem_buf(buf.as_ptr() as *const _, buf.len() as c_int); - if r.is_null() { - Err(ErrorStack::get()) - } else { - Ok(r) - } - }? - }; - - Ok(MemBioSlice(bio, PhantomData)) - } - - pub fn as_ptr(&self) -> *mut openssl_sys::BIO { - self.0 - } -} - impl StackableX509Crl { pub fn stack_from_pem(pem: &[u8]) -> Result, ErrorStack> { unsafe { openssl_sys::init(); - let bio = MemBioSlice::new(pem)?; + let bio = BioMemSlice::new(pem)?; let mut crls = vec![]; loop { diff --git a/rust/pv/src/pem_utils.rs b/rust/pv/src/pem_utils.rs new file mode 100644 index 00000000..e6462519 --- /dev/null +++ b/rust/pv/src/pem_utils.rs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use crate::Result; +use crate::{openssl_extensions::BioMem, Error}; +use openssl::error::ErrorStack; +use pv_core::request::Confidential; +use std::{ + ffi::{c_char, CString}, + fmt::Display, +}; + +mod ffi { + use openssl_sys::BIO; + use std::ffi::{c_char, c_int, c_long, c_uchar}; + extern "C" { + pub fn PEM_write_bio( + bio: *mut BIO, + name: *const c_char, + header: *const c_char, + data: *const c_uchar, + len: c_long, + ) -> c_int; + } +} + +/// Thin wrapper around [`CString`] only containing ASCII chars. +#[derive(Debug)] +struct AsciiCString(CString); + +impl AsciiCString { + /// Convert from string + /// + /// # Returns + /// Error if string is not ASCII or contains null chars + pub(crate) fn from_str(s: &str) -> Result { + match s.is_ascii() { + true => Ok(Self(CString::new(s).map_err(|_| Error::NonAscii)?)), + false => Err(Error::NonAscii), + } + } + + fn as_ptr(&self) -> *const c_char { + self.0.as_ptr() + } +} + +/// Helper struct to construct the PEM format +#[derive(Debug)] +struct InnerPem<'d> { + name: AsciiCString, + header: Option, + data: &'d [u8], +} + +impl<'d> InnerPem<'d> { + fn new(name: &str, header: Option, data: &'d [u8]) -> Result { + Ok(Self { + name: AsciiCString::from_str(name)?, + header: match header { + Some(h) => Some(AsciiCString::from_str(&h)?), + None => None, + }, + data, + }) + } + + /// Generate PEM representation of the data + fn to_pem(&self) -> Result> { + let bio = BioMem::new()?; + let hdr_ptr = match self.header { + // avoid moving variable -> use reference + Some(ref h) => h.as_ptr(), + None => std::ptr::null(), + }; + + // SAFETY: + // All pointers point to valid C strings or memory regions + let rc = unsafe { + ffi::PEM_write_bio( + bio.as_ptr(), + self.name.as_ptr(), + hdr_ptr, + self.data.as_ptr(), + self.data.len() as std::ffi::c_long, + ) + }; + + match rc { + 1 => Err(Error::InternalSsl("Could not write PEM", ErrorStack::get())), + _ => Ok(bio.to_vec()), + } + } +} + +/// Data in PEM format +/// +/// Displays into a printable PEM structure. +/// Must be constructed from another structure in this library. +/// +/// ```rust,ignore +/// let pem: Pem = ...; +/// println!("PEM {pem}"); +/// ``` +/// ```PEM +///-----BEGIN ----- +///
+/// +/// +///-----END ----- + +#[derive(Debug)] +pub struct Pem { + pem: Confidential, +} + +#[allow(unused)] +impl Pem { + /// Create a new PEM structure. + /// + /// # Errors + /// + /// This function will return an error if name or header contain non-ASCII chars, or OpenSSL + /// could not generate the PEM (very likely due to OOM). + pub(crate) fn new(name: &str, header: H, data: D) -> Result + where + D: AsRef<[u8]>, + H: Into>, + { + let mut header = header.into(); + let header = match header { + Some(h) if h.ends_with('\n') => Some(h), + Some(h) if h.is_empty() => None, + Some(mut h) => { + h.push('\n'); + Some(h) + } + None => None, + }; + + let inner_pem = InnerPem::new(name, header, data.as_ref())?; + + // Create the PEM format eagerly so that to_string/display cannot fail because of ASCII or OpenSSL Errors + // Both error should be very unlikely + // OpenSSL should be able to create PEM if there is enough memory and produce a non-null + // terminated ASCII-string + // Unwrap succeeds it's all ASCII + // Std lib implements all the conversations without a copy + let pem = CString::new(inner_pem.to_pem()?) + .map_err(|_| Error::NonAscii)? + .into_string() + .unwrap() + .into(); + + Ok(Self { pem }) + } + + /// Converts the PEM-data into a byte vector. + /// + /// This consumes the `PEM`. + #[inline] + #[must_use = "`self` will be dropped if the result is not used"] + pub fn into_bytes(self) -> Confidential> { + self.pem.into_inner().into_bytes().into() + } +} + +impl Display for Pem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.pem.value().fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn no_data() { + const EXP: &str = + "-----BEGIN PEM test-----\ntest hdr value: 17\n\n-----END PEM test-----\n"; + let test_pem = Pem::new("PEM test", "test hdr value: 17".to_string(), []).unwrap(); + let pem_str = test_pem.to_string(); + assert_eq!(pem_str, EXP); + } + + #[test] + fn no_hdr() { + const EXP: &str = + "-----BEGIN PEM test-----\ndmVyeSBzZWNyZXQga2V5\n-----END PEM test-----\n"; + let test_pem = Pem::new("PEM test", None, "very secret key").unwrap(); + let pem_str = test_pem.to_string(); + assert_eq!(pem_str, EXP); + } + + #[test] + fn some_data() { + const EXP: &str= "-----BEGIN PEM test-----\ntest hdr value: 17\n\ndmVyeSBzZWNyZXQga2V5\n-----END PEM test-----\n"; + let test_pem = Pem::new( + "PEM test", + "test hdr value: 17".to_string(), + "very secret key", + ) + .unwrap(); + let pem_str = test_pem.to_string(); + assert_eq!(pem_str, EXP); + } + + #[test] + fn data_linebreak() { + const EXP: &str= "-----BEGIN PEM test-----\ntest hdr value: 17\n\ndmVyeSBzZWNyZXQga2V5\n-----END PEM test-----\n"; + let test_pem = Pem::new( + "PEM test", + "test hdr value: 17\n".to_string(), + "very secret key", + ) + .unwrap(); + let pem_str = test_pem.to_string(); + assert_eq!(pem_str, EXP); + } +}