468 lines
13 KiB
Diff
468 lines
13 KiB
Diff
|
From d1636168b26cc842bc0766235c8a4f2da9663f20 Mon Sep 17 00:00:00 2001
|
||
|
From: Steffen Eiden <seiden@linux.ibm.com>
|
||
|
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 <marc@linux.ibm.com>
|
||
|
Acked-by: Christoph Schlameuss <schlameuss@linux.ibm.com>
|
||
|
Signed-off-by: Steffen Eiden <seiden@linux.ibm.com>
|
||
|
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
|
||
|
---
|
||
|
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<Self, ErrorStack> {
|
||
|
+ 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<u8> {
|
||
|
+ 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<BioMemSlice<'a>, 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<MemBioSlice<'a>, 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<Vec<X509Crl>, 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<Self> {
|
||
|
+ 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<AsciiCString>,
|
||
|
+ data: &'d [u8],
|
||
|
+}
|
||
|
+
|
||
|
+impl<'d> InnerPem<'d> {
|
||
|
+ fn new(name: &str, header: Option<String>, data: &'d [u8]) -> Result<Self> {
|
||
|
+ 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<Vec<u8>> {
|
||
|
+ 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 <name>-----
|
||
|
+///<header>
|
||
|
+///
|
||
|
+///<Base64 formatted binary data>
|
||
|
+///-----END <name>-----
|
||
|
+
|
||
|
+#[derive(Debug)]
|
||
|
+pub struct Pem {
|
||
|
+ pem: Confidential<String>,
|
||
|
+}
|
||
|
+
|
||
|
+#[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<D, H>(name: &str, header: H, data: D) -> Result<Self>
|
||
|
+ where
|
||
|
+ D: AsRef<[u8]>,
|
||
|
+ H: Into<Option<String>>,
|
||
|
+ {
|
||
|
+ 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<Vec<u8>> {
|
||
|
+ 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);
|
||
|
+ }
|
||
|
+}
|