// SPDX-License-Identifier: MIT // // Copyright IBM Corp. 2024 #![allow(unused)] use std::io::{ErrorKind, Read, Seek, SeekFrom, Write}; use std::mem::size_of; use anyhow::{anyhow, bail, Error, Result}; use pv::assert_size; use pv::request::MagicValue; use pv::uv::{AttestationCmd, ConfigUid}; use zerocopy::{BigEndian, ByteOrder, FromBytes, Immutable, IntoBytes, KnownLayout, U32, U64}; use crate::additional; const INV_EXCHANGE_FMT_ERROR_TEXT: &str = "The input has not the correct format:"; #[repr(C)] #[derive(Debug, IntoBytes, PartialEq, Eq, Default, FromBytes, Immutable, KnownLayout)] struct Entry { size: U32, offset: U32, } assert_size!(Entry, 8); /// If size == 0 the offset is ignored. (entry does not exist) /// If offset >0 and <0x40 -> invalid format /// If offset == 0 and size > 0 no data saved, however the request will need this amount of memory /// to succeed. Only makes sense for measurement and additional data. This however, is not /// enforced. impl Entry { fn new(size: u32, offset: u32) -> Self { Self { size: size.into(), offset: offset.into(), } } /// # Panic /// /// panics if `val` is larger than `max_size` bytes fn from_slice(val: Option<&[u8]>, max_size: u32, offset: &mut u32) -> Self { val.map_or_else(Self::default, |val| { assert!(val.len() <= max_size as usize); let size = val.len() as u32; let res = Self::new(size, *offset); *offset += size; res }) } /// # Panic /// /// panics if `val` is larger than `max_size` bytes fn from_exp(val: Option) -> Self { val.map_or_else(Self::default, |val| Self::new(val, 0)) } fn from_none() -> Self { Self::default() } /// Reads data from stream if required fn read(&self, reader: &mut R) -> Result where R: Read + Seek, { match self { Self { size, .. } if size.get() == 0 => Ok(ExpOrData::None), Self { size, offset } if offset.get() == 0 => Ok(ExpOrData::Exp(size.get())), Self { size, offset } => { reader.seek(SeekFrom::Start(offset.get() as u64))?; let mut buf = vec![0; size.get() as usize]; reader.read_exact(&mut buf)?; Ok(ExpOrData::Data(buf)) } } } } #[repr(C)] #[derive(Debug, IntoBytes, FromBytes, Immutable, KnownLayout)] struct ExchangeFormatV1Hdr { magic: U64, version: U32, size: U32, reserved: U64, /// v1 specific arcb: Entry, measurement: Entry, additional: Entry, user: Entry, config_uid: Entry, } assert_size!(ExchangeFormatV1Hdr, 0x40); impl ExchangeFormatV1Hdr { fn new_request(arcb: &[u8], measurement: u32, additional: u32) -> Result { let mut offset: u32 = size_of::() as u32; let arcb_entry = Entry::from_slice(Some(arcb), AttestationCmd::ARCB_MAX_SIZE, &mut offset); let measurement_entry = Entry::from_exp(Some(measurement)); let exp_add = match additional { 0 => None, size => { if size > AttestationCmd::ADDITIONAL_MAX_SIZE { bail!( "Additional data size ({}) exceeds maximum allowed size ({})", size, AttestationCmd::ADDITIONAL_MAX_SIZE ); } Some(size) } }; let additional_entry = Entry::from_exp(exp_add); let user_entry = Entry::from_none(); let cuid_entry = Entry::from_none(); Ok(Self { magic: U64::from_bytes(ExchangeMagic::MAGIC), version: ExchangeFormatVersion::One.into(), size: offset.into(), reserved: 0.into(), arcb: arcb_entry, measurement: measurement_entry, additional: additional_entry, user: user_entry, config_uid: cuid_entry, }) } fn new_response( arcb: &[u8], measurement: &[u8], additional: Option<&[u8]>, user: Option<&[u8]>, config_uid: &[u8], ) -> Result { let mut offset: u32 = size_of::() as u32; let arcb_entry = Entry::from_slice(Some(arcb), AttestationCmd::ARCB_MAX_SIZE, &mut offset); let measurement_entry = Entry::from_slice( Some(measurement), AttestationCmd::MEASUREMENT_MAX_SIZE, &mut offset, ); let additional_entry = Entry::from_slice(additional, AttestationCmd::ADDITIONAL_MAX_SIZE, &mut offset); let user_entry = Entry::from_slice(user, AttestationCmd::USER_MAX_SIZE, &mut offset); let cuid_entry = Entry::from_slice(Some(config_uid), 0x10, &mut offset); Ok(Self { magic: U64::from_bytes(ExchangeMagic::MAGIC), version: ExchangeFormatVersion::One.into(), size: offset.into(), reserved: 0.into(), arcb: arcb_entry, measurement: measurement_entry, additional: additional_entry, user: user_entry, config_uid: cuid_entry, }) } } /// The magic value used to identify an [`ExchangeFormatRequest`] /// /// The magic value is ASCII: /// ```rust /// # use s390_pv_core::attest::ExchangeMagic; /// # use s390_pv_core::request::MagicValue; /// # fn main() { /// # let magic = /// b"pvattest" /// # ; /// # assert!(ExchangeMagic::starts_with_magic(magic)); /// # } /// ``` pub struct ExchangeMagic; impl MagicValue<8> for ExchangeMagic { const MAGIC: [u8; 8] = [0x70, 0x76, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74]; } /// Version identifier for an [`ExchangeFormatRequest`] #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExchangeFormatVersion { /// Version 1 (= 0x0100) One = 0x0100, } impl TryFrom> for ExchangeFormatVersion { type Error = Error; fn try_from(value: U32) -> Result { if value.get() == Self::One as u32 { Ok(Self::One) } else { bail!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Unsupported version: ({})", value.get() ); } } } impl From for U32 { fn from(value: ExchangeFormatVersion) -> Self { (value as u32).into() } } /// A parsed exchange entry value /// /// An entry can be all zero(None), just a size (Exp) or a offset+size to some data (Data) #[derive(Debug, PartialEq, Eq)] pub(crate) enum ExpOrData { Exp(u32), Data(Vec), None, } impl ExpOrData { /// calculates the (expected or real) size fn size(&self) -> u32 { match self { Self::Exp(s) => *s, // size is max u32 large as read in before Self::Data(v) => v.len() as u32, Self::None => 0, } } /// Returns data if self is [`ExpOrData::Data`] /// /// Consumes itself fn data(self) -> Option> { match self { Self::Data(v) => Some(v), _ => None, } } } impl From> for ExpOrData { fn from(value: Option) -> Self { value.map_or(Self::None, Self::Exp) } } impl From<&ExpOrData> for Option { fn from(value: &ExpOrData) -> Self { match value { ExpOrData::Exp(v) => Some(*v), _ => None, } } } impl From for Option> { fn from(value: ExpOrData) -> Self { match value { ExpOrData::Exp(s) => Some(vec![0; s as usize]), ExpOrData::Data(d) => Some(d), ExpOrData::None => None, } } } /// The _exchange format_ is a simple file format to send labeled binary blobs between /// pvattest instances on different machines. #[derive(Debug, PartialEq, Eq)] pub struct ExchangeFormatRequest { // all sizes are guaranteed to fit in the exchange format/UV Call at any time // pub to allow deconstruction of this struct pub arcb: Vec, pub exp_measurement: u32, pub exp_additional: u32, } /// The _exchange format_ is a simple file format to send labeled binary blobs between /// pvattest instances on different machines. #[derive(Debug, PartialEq, Eq)] pub struct ExchangeFormatResponse { // all sizes are guaranteed to fit in the exchange format/UV Call at any time // pub to allow deconstruction of this struct pub arcb: Vec, pub measurement: Vec, pub additional: Option>, pub user: Option>, pub config_uid: ConfigUid, } impl ExchangeFormatRequest { /// Creates a new exchange context, with an attestation request, expected measurement and /// optional an additional data size. Useful for creating a attestation request. pub fn new(arcb: Vec, exp_measurement: u32, exp_additional: u32) -> Result { verify_size( exp_measurement, 1, AttestationCmd::MEASUREMENT_MAX_SIZE, "Expected measurement size", )?; verify_size( exp_additional, 0, AttestationCmd::ADDITIONAL_MAX_SIZE, "Expected additional data size", )?; verify_slice(&arcb, AttestationCmd::ARCB_MAX_SIZE, "Attestation request")?; Ok(Self { arcb, exp_measurement, exp_additional, }) } fn write_v1(&self, writer: &mut W) -> Result<()> where W: Write, { let hdr = ExchangeFormatV1Hdr::new_request( self.arcb.as_slice(), self.exp_measurement, self.exp_additional, )?; writer.write_all(hdr.as_bytes())?; writer.write_all(&self.arcb)?; Ok(()) } /// Serializes the encapsulated data into the provides stream in the provided format pub fn write(&self, writer: &mut W, version: ExchangeFormatVersion) -> Result<()> where W: Write, { match version { ExchangeFormatVersion::One => self.write_v1(writer), } } /// Reads and deserializes the exchange file in the provided stream /// /// # Errors /// /// Returns an error if the stream does not contain data in exchange format, CUID or user data /// do not fit, or any IO error that can appear during reading streams. pub fn read(reader: &mut R) -> Result where R: Read + Seek, { let mut buf = vec![0; size_of::()]; match reader.read_exact(&mut buf) { Ok(it) => it, // report hdr file to small for header Err(err) if err.kind() == ErrorKind::UnexpectedEof => { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."); } Err(err) => return Err(err.into()), }; if !ExchangeMagic::starts_with_magic(&buf) { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Does not start with the magic value.",); } let hdr = ExchangeFormatV1Hdr::ref_from_bytes(buf.as_slice()) .map_err(|_| anyhow!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."))?; match TryInto::::try_into(hdr.version)? { ExchangeFormatVersion::One => (), } if stream_len(reader)? < hdr.size.get() as u64 { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} File size too small"); } let arcb = hdr.arcb.read(reader)?.data().ok_or(anyhow!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation request.", ))?; let measurement = hdr.measurement.read(reader)?.size(); let additional = hdr.additional.read(reader)?.size(); Self::new(arcb, measurement, additional) } } // Seek::stream_is unstable // not expose to API users // taken from rust std::io::seek; fn stream_len(seek: &mut S) -> Result where S: Seek, { let old_pos = seek.stream_position()?; let len = seek.seek(SeekFrom::End(0))?; // Avoid seeking a third time when we were already at the end of the // stream. The branch is usually way cheaper than a seek operation. if old_pos != len { seek.seek(SeekFrom::Start(old_pos))?; } Ok(len) } fn verify_size(size: u32, min_size: u32, max_size: u32, field: &'static str) -> Result<()> { if size < min_size { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too small ({size})"); } if size > max_size { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too large ({size})"); } Ok(()) } /// check that a slice has at max `max_size` amount of bytes fn verify_slice(val: &[u8], max_size: u32, field: &'static str) -> Result<()> { if val.len() > max_size as usize { bail!( "{INV_EXCHANGE_FMT_ERROR_TEXT} The {field} field is too large ({})", val.len() ); } Ok(()) } impl ExchangeFormatResponse { /// Creates a new exchange context, with an attestation request, measurement and /// cuid. pub fn new( arcb: Vec, measurement: Vec, additional: Option>, user: Option>, config_uid: ConfigUid, ) -> Result { // should not fail; Already checked during import. verify_slice( &arcb, AttestationCmd::ARCB_MAX_SIZE, "Attestation request data", )?; verify_slice( &measurement, AttestationCmd::MEASUREMENT_MAX_SIZE, "Attestation Measurement", )?; if let Some(additional) = &additional { verify_slice( additional, AttestationCmd::ADDITIONAL_MAX_SIZE, "Additional data", )?; } if let Some(user) = &user { verify_slice(user, AttestationCmd::USER_MAX_SIZE, "User data")?; } Ok(Self { arcb, measurement, additional, user, config_uid, }) } fn write_v1(&self, writer: &mut W) -> Result<()> where W: Write, { let hdr = ExchangeFormatV1Hdr::new_response( self.arcb.as_slice(), &self.measurement, self.additional.as_deref(), self.user.as_deref(), &self.config_uid, )?; writer.write_all(hdr.as_bytes())?; writer.write_all(&self.arcb)?; writer.write_all(&self.measurement)?; if let Some(data) = &self.additional { writer.write_all(data)?; } if let Some(data) = &self.user { writer.write_all(data)?; } writer.write_all(&self.config_uid)?; Ok(()) } /// Serializes the encapsulated data into the provides stream in the provided format pub fn write(&self, writer: &mut W, version: ExchangeFormatVersion) -> Result<()> where W: Write, { match version { ExchangeFormatVersion::One => self.write_v1(writer), } } /// Reads and deserializes the exchange file in the provided stream /// /// # Errors /// /// Returns an error if the stream does not contain data in exchange format, CUID or user data /// do not fit, or any IO error that can appear during reading streams. pub fn read(reader: &mut R) -> Result where R: Read + Seek, { let mut buf = vec![0; size_of::()]; match reader.read_exact(&mut buf) { Ok(it) => it, // report hdr file to small for header Err(err) if err.kind() == ErrorKind::UnexpectedEof => { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."); } Err(err) => return Err(err.into()), }; if !ExchangeMagic::starts_with_magic(&buf) { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Does not start with the magic value."); } let hdr = ExchangeFormatV1Hdr::ref_from_bytes(buf.as_slice()) .map_err(|_| anyhow!("{INV_EXCHANGE_FMT_ERROR_TEXT} Invalid Header."))?; match TryInto::::try_into(hdr.version)? { ExchangeFormatVersion::One => (), } if stream_len(reader)? < hdr.size.get() as u64 { bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} File size too small"); } let arcb = hdr.arcb.read(reader)?.data().ok_or(anyhow!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation request.", ))?; let measurement = hdr.measurement.read(reader)?.data().ok_or(anyhow!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation response (Measurement missing).", ))?; let additional = hdr.additional.read(reader)?.data(); let user = hdr.user.read(reader)?.data(); let config_uid: ConfigUid = match hdr.config_uid.read(reader)?.data() { Some(v) => v.try_into().map_err(|_| { anyhow!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Configuration UID has an invalid size. Expected size 16, is {}",hdr.config_uid.size.get() ) })?, None => bail!("{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation response (CUID missing).") , }; Self::new(arcb, measurement, additional, user, config_uid) } /// Returns the measurement of this [`ExchangeFormatRequest`]. pub fn measurement(&self) -> &[u8] { &self.measurement } /// Returns the additional data of this [`ExchangeFormatRequest`]. pub fn additional(&self) -> Option<&[u8]> { self.additional.as_deref() } /// Returns the user data of this [`ExchangeFormatRequest`]. pub fn user(&self) -> Option<&[u8]> { self.user.as_deref() } /// Returns the config UID of this [`ExchangeFormatRequest`]. /// /// # Error /// Returns an error if the [`ExchangeFormatRequest`] contains no CUID, pub const fn config_uid(&self) -> &ConfigUid { &self.config_uid } /// Returns a reference to the attestation request of this [`ExchangeFormatRequest`]. pub fn arcb(&self) -> &[u8] { self.arcb.as_ref() } } #[cfg(test)] mod test { use std::io::Cursor; use pv::misc::read_file; use super::*; #[test] fn exchange_from_slice() { let val = &[0; 17]; let mut offset = 18; let entry = Entry::from_slice(Some(val), 20, &mut offset); assert_eq!( entry, Entry { size: 17.into(), offset: 18.into(), } ); assert_eq!(offset, 18 + 17); } static ARCB: [u8; 16] = [0x11; 16]; static MEASUREMENT: [u8; 64] = [0x12; 64]; static ADDITIONAL: [u8; 32] = [0x13; 32]; static CUID: [u8; 16] = [0x14; 16]; static USER: [u8; 256] = [0x15; 256]; fn test_read_write_request( path: &'static str, arcb: Vec, measurement: usize, additional: usize, ) { // TODO as 32 checks let ctx_write = ExchangeFormatRequest::new(arcb, measurement as u32, additional as u32) .expect("exchange fmt creation"); // let mut out = create_file(path).unwrap(); let mut out = vec![]; ctx_write .write(&mut out, ExchangeFormatVersion::One) .unwrap(); let buf = read_file(path, "test read exchange").unwrap(); assert_eq!(out, buf); let ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap(); assert_eq!(ctx_read, ctx_write); } fn test_read_write_response( path: &'static str, arcb: Vec, measurement: Vec, additional: Option>, user: Option>, cuid: ConfigUid, ) { let ctx_write = ExchangeFormatResponse::new(arcb, measurement, additional, user, cuid) .expect("exchange fmt creation"); // let mut out = create_file(path).unwrap(); let mut out = vec![]; ctx_write .write(&mut out, ExchangeFormatVersion::One) .unwrap(); let buf = read_file(path, "test read exchange").unwrap(); assert_eq!(out, buf); let ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap(); assert_eq!(ctx_read, ctx_write); } #[test] fn full_req() { test_read_write_request( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/full_req.bin" ), ARCB.to_vec(), MEASUREMENT.len(), ADDITIONAL.len(), ); } #[test] fn add_req() { test_read_write_request( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/add_req.bin" ), ARCB.to_vec(), MEASUREMENT.len(), ADDITIONAL.len(), ); } #[test] fn invalid_req() { ExchangeFormatRequest::new(ARCB.to_vec(), 0, ADDITIONAL.len() as u32).unwrap_err(); } #[test] fn test_additional_data_size_validation() { // Test for TODO 1 fix: Additional data size validation let arcb = ARCB.to_vec(); // Test with valid size at maximum let result = ExchangeFormatV1Hdr::new_request( &arcb, MEASUREMENT.len() as u32, AttestationCmd::ADDITIONAL_MAX_SIZE, ); assert!( result.is_ok(), "Maximum additional data size should be accepted" ); // Test with size exceeding maximum let result = ExchangeFormatV1Hdr::new_request( &arcb, MEASUREMENT.len() as u32, AttestationCmd::ADDITIONAL_MAX_SIZE + 1, ); assert!( result.is_err(), "Additional data size exceeding maximum should fail" ); if let Err(e) = result { let error_msg = e.to_string(); assert!( error_msg.contains("exceeds maximum"), "Error should mention exceeding maximum: {}", error_msg ); } // Test with zero size (no additional data) let result = ExchangeFormatV1Hdr::new_request(&arcb, MEASUREMENT.len() as u32, 0); assert!( result.is_ok(), "Zero additional data size should be accepted" ); } #[test] fn min_req() { test_read_write_request( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" ), ARCB.to_vec(), MEASUREMENT.len(), 0, ); } #[test] fn full_resp() { test_read_write_response( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/full_resp.bin" ), ARCB.to_vec(), MEASUREMENT.to_vec(), ADDITIONAL.to_vec().into(), USER.to_vec().into(), CUID, ); } #[test] fn add_resp() { test_read_write_response( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/add_resp.bin" ), ARCB.to_vec(), MEASUREMENT.to_vec(), ADDITIONAL.to_vec().into(), None, CUID, ); } #[test] fn user_resp() { test_read_write_response( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/user_resp.bin" ), ARCB.to_vec(), MEASUREMENT.to_vec(), None, USER.to_vec().into(), CUID, ); } #[test] fn min_resp() { test_read_write_response( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_resp.bin" ), ARCB.to_vec(), MEASUREMENT.to_vec(), None, None, CUID, ) } #[test] fn resp_no_cuid() { let buf = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" )); let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn resp_inv_magic() { let mut buf = read_file( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" ), "test resp inv magic", ) .unwrap(); // tamper with the magic buf[0] = !buf[0]; let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn no_arcb() { let mut buf = read_file( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" ), "test resp inv magic", ) .unwrap(); // delete the arcb entry buf[0x18..0x20].copy_from_slice(&[0; 8]); let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn small() { let mut buf = read_file( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" ), "test resp inv magic", ) .unwrap(); buf.pop(); let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn hdr() { // buffer smaller than the header but containing the magic let buf = [ 0x70, 0x76, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x1, 0x2, 0x3, 0x4, ]; let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn version() { let mut buf = read_file( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_req.bin" ), "test resp inv magic", ) .unwrap(); // tamper with the version buf[0x8] = 0xff; let _ctx_read = ExchangeFormatRequest::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } #[test] fn cuid_size() { let mut buf = read_file( concat!( env!("CARGO_MANIFEST_DIR"), "/tests/assets/", "exp/exchange/min_resp.bin" ), "test resp inv magic", ) .unwrap(); // tamper with the cuid size buf[0x3b] = 0xf; let _ctx_read = ExchangeFormatResponse::read(&mut Cursor::new(&mut &buf)).unwrap_err(); } }