From 61c5d7d4314094867e406a774265c31ec532707e Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 26 Feb 2024 12:27:44 +0100 Subject: [PATCH] rust/pv: Attestation generation and verification support Add functionality to generate Attestation Measurement requests. Add functionality to verify Attestation Measurement responses. Acked-by: Marc Hartmayer Signed-off-by: Steffen Eiden --- rust/pv/src/crypto.rs | 2 - rust/pv/src/error.rs | 19 ++ rust/pv/src/lib.rs | 28 ++ rust/pv/src/req.rs | 33 +- rust/pv/src/uvattest.rs | 5 + rust/pv/src/uvattest/additional.rs | 123 +++++++ rust/pv/src/uvattest/arcb.rs | 456 ++++++++++++++++++++++++++ rust/pv/src/uvattest/attest.rs | 245 ++++++++++++++ rust/pv/tests/assets/exp/arcb.bin | Bin 0 -> 400 bytes rust/pv/tests/assets/exp/exchange.bin | Bin 0 -> 448 bytes rust/pv_core/src/uvdevice/attest.rs | 12 +- rust/pv_core/src/uvdevice/ffi.rs | 4 +- 12 files changed, 914 insertions(+), 13 deletions(-) create mode 100644 rust/pv/src/uvattest.rs create mode 100644 rust/pv/src/uvattest/additional.rs create mode 100644 rust/pv/src/uvattest/arcb.rs create mode 100644 rust/pv/src/uvattest/attest.rs create mode 100644 rust/pv/tests/assets/exp/arcb.bin create mode 100644 rust/pv/tests/assets/exp/exchange.bin diff --git a/rust/pv/src/crypto.rs b/rust/pv/src/crypto.rs index 2424ac7b..ecd13e02 100644 --- a/rust/pv/src/crypto.rs +++ b/rust/pv/src/crypto.rs @@ -237,7 +237,6 @@ pub(crate) fn encrypt_aes_gcm( /// # Errors /// /// This function will return an error if the data could not be encrypted by OpenSSL. -#[allow(unused)] pub(crate) fn decrypt_aes_gcm( key: &SymKey, iv: &[u8], @@ -272,7 +271,6 @@ pub(crate) fn hash(t: MessageDigest, data: &[u8]) -> Result { } /// Calculate the HMAC of the given message. -#[allow(unused)] pub(crate) fn calculate_hmac( hmac_key: &PKeyRef, dgst: MessageDigest, diff --git a/rust/pv/src/error.rs b/rust/pv/src/error.rs index d3cf2cf1..2c39af0a 100644 --- a/rust/pv/src/error.rs +++ b/rust/pv/src/error.rs @@ -85,6 +85,25 @@ pub enum Error { #[error("Invalid input size ({0}) for boot hdr")] InvBootHdrSize(usize), + #[error("Input does not contain an attestation request")] + NoArcb, + + #[error("The attestation request has an unknown version (.0)")] + BinArcbInvVersion(u32), + + #[error( + "The attestation request encrypted sice is to0 small (.0). Request probably tampered with." + )] + BinArcbSeaSmall(u32), + + #[error("The input is missing the Configuration UID entry. It is probably not an attestation response")] + AttExCuidMissing, + + #[error( + "Attestation flags indicating that the additional data contains {0}, but no data was provided." + )] + AddDataMissing(&'static str), + // 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 35677c97..b419424b 100644 --- a/rust/pv/src/lib.rs +++ b/rust/pv/src/lib.rs @@ -20,12 +20,27 @@ //! //! ## Lock //! [`uv::UvDevice`] and [`uv::LockCmd`] +//! +//! # Attestation +//! +//! This crate provides functionalities for creating, performing, and verifying Attestation +//! measurements for _IBM Secure Execution for Linux_. See: +//! +//! ## Create +//! [`attest::AttestationRequest`] +//! +//! ## Perform +//! [`uv::UvDevice`] and [`uv::AttestationCmd`] +//! +//! # Verify +//! [`attest::AttestationItems`], [`attest::AttestationMeasurement`] mod brcb; mod confidential; mod crypto; mod error; mod req; mod utils; +mod uvattest; mod uvsecret; mod verify; @@ -45,6 +60,17 @@ pub mod uv { pub use pv_core::uv::*; } +/// Functionalities for creating attestation requests +pub mod attest { + pub use crate::uvattest::{ + additional::AdditionalData, + arcb::{AttestationAuthenticated, AttestationRequest}, + arcb::{AttestationFlags, AttestationVersion}, + attest::{AttestationItems, AttestationMeasurement}, + }; + pub use pv_core::attest::*; +} + /// Miscellaneous functions and definitions pub mod misc { pub use crate::utils::read_certs; @@ -53,6 +79,8 @@ pub mod misc { pub use crate::error::HkdVerifyErrorType; pub use error::{Error, Result}; +pub use pv_core::Error as PvCoreError; +pub use pv_core::{FileAccessErrorType, FileIoErrorType}; /// Functionalities to build UV requests pub mod request { diff --git a/rust/pv/src/req.rs b/rust/pv/src/req.rs index 5e53e635..934b8468 100644 --- a/rust/pv/src/req.rs +++ b/rust/pv/src/req.rs @@ -4,10 +4,11 @@ use crate::assert_size; use crate::crypto::{ - derive_key, encrypt_aes_gcm, gen_ec_key, random_array, AesGcmResult, SymKey, SymKeyType, - AES_256_GCM_TAG_SIZE, + decrypt_aes_gcm, derive_key, encrypt_aes_gcm, gen_ec_key, random_array, AesGcmResult, SymKey, + SymKeyType, AES_256_GCM_TAG_SIZE, }; use crate::misc::to_u32; +use crate::request::Confidential; use crate::{Error, Result}; use openssl::bn::{BigNum, BigNumContext}; use openssl::ec::{EcGroupRef, EcPointRef}; @@ -256,6 +257,11 @@ impl ReqEncrCtx { pub(crate) fn encrypt_aead(&self, aad: &[u8], conf: &[u8]) -> Result { encrypt_aes_gcm(&self.prot_key, &self.iv, aad, conf) } + + /// Returns a reference to the request protection key of this [`ReqEncrCtx`]. + pub fn prot_key(&self) -> &SymKey { + &self.prot_key + } } #[repr(C)] @@ -375,7 +381,6 @@ pub trait Request { /// A struct to represent some parts of a binary/encrypted request. #[derive(Debug)] -#[allow(unused)] #[allow(clippy::len_without_is_empty)] pub struct BinReqValues<'a> { iv: &'a [u8], @@ -432,6 +437,28 @@ impl<'a> BinReqValues<'a> { pub fn len(&self) -> usize { self.len } + + /// Returns the size of the encrypted area + pub fn sea(&self) -> u32 { + self.encr.len() as u32 + } + + /// Decrypts the encrypted area with the provided key + pub fn decrypt(&self, key: &SymKey) -> Result>> { + decrypt_aes_gcm(key, self.iv, self.aad, self.encr, self.tag) + } + + /// Returns a reference to the request dependent authenticated area of this [`BinReqValues`] + /// already interpreted. + /// + /// If target struct is larger than the request dependend-aad None is returned. See + /// [`FromBytes::ref_from_prefix`] + pub fn req_dep_aad(&self) -> Option<&T> + where + T: FromBytes + Sized, + { + T::ref_from_prefix(self.req_dep_aad) + } } #[cfg(test)] diff --git a/rust/pv/src/uvattest.rs b/rust/pv/src/uvattest.rs new file mode 100644 index 00000000..c0b97bc4 --- /dev/null +++ b/rust/pv/src/uvattest.rs @@ -0,0 +1,5 @@ +pub mod additional; +pub mod arcb; +pub mod attest; + +type AttNonce = [u8; 16]; diff --git a/rust/pv/src/uvattest/additional.rs b/rust/pv/src/uvattest/additional.rs new file mode 100644 index 00000000..6f9b7483 --- /dev/null +++ b/rust/pv/src/uvattest/additional.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 +use super::arcb::AttestationFlags; +use crate::req::Keyslot; +use crate::static_assert; +use crate::{Error, Result}; +use serde::Serialize; +use std::fmt::Display; +use zerocopy::FromBytes; + +/// Hash for additional-data stuff used for parsing [`AdditionalData`] +pub(crate) type AttAddHash = [u8; ATT_ADD_HASH_SIZE as usize]; +pub(crate) const ATT_ADD_HASH_SIZE: u32 = 0x20; +static_assert!(Keyslot::PHKH_SIZE == ATT_ADD_HASH_SIZE); + +/// Struct describing the additional-data of an Attestation Request +#[derive(Serialize, Debug)] +#[serde(default)] +pub struct AdditionalData +where + T: Serialize, +{ + #[serde(skip_serializing_if = "Option::is_none")] + image_phkh: Option, + #[serde(skip_serializing_if = "Option::is_none")] + attestation_phkh: Option, +} + +impl Display for AdditionalData +where + T: Display + Serialize, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn write_field( + f: &mut std::fmt::Formatter<'_>, + name: &'static str, + s: &Option, + ) -> std::fmt::Result { + if let Some(hash) = s { + writeln!(f, "{name}")?; + match f.alternate() { + true => writeln!(f, "{hash:#}")?, + false => writeln!(f, "{hash}")?, + }; + } + Ok(()) + } + write_field(f, "Image PHKH", &self.image_phkh)?; + write_field(f, "Attestation PHKH", &self.attestation_phkh) + } +} + +fn read_hash<'a>( + data: &'a [u8], + read: bool, + name: &'static str, +) -> Result<(Option<&'a AttAddHash>, &'a [u8])> { + match read { + true => { + let (v, data) = + AttAddHash::slice_from_prefix(data, 1).ok_or(Error::AddDataMissing(name))?; + // slice from prefix ensures that there is 1 element. + Ok((Some(&v[0]), data)) + } + false => Ok((None, data)), + } +} + +impl AdditionalData { + /// Provides a reference to the image public host key hash. + /// + /// This is the hash of the public host key of the corresponding private machine key that + /// decrypted the Secure Execution guest. + /// Contains a value if that value was requested by the attestation request. + pub fn image_public_host_key_hash(&self) -> Option<&T> { + self.image_phkh.as_ref() + } + + /// Provides a reference to the attestation public host key hash. + /// + /// This is the hash of the public host key of the corresponding private machine key that + /// decrypted the Attestation request. + /// Contains a value if that value was requested by the attestation request. + pub fn attestation_public_host_key_hash(&self) -> Option<&T> { + self.attestation_phkh.as_ref() + } +} + +impl<'a, T: Serialize + From<&'a [u8]> + Sized> AdditionalData { + /// Create Additional data from the u8-slice variant + pub fn from_other(other: AdditionalData<&'a [u8]>) -> Self { + let AdditionalData { + image_phkh, + attestation_phkh, + } = other; + Self { + image_phkh: image_phkh.map(|i| i.into()), + attestation_phkh: attestation_phkh.map(|i| i.into()), + } + } +} + +impl<'a> AdditionalData<&'a [u8]> { + /// Create from a slice of additional-data + /// + /// `flags`: Flags indicating which additional-data field is present. + /// + /// # Error + /// + /// Fails if there is a mismatch between the data and the flags. Should not happen after a + /// successful attestation verification. + pub fn from_slice(data: &'a [u8], flags: &AttestationFlags) -> Result { + let _data = data; + let (image_phkh, _data) = read_hash(data, flags.image_phkh(), "Image PHKH")?; + let (attestation_phkh, _data) = read_hash(data, flags.attest_phkh(), "Attestation PHKH")?; + + Ok(Self { + image_phkh: image_phkh.map(|v| v.as_slice()), + attestation_phkh: attestation_phkh.map(|v| v.as_slice()), + }) + } +} diff --git a/rust/pv/src/uvattest/arcb.rs b/rust/pv/src/uvattest/arcb.rs new file mode 100644 index 00000000..a5a5a645 --- /dev/null +++ b/rust/pv/src/uvattest/arcb.rs @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 +use super::{additional::ATT_ADD_HASH_SIZE, AttNonce}; +use crate::{ + assert_size, + attest::{AttestationMagic, AttestationMeasAlg}, + crypto::random_array, + misc::Flags, + req::{Aad, BinReqValues, Keyslot, ReqEncrCtx}, + request::{Confidential, MagicValue, Request, RequestVersion, SymKey, Zeroize}, + static_assert, + uv::UvFlags, + Error, Result, +}; +use openssl::pkey::{PKey, Public}; +use std::mem::size_of; +use zerocopy::{AsBytes, BigEndian, FromBytes, FromZeroes, U32}; + +#[cfg(doc)] +use crate::{ + request::SymKeyType, + uv::AttestationCmd, + verify::{CertVerifier, HkdVerifier}, +}; + +/// Retrieve Attestation Request Control Block +/// +/// An ARCB holds an Attestation Measurement key to attest a SE-guest. +/// The (architectural optional) nonce is always used and freshly generated for a new +/// [`AttestationRequest`]. +/// +/// Layout: +/// ```none +/// _______________________________________________________________ +/// | generic header (48) +/// | --------------------------------------------------- | +/// | Plaintext Attestation flags (8) | +/// | Measurement Algorithm Identifier (4) | +/// | Reserved(4) | +/// | Customer Public Key (160) generated for each request | +/// | N Keyslots(80 each) | +/// | --------------------------------------------------- | +/// | Measurement key (64) | Encrypted +/// | Optional Nonce (0 or 16) | Encrypted +/// | --------------------------------------------------- | +/// | AES GCM Tag (16) | +/// |_____________________________________________________________| +/// ``` +/// +/// # Example +/// Create an Attestation request with default flags (= use a nonce) +/// +/// ```rust,no_run +/// use pv::attest::{AttestationFlags, AttestationMeasAlg, AttestationRequest, AttestationVersion}; +/// use pv::request::{SymKeyType, Request, ReqEncrCtx}; +/// # fn main() -> pv::Result<()> { +/// let att_version = AttestationVersion::One; +/// let meas_alg = AttestationMeasAlg::HmacSha512; +/// let mut arcb = AttestationRequest::new(att_version, meas_alg, AttestationFlags::default())?; +/// // read-in hostkey document(s). Not verified for brevity. +/// let hkd = pv::misc::read_certs(&std::fs::read("host-key-document.crt")?)?; +/// // IBM issued HKD certificates typically have one X509 +/// let hkd = hkd.first().unwrap().public_key()?; +/// arcb.add_hostkey(hkd); +/// // you can add multiple hostkeys +/// // arcb.add_hostkey(another_hkd); +/// // encrypt it +/// let ctx = ReqEncrCtx::random(SymKeyType::Aes256)?; +/// let arcb = arcb.encrypt(&ctx)?; +/// # Ok(()) +/// # } +/// ``` +/// # See Also +/// +/// * [`AttestationFlags`] +/// * [`AttestationMeasAlg`] +/// * [`AttestationVersion`] +/// * [`SymKeyType`] +/// * [`Request`] +/// * [`ReqEncrCtx`] +/// * [`AttestationCmd`] +/// * [`HkdVerifier`], [`CertVerifier`] +#[derive(Debug)] +pub struct AttestationRequest { + version: AttestationVersion, + aad: AttestationAuthenticated, + keyslots: Vec, + conf: Confidential, +} + +impl AttestationRequest { + /// Create a new retrieve attestation measurement request + pub fn new( + version: AttestationVersion, + mai: AttestationMeasAlg, + mut flags: AttestationFlags, + ) -> Result { + // This implementation enforces using a nonce + flags.set_nonce(); + Ok(Self { + version, + aad: AttestationAuthenticated::new(flags, mai), + keyslots: vec![], + conf: ReqConfData::random()?, + }) + } + + /// Returns a reference to the flags of this [`AttestationRequest`]. + pub fn flags(&self) -> &AttestationFlags { + &self.aad.flags + } + + /// Returns a copy of the confidential data of this [`AttestationRequest`]. + /// + /// Gives a copy of the confidential data of this request for further + /// processing. This data should be never exposed in cleartext to anyone but + /// the creator and the verifier of this request. + pub fn confidential_data(&self) -> AttestationConfidential { + let conf = self.conf.value(); + AttestationConfidential::new(conf.meas_key.to_vec(), conf.nonce.into()) + } + + fn aad(&self, ctx: &ReqEncrCtx) -> Result> { + let cust_pub_key = ctx.key_coords()?; + let mut aad: Vec = Vec::with_capacity(self.keyslots.len() + 2); + aad.push(Aad::Plain(self.aad.as_bytes())); + aad.push(Aad::Plain(cust_pub_key.as_ref())); + self.keyslots.iter().for_each(|k| aad.push(Aad::Ks(k))); + ctx.build_aad( + self.version.into(), + &aad, + size_of::(), + AttestationMagic::MAGIC, + ) + } + + /// Decrypts the request and extracts the authenticated and confidential data + /// + /// Deconstructs the `arcb` and decrypts it using `arpk` + /// + /// # Error + /// + /// Returns an error if the request is malformed or the decryption failed + pub fn decrypt_bin( + arcb: &[u8], + arpk: &SymKey, + ) -> Result<(AttestationAuthenticated, AttestationConfidential)> { + if !AttestationMagic::starts_with_magic(arcb) { + return Err(Error::NoArcb); + } + + let values = BinReqValues::get(arcb)?; + + match values.version().try_into()? { + AttestationVersion::One => (), + }; + let auth: &AttestationAuthenticated = values.req_dep_aad().ok_or(Error::BinRequestSmall)?; + + let mai = auth.mai.try_into()?; + let keysize = match mai { + v @ AttestationMeasAlg::HmacSha512 => v.exp_size(), + } as usize; + + if keysize > values.sea() as usize { + return Err(Error::BinArcbSeaSmall(values.sea())); + } + + let decr = values.decrypt(arpk)?; + + // size sanitized by fence before + let meas_key = &decr.value()[..keysize]; + let nonce = if decr.value().len() == size_of::() { + Some( + (&decr.value()[keysize..decr.value().len()]) + .try_into() + .unwrap(), + ) + } else { + None + }; + let conf = AttestationConfidential::new(meas_key.to_vec(), nonce); + + Ok((auth.to_owned(), conf)) + } +} + +/// Confidential Data of an attestation request +/// +/// contains a measurement key and an optional nonce +#[derive(Debug)] +pub struct AttestationConfidential { + measurement_key: Confidential>, + nonce: Option>, +} + +impl AttestationConfidential { + /// Returns a reference to the measurement key of this [`AttestationConfidential`]. + pub fn measurement_key(&self) -> &[u8] { + self.measurement_key.value() + } + + /// Returns a reference to the nonce of this [`AttestationConfidential`]. + pub fn nonce(&self) -> &Option> { + &self.nonce + } + + fn new(measurement_key: Vec, nonce: Option) -> Self { + Self { + measurement_key: measurement_key.into(), + nonce: nonce.map(Confidential::new), + } + } +} + +impl Request for AttestationRequest { + fn encrypt(&self, ctx: &ReqEncrCtx) -> Result> { + let conf = self.conf.value().as_bytes(); + let aad = self.aad(ctx)?; + ctx.encrypt_aead(&aad, conf).map(|res| res.data()) + } + + fn add_hostkey(&mut self, hostkey: PKey) { + self.keyslots.push(Keyslot::new(hostkey)) + } +} + +/// Versions for [`AttestationRequest`] +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AttestationVersion { + /// Version 1 (= 0x0100) + One = 0x0100, +} + +impl TryFrom for AttestationVersion { + type Error = Error; + + fn try_from(value: u32) -> Result { + if value == Self::One as u32 { + Ok(Self::One) + } else { + Err(Error::BinArcbInvVersion(value)) + } + } +} + +impl From for RequestVersion { + fn from(val: AttestationVersion) -> Self { + val as RequestVersion + } +} + +/// Authenticated additional Data of an [`AttestationRequest`] +#[repr(C)] +#[derive(Debug, AsBytes, FromZeroes, FromBytes, Clone, Copy)] +pub struct AttestationAuthenticated { + flags: AttestationFlags, + mai: U32, + res: u32, +} +assert_size!(AttestationAuthenticated, 0x10); + +impl AttestationAuthenticated { + fn new(flags: AttestationFlags, mai: AttestationMeasAlg) -> Self { + Self { + flags, + mai: mai.into(), + res: 0, + } + } + + /// Returns a reference to the flags of this [`AttestationAuthenticated`]. + pub fn flags(&self) -> &AttestationFlags { + &self.flags + } + + /// Returns the [`AttestationMeasAlg`] of this [`AttestationAuthenticated`]. + /// + /// # Panics + /// + /// Panics if the library failed to set up the MAI correctly. + pub fn mai(&self) -> AttestationMeasAlg { + AttestationMeasAlg::try_from(self.mai).expect("ReqAuthData invariant hurt. Invalid MAI") + } +} + +/// Attestation flags +#[repr(C)] +#[derive(Default, Debug, AsBytes, FromZeroes, FromBytes, Clone, Copy)] +pub struct AttestationFlags(UvFlags); +static_assert!(AttestationFlags::FLAG_TO_ADD_SIZE.len() < 64); + +impl AttestationFlags { + /// Maps the flag to the (maximum) required size for the additional data + pub(crate) const FLAG_TO_ADD_SIZE: [u32; 4] = [0, 0, ATT_ADD_HASH_SIZE, ATT_ADD_HASH_SIZE]; + + /// Returns the maximum size this flag requires for additional data + pub fn expected_additional_size(&self) -> u32 { + Self::FLAG_TO_ADD_SIZE + .iter() + .enumerate() + .fold(0, |size, (b, s)| size + self.0.is_set(b as u8) as u32 * s) + } + + /// Flag 1 - use a nonce + /// + /// This attestation implementation forces the use of a nonce, so this will always be on and + /// the function is non-public + fn set_nonce(&mut self) { + self.0.set_bit(1); + } + + /// Flag 2 - request the image public host-key hash + /// + /// Asks the Ultravisor to provide the host-key hash that unpacked the SE-image to be added in + /// additional data. Requires 32 bytes. + pub fn set_image_phkh(&mut self) { + self.0.set_bit(2); + } + + /// Check weather the image public host key hash flag is on + pub fn image_phkh(&self) -> bool { + self.0.is_set(2) + } + + /// Flag 3 - request the attestation public host-key hash + /// + /// Asks the Ultravisor to provide the host-key hash that unpacked the attestation request to + /// be added in additional data. Requires 32 bytes. + pub fn set_attest_phkh(&mut self) { + self.0.set_bit(3); + } + + /// Check weather the attestation public host key hash flag is on + pub fn attest_phkh(&self) -> bool { + self.0.is_set(3) + } +} + +#[repr(C)] +#[derive(Debug, AsBytes)] +struct ReqConfData { + meas_key: [u8; 64], + nonce: AttNonce, +} +assert_size!(ReqConfData, 80); + +impl ReqConfData { + fn random() -> Result> { + Ok(Confidential::new(Self { + meas_key: random_array()?, + nonce: random_array()?, + })) + } +} + +impl Zeroize for ReqConfData { + fn zeroize(&mut self) { + self.meas_key.zeroize(); + self.nonce.zeroize(); + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::{get_test_asset, request::SymKey, test_utils::get_test_keys}; + + const ARPK: [u8; 32] = [0x17; 32]; + const NONCE: [u8; 16] = [0xab; 16]; + const MEAS: [u8; 64] = [0x77; 64]; + + fn mk_arcb() -> Vec { + let (cust_key, host_key) = get_test_keys(); + let ctx = ReqEncrCtx::new_aes_256( + Some([0x55; 12]), + Some(cust_key), + Some(SymKey::Aes256(ARPK.into())), + ) + .unwrap(); + + let mut flags = AttestationFlags::default(); + flags.set_image_phkh(); + flags.set_attest_phkh(); + + let mut arcb = AttestationRequest::new( + AttestationVersion::One, + AttestationMeasAlg::HmacSha512, + flags, + ) + .unwrap(); + + // manually set confidential data (API does not allow this) + arcb.conf.value_mut().nonce = NONCE; + arcb.conf.value_mut().meas_key = MEAS; + + arcb.add_hostkey(host_key); + arcb.encrypt(&ctx).unwrap() + } + + #[test] + fn arcb() { + let request = mk_arcb(); + let exp = get_test_asset!("exp/arcb.bin"); + + assert_eq!(request, exp); + } + + #[test] + fn decrypt_bin() { + let request = mk_arcb(); + let arpk = SymKey::Aes256(ARPK.into()); + let (_, conf) = AttestationRequest::decrypt_bin(&request, &arpk).unwrap(); + assert_eq!(conf.measurement_key(), &MEAS); + assert_eq!(conf.nonce().as_ref().unwrap().value(), &NONCE); + } + + #[test] + fn decrypt_bin_fail_magic() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb(); + + // tamper magic + tamp_arcb[0] = 17; + let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk); + assert!(matches!(ret, Err(Error::NoArcb))); + } + + #[test] + fn decrypt_bin_fail_mai() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb(); + + // tamper MAI + tamp_arcb[0x3b] = 17; + let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk); + println!("{ret:?}"); + assert!(matches!( + ret, + Err(Error::PvCore(pv_core::Error::BinArcbInvAlgorithm(17))) + )); + } + + #[test] + fn decrypt_bin_fail_aad() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb(); + + // tamper AAD + tamp_arcb[0x3c] = 17; + let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk); + assert!(matches!(ret, Err(Error::GcmTagMismatch))); + } +} diff --git a/rust/pv/src/uvattest/attest.rs b/rust/pv/src/uvattest/attest.rs new file mode 100644 index 00000000..40ead782 --- /dev/null +++ b/rust/pv/src/uvattest/attest.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use super::AttNonce; +use crate::{ + attest::AttestationMeasAlg, brcb::BootHdrTags, crypto::calculate_hmac, request::Confidential, + uv::ConfigUid, Result, +}; +use openssl::{ + hash::MessageDigest, + pkey::{PKeyRef, Private}, +}; +use std::mem::size_of; +use zerocopy::{AsBytes, BigEndian, U16, U32}; + +#[cfg(doc)] +use crate::attest::AttestationRequest; + +/// Holds the data to be measured. +/// +/// The Attestation measurement is an authentication code of the following data: +/// +/// ```none +/// |-------------------------------| +/// | From SE-header: | +/// | Page List Digest (64) | +/// | Address List Digest (64) | +/// | Tweak List Digest (64) | +/// | SE Header Tag (16) | +/// | Configuration Unique Id (16) | +/// | user-data length (2) | +/// | zeros (2) | +/// | additional data length (4) | +/// | user-data (0-256) | +/// | optional nonce (0 or 16) | +/// | additional data (0+) | +/// |-------------------------------| +/// ``` +#[derive(Debug)] +pub struct AttestationItems(Confidential>); + +// tags: BootHdrTags, +// cuid: ConfigUid, +// user_data_len: U16, +// res: u16, +// additional_len: U32, +// user_data: Vec, +// nonce: Option<[u8; 16]>, +// additional: Vec, +impl AttestationItems { + /// Create a new attestation item struct. + /// + /// * `tags`: The tags from the SE header + /// * `cuid`: The Configuration Unique Id from the SE guest for which the Measurement was + /// calculated + /// * `user`: up to 256 bytes of arbitrary data generated on the SE-guest before measuring + /// * `nonce`: technically optional nonce, but [`AttestationRequest`] enforces it + /// * `additional`: additional data generated by the Firmware depending on the Attestation flags + /// + /// If size values of `user` or `additional` are longer than 16/32 bit they are silently + /// truncated. `user-data` is limited to 256 bytes architecture wise, and additional data is + /// limited to 8 pages by the uvdevice. Larger sizes will produce invalid measurements + /// anyhow. + pub fn new( + tags: &BootHdrTags, + cuid: &ConfigUid, + user: Option<&[u8]>, + nonce: Option<&AttNonce>, + additional: Option<&[u8]>, + ) -> Self { + // expectations are ensured by ExchangeCtx invariants + let user = user.unwrap_or(&[]); + let user_data_len: U16 = (user.len() as u16).into(); + + let additional = additional.unwrap_or(&[]); + let additional_len: U32 = (additional.len() as u32).into(); + + let size = size_of::() // PLD ALD TLD TAG + + size_of::() + + size_of::() // user_len + + size_of::() // reserved + + size_of::() // additional_len + + user.len() + + match nonce { + Some(_) => size_of::(), + None => 0, + } + + additional.len(); + + let mut items = Vec::with_capacity(size); + items.extend_from_slice(tags.as_bytes()); + items.extend_from_slice(cuid.as_bytes()); + items.extend_from_slice(user_data_len.as_bytes()); + items.extend_from_slice(&[0, 0]); + items.extend_from_slice(additional_len.as_bytes()); + items.extend_from_slice(user); + if let Some(nonce) = nonce { + items.extend_from_slice(nonce); + } + items.extend_from_slice(additional); + assert!(items.len() == size); + Self(items.into()) + } +} + +/// Holds an attestation measurement +#[derive(Debug)] +#[allow(clippy::len_without_is_empty)] +pub struct AttestationMeasurement(Vec); +impl AttestationMeasurement { + /// Calculate an attestation measurement + pub fn calculate( + items: AttestationItems, + mai: AttestationMeasAlg, + meas_key: &PKeyRef, + ) -> Result { + match mai { + AttestationMeasAlg::HmacSha512 => { + calculate_hmac(meas_key, MessageDigest::sha512(), items.0.value()).map(Self) + } + } + } + + /// Returns the length of the [`AttestationMeasurement`]. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Securely compares the calculated measurement with a given one + /// + /// Exists early when sizes do not match + pub fn eq_secure(&self, other: &[u8]) -> bool { + if self.len() != other.len() { + return false; + } + openssl::memcmp::eq(&self.0, other) + } +} + +impl AsRef<[u8]> for AttestationMeasurement { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl From> for AttestationMeasurement { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod test { + use super::*; + use openssl::pkey::PKey; + + const M_KEY: [u8; 64] = [0x41; 64]; + const BOOT_HDR_TAGS: BootHdrTags = BootHdrTags::new([1; 64], [2; 64], [3; 64], [4; 16]); + const CUID: [u8; 16] = [5; 16]; + const USER: [u8; 256] = [7; 256]; + const NONCE: [u8; 16] = [8; 16]; + const ADDITIONAL: [u8; 128] = [9; 128]; + + // just for better output in case of a test failure + impl PartialEq<[u8]> for AttestationMeasurement { + fn eq(&self, other: &[u8]) -> bool { + self.eq_secure(other) + } + } + + #[test] + fn measurement_all() { + const EXP_HMAC: [u8; 64] = [ + 0x88, 0x79, 0x4c, 0x62, 0xcc, 0xe7, 0xbc, 0xf2, 0x62, 0x16, 0xde, 0xb3, 0xf4, 0x8f, + 0x13, 0xfe, 0xa6, 0x37, 0x4b, 0x6d, 0x7e, 0x35, 0xbc, 0xc5, 0xc2, 0xce, 0x68, 0x12, + 0x1d, 0xb6, 0xf4, 0x5d, 0xfc, 0x8c, 0x17, 0x18, 0x56, 0x46, 0x35, 0x49, 0x40, 0x8b, + 0xf8, 0xe7, 0xd1, 0xac, 0xa1, 0x1e, 0xfa, 0xd0, 0xa8, 0x78, 0xaf, 0x97, 0xdc, 0x9e, + 0x21, 0xa1, 0xfc, 0x2a, 0x32, 0xf3, 0xa6, 0x75, + ]; + let items = AttestationItems::new( + &BOOT_HDR_TAGS, + &CUID, + Some(&USER), + Some(&NONCE), + Some(&ADDITIONAL), + ); + let key = PKey::hmac(&M_KEY).unwrap(); + let meas = + AttestationMeasurement::calculate(items, AttestationMeasAlg::HmacSha512, &key).unwrap(); + assert_eq!(meas, EXP_HMAC[..]); + assert!(meas.eq_secure(&EXP_HMAC[..])); + } + + #[test] + fn measurement_user_add() { + const EXP_HMAC: [u8; 64] = [ + 0xfb, 0xd4, 0xf7, 0x38, 0xa3, 0x90, 0xed, 0xd9, 0x47, 0xcd, 0x4f, 0x11, 0xaf, 0x3a, + 0x2f, 0x3b, 0xab, 0x2f, 0xdf, 0x8b, 0xf8, 0x9b, 0xf8, 0x1b, 0xeb, 0x49, 0x51, 0x17, + 0xf4, 0x38, 0x2c, 0xf4, 0x2f, 0x07, 0x30, 0xc8, 0xc7, 0xd9, 0xe3, 0xca, 0x27, 0xfb, + 0x25, 0xad, 0xfc, 0xeb, 0x21, 0x22, 0x4f, 0x57, 0xfd, 0xb3, 0x98, 0xdc, 0xf4, 0x1a, + 0x83, 0xc1, 0x46, 0xe6, 0xa2, 0x3d, 0xb7, 0x60, + ]; + let items = + AttestationItems::new(&BOOT_HDR_TAGS, &CUID, Some(&USER), None, Some(&ADDITIONAL)); + let key = PKey::hmac(&M_KEY).unwrap(); + let meas = + AttestationMeasurement::calculate(items, AttestationMeasAlg::HmacSha512, &key).unwrap(); + assert_eq!(meas, EXP_HMAC[..]); + assert!(meas.eq_secure(&EXP_HMAC[..])); + } + + #[test] + fn measurement_add() { + const EXP_HMAC: [u8; 64] = [ + 0x63, 0x67, 0x1f, 0xbf, 0x29, 0x50, 0x36, 0xeb, 0x10, 0x23, 0xea, 0x71, 0xf7, 0x18, + 0x2e, 0x7d, 0x63, 0x43, 0xdc, 0x7b, 0x2d, 0xa5, 0x84, 0xe8, 0x24, 0xd0, 0xa7, 0xd1, + 0x98, 0xab, 0x9c, 0xde, 0xd7, 0x56, 0xc9, 0x3b, 0x39, 0x05, 0x0f, 0xfb, 0x76, 0x45, + 0x55, 0xb0, 0x1f, 0x88, 0xcb, 0x82, 0x01, 0x7a, 0x6a, 0x15, 0xc7, 0xe0, 0xba, 0xfc, + 0x60, 0x05, 0xf1, 0xe4, 0xf7, 0x8a, 0xa1, 0x24, + ]; + let items = AttestationItems::new(&BOOT_HDR_TAGS, &CUID, None, None, Some(&ADDITIONAL)); + let key = PKey::hmac(&M_KEY).unwrap(); + let meas = + AttestationMeasurement::calculate(items, AttestationMeasAlg::HmacSha512, &key).unwrap(); + assert_eq!(meas, EXP_HMAC[..]); + assert!(meas.eq_secure(&EXP_HMAC[..])); + } + + #[test] + fn measurement_minimal() { + const EXP_HMAC: [u8; 64] = [ + 0xc5, 0xc3, 0x4c, 0x93, 0x83, 0x5d, 0x1e, 0xc2, 0x3f, 0x5c, 0x2d, 0x77, 0x8d, 0xfa, + 0x20, 0x12, 0x9b, 0x11, 0xb3, 0x05, 0x60, 0x17, 0x42, 0xcb, 0x2f, 0x38, 0xe0, 0xed, + 0x98, 0x94, 0xdc, 0xdb, 0x73, 0xfc, 0x86, 0x95, 0xab, 0x6a, 0x8d, 0xba, 0xd0, 0x74, + 0x40, 0x73, 0xdd, 0xc8, 0x1a, 0x5e, 0xaa, 0xfa, 0x52, 0xe4, 0xa1, 0x5a, 0xf8, 0xde, + 0xb8, 0xd7, 0x61, 0x09, 0x19, 0x22, 0x84, 0x7f, + ]; + let items = AttestationItems::new(&BOOT_HDR_TAGS, &CUID, None, None, None); + let key = PKey::hmac(&M_KEY).unwrap(); + let meas = + AttestationMeasurement::calculate(items, AttestationMeasAlg::HmacSha512, &key).unwrap(); + assert_eq!(meas, EXP_HMAC[..]); + assert!(meas.eq_secure(&EXP_HMAC[..])); + } +} diff --git a/rust/pv/tests/assets/exp/arcb.bin b/rust/pv/tests/assets/exp/arcb.bin new file mode 100644 index 0000000000000000000000000000000000000000..162cdfb15f327fa78669d77bed04e83704e445f7 GIT binary patch literal 400 zcmZQzfB{AzgK zjawhZ(z}vA`8+l{Iy0WbH9Ku%?$xMCN7+99FKU zG5^BcRa;(Ku4j?YEG`N4-E;dQ_Ys@Q{%dYc*?Bpt**BY>zn|h(UvBuub^W$m6G|6c w6I&6$^4pkSy3EBVttg0VqT=;sUeBAqcLz>7|6#S=M8iFrE$=sZXU^FI0L^-xf&c&j literal 0 HcmV?d00001 diff --git a/rust/pv/tests/assets/exp/exchange.bin b/rust/pv/tests/assets/exp/exchange.bin new file mode 100644 index 0000000000000000000000000000000000000000..d249a29c4f2b022aaf36aa13715f534fa39c8ac0 GIT binary patch literal 448 zcmXRYODri#EiPeTU}ON|13&@>1b_?&C=LM93P3yoqK;t$kPVb)+#rYsgeU~UqX2IR Hh@b%g@^vF{ literal 0 HcmV?d00001 diff --git a/rust/pv_core/src/uvdevice/attest.rs b/rust/pv_core/src/uvdevice/attest.rs index 697a8bca..06933dfc 100644 --- a/rust/pv_core/src/uvdevice/attest.rs +++ b/rust/pv_core/src/uvdevice/attest.rs @@ -9,17 +9,17 @@ use zerocopy::{AsBytes, FromZeroes}; /// _Retrieve Attestation Measurement_ UVC /// /// The Attestation Request has two input and three outputs. -/// ARCB and User Data are inputs for the UV. -/// Measurement, Additional Data, and the Configuration Unique ID are outputs generated by UV. +/// ARCB and user-data are inputs for the UV. +/// Measurement, additional data, and the Configuration Unique ID are outputs generated by UV. /// /// The Attestation Request Control Block (ARCB) is a cryptographically verified -/// and secured request to UV and User Data is some plaintext data which is +/// and secured request to UV and user-Data is some plaintext data which is /// going to be included in the Attestation Measurement calculation. /// /// Measurement is a cryptographic measurement of the callers properties, -/// optional data configured by the ARCB and the user data. If specified by the -/// ARCB, UV will add some Additional Data to the measurement calculation. -/// This Additional Data is then returned as well. +/// optional data configured by the ARCB and the user-data. If specified by the +/// ARCB, UV will add some additional Data to the measurement calculation. +/// This additional data is then returned as well. /// /// If the Retrieve Attestation Measurement UV facility is not present, /// UV will return invalid command rc. diff --git a/rust/pv_core/src/uvdevice/ffi.rs b/rust/pv_core/src/uvdevice/ffi.rs index 8b78c906..2b0aeb13 100644 --- a/rust/pv_core/src/uvdevice/ffi.rs +++ b/rust/pv_core/src/uvdevice/ffi.rs @@ -70,8 +70,8 @@ pub const UVIO_ATT_UID_LEN: usize = 0x10; /// Request Attestation Measurement control block /// /// The Attestation Request has two input and two outputs. -/// ARCB and User Data are inputs for the UV. -/// Measurement and Additional Data are outputs generated by UV. +/// ARCB and user-data are inputs for the UV. +/// Measurement and additional-data are outputs generated by UV. /// /// The Attestation Request Control Block (ARCB) is a cryptographically verified /// and secured request to UV and user-data is some plaintext data which is