From 9dca2d318133cd59b42d93d15d63c3262bb689a0 Mon Sep 17 00:00:00 2001 From: Timo Keller Date: Tue, 21 Jul 2026 17:05:00 +0200 Subject: [PATCH] pvattest: Use hybrid keys Allow the creation of Attestation requests using hybrid (=quantum safe) keys. This results in using the headers in version 2 (0x200). Co-developed-by: Marc Hartmayer Signed-off-by: Marc Hartmayer Signed-off-by: Timo Keller Reviewed-by: Steffen Eiden Signed-off-by: Steffen Eiden --- rust/pv/src/uvattest/arcb.rs | 221 +++++++++++++++++++++++++++++++- rust/pv/src/uvattest/attest.rs | 2 + rust/pvattest/src/cli.rs | 32 ++++- rust/pvattest/src/cmd.rs | 2 +- rust/pvattest/src/cmd/create.rs | 48 ++++++- rust/pvattest/src/exchange.rs | 61 ++++++++- 6 files changed, 348 insertions(+), 18 deletions(-) diff --git a/rust/pv/src/uvattest/arcb.rs b/rust/pv/src/uvattest/arcb.rs index ede31044..ea1dd086 100644 --- a/rust/pv/src/uvattest/arcb.rs +++ b/rust/pv/src/uvattest/arcb.rs @@ -4,6 +4,7 @@ use std::mem::size_of; +use pv_core::static_assert; use zerocopy::{BigEndian, FromBytes, Immutable, IntoBytes, KnownLayout, U32}; use super::additional::{FW_STATE_SIZE, PHKH_SIZE, SECRET_STORE_HASH_SIZE}; @@ -14,7 +15,7 @@ use crate::misc::Flags; use crate::req::{Aad, BinReqValues, HostKey, Keyslot, ReqEncrCtx}; use crate::request::{Confidential, MagicValue, Request, RequestVersion, SymKey, Zeroize}; use crate::uv::UvFlags; -use crate::{assert_size, static_assert, Error, Result}; +use crate::{assert_size, Error, Result}; #[cfg(doc)] use crate::{ request::SymKeyType, @@ -62,7 +63,7 @@ use crate::{ /// let hkd = hkd.first().unwrap().public_key()?; /// arcb.add_hostkey(HostKey::V1(hkd)); /// // you can add multiple hostkeys -/// // arcb.add_hostkey(another_hkd); +/// // arcb.add_hostkey(HostKey::V1(another_hkd)); /// // encrypt it /// let ctx = ReqEncrCtx::random(SymKeyType::Aes256Gcm)?; /// let arcb = arcb.encrypt(&ctx)?; @@ -106,7 +107,13 @@ impl AttestationRequest { /// Returns a reference to the flags of this [`AttestationRequest`]. pub fn flags(&self) -> &AttestationFlags { - &self.aad.flags + self.aad.flags() + } + + /// Returns the request version, derived from the type of added host-keys. + /// Returns [`AttestationVersion::One`] if no host-keys have been added yet. + pub fn version(&self) -> AttestationVersion { + self.version } /// Returns a copy of the confidential data of this [`AttestationRequest`]. @@ -142,6 +149,7 @@ impl AttestationRequest { let values = BinReqValues::get(arcb)?; match values.version().try_into()? { AttestationVersion::One => (), + AttestationVersion::Two => (), }; Ok(values) @@ -246,6 +254,8 @@ impl Request for AttestationRequest { pub enum AttestationVersion { /// Version 1 (= 0x0100) One = 0x0100, + /// Version 2 (= 0x0200) + Two = 0x0200, } impl TryFrom for AttestationVersion { @@ -254,6 +264,8 @@ impl TryFrom for AttestationVersion { fn try_from(value: u32) -> Result { if value == Self::One as u32 { Ok(Self::One) + } else if value == Self::Two as u32 { + Ok(Self::Two) } else { Err(Error::BinArcbInvVersion(value)) } @@ -412,8 +424,8 @@ impl Zeroize for ReqConfData { mod test { use super::*; use crate::get_test_asset; - use crate::request::SymKey; - use crate::test_utils::get_test_keys; + use crate::request::{HybridPKey, SymKey}; + use crate::test_utils::{get_test_keys, get_test_keys_hybrid}; const ARPK: [u8; 32] = [0x17; 32]; const NONCE: [u8; 16] = [0xab; 16]; @@ -421,6 +433,8 @@ mod test { fn mk_arcb() -> Vec { let (cust_key, host_key) = get_test_keys(); + let host_key = HostKey::V1(host_key); + let ctx = ReqEncrCtx::new_aes_256( Some([0x55; 12]), Some(cust_key), @@ -443,7 +457,37 @@ mod test { arcb.conf.value_mut().nonce = NONCE; arcb.conf.value_mut().meas_key = MEAS; - arcb.add_hostkey(HostKey::V1(host_key)); + arcb.add_hostkey(host_key); + arcb.encrypt(&ctx).unwrap() + } + + fn mk_arcb_v2() -> Vec { + let (cust_key, host_key1, host_key2) = get_test_keys_hybrid(); + let host_key = HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap()); + + 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::Two, + 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() } @@ -455,6 +499,32 @@ mod test { assert_eq!(request, exp); } + #[test] + fn arcb_v2() { + let request = mk_arcb_v2(); + // Expected bytes for a V2 ARCB: rqvn = 0x0200 (bytes 8-11), rest deterministic. + // The first 288 bytes cover header + customer-public-key + one V2 keyslot header. + let exp: [u8; 288] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 7, 208, 85, 85, 85, 85, 85, 85, 85, 85, 85, + 85, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 80, 112, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 199, + 93, 52, 249, 22, 82, 219, 69, 123, 11, 32, 156, 70, 164, 145, 164, 78, 226, 177, 110, + 35, 194, 216, 218, 241, 22, 103, 138, 98, 242, 76, 227, 50, 197, 153, 95, 8, 69, 107, + 102, 177, 109, 213, 90, 146, 197, 7, 241, 227, 26, 247, 140, 100, 168, 46, 122, 84, 27, + 21, 19, 80, 21, 242, 2, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 64, 128, 88, + 167, 241, 165, 195, 80, 151, 83, 58, 2, 169, 56, 121, 231, 222, 103, 186, 40, 11, 206, + 131, 101, 236, 148, 178, 185, 8, 245, 137, 195, 169, 152, 216, 190, 30, 99, 7, 215, 74, + 224, 26, 220, 70, 130, 95, 246, 187, 111, 160, 92, 17, 71, 207, 226, 204, 244, 162, 79, + 61, 131, 61, 218, 112, 255, 94, 191, 53, 220, 196, 47, 37, 93, 227, 234, 101, 1, 174, + 171, 68, 42, 136, 92, 238, 72, 6, 17, 77, 231, 225, 174, 22, 222, 188, 212, 15, 248, + 145, 72, 126, 139, 17, 233, 225, 156, 46, 233, 151, 54, 2, 175, 88, 215, 254, 243, 222, + 37, 81, 50, 110, 18, 76, 252, 12, 210, 146, 66, 23, + ]; + + // only compare non-randomized part + assert_eq!(request[..288], exp[..288]); + } + #[test] fn auth_bin() { let request = mk_arcb(); @@ -464,6 +534,15 @@ mod test { assert_eq!(exp, auth_bin.as_bytes()); } + #[test] + fn auth_bin_v2() { + let request = mk_arcb_v2(); + let auth_bin = AttestationRequest::auth_bin(&request).unwrap(); + let exp = &request[0x30..0x40]; + + assert_eq!(exp, auth_bin.as_bytes()); + } + #[test] fn decrypt_bin() { let request = mk_arcb(); @@ -473,6 +552,99 @@ mod test { assert_eq!(conf.nonce().as_ref().unwrap().value(), &NONCE); } + #[test] + fn decrypt_bin_v2() { + let request = mk_arcb_v2(); + 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 arcb_v1_version() { + // Without any host-keys, version defaults to One + let arcb = AttestationRequest::new( + AttestationVersion::One, + AttestationMeasAlg::HmacSha512, + AttestationFlags::default(), + ) + .unwrap(); + + assert_eq!(arcb.version(), AttestationVersion::One); + } + + #[test] + fn arcb_v2_version() { + // After adding a V2 host-key, version is Two + let mut arcb = AttestationRequest::new( + AttestationVersion::Two, + AttestationMeasAlg::HmacSha512, + AttestationFlags::default(), + ) + .unwrap(); + let (_, host_key1, host_key2) = get_test_keys_hybrid(); + arcb.add_hostkey(HostKey::V2(HybridPKey::new(host_key1, host_key2).unwrap())); + + assert_eq!(arcb.version(), AttestationVersion::Two); + } + + #[test] + fn attestation_version_try_from() { + // Test version conversion from u32 + assert_eq!( + AttestationVersion::try_from(0x0100).unwrap(), + AttestationVersion::One + ); + assert_eq!( + AttestationVersion::try_from(0x0200).unwrap(), + AttestationVersion::Two + ); + + // Invalid version should error + assert!(AttestationVersion::try_from(0x0300).is_err()); + } + + #[test] + fn attestation_flags_expected_size() { + // Test expected additional data size calculation for V1 + let mut flags = AttestationFlags::default(); + + // Image PHKH flag - should be 32 bytes + flags.set_image_phkh(); + assert_eq!(flags.expected_additional_size(), 32); + + // Add attest PHKH flag - should be 64 bytes (32 + 32) + flags.set_attest_phkh(); + assert_eq!(flags.expected_additional_size(), 64); + + // Add secret store hash - should be 128 bytes (64 + 64) + flags.set_secret_store_hash(); + assert_eq!(flags.expected_additional_size(), 128); + + // Add firmware state - should be 448 bytes (128 + 320) + flags.set_firmware_state(); + assert_eq!(flags.expected_additional_size(), 448); + } + + #[test] + fn confidential_data_v2() { + // Test confidential data extraction (version-independent) + let arcb = AttestationRequest::new( + AttestationVersion::Two, + AttestationMeasAlg::HmacSha512, + AttestationFlags::default(), + ) + .unwrap(); + + let conf = arcb.confidential_data(); + + // Should have measurement key and nonce + assert_eq!(conf.measurement_key().len(), 64); + assert!(conf.nonce().is_some()); + assert_eq!(conf.nonce().as_ref().unwrap().value().len(), 16); + } + #[test] fn decrypt_bin_fail_magic() { let arpk = SymKey::Aes256(ARPK.into()); @@ -484,6 +656,17 @@ mod test { assert!(matches!(ret, Err(Error::NoArcb))); } + #[test] + fn decrypt_bin_fail_magic_v2() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb_v2(); + + // 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()); @@ -499,6 +682,21 @@ mod test { )); } + #[test] + fn decrypt_bin_fail_mai_v2() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb_v2(); + + // 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()); @@ -509,4 +707,15 @@ mod test { let ret = AttestationRequest::decrypt_bin(&tamp_arcb, &arpk); assert!(matches!(ret, Err(Error::GcmTagMismatch))); } + + #[test] + fn decrypt_bin_fail_aad_v2() { + let arpk = SymKey::Aes256(ARPK.into()); + let mut tamp_arcb = mk_arcb_v2(); + + // 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 index d9443016..87b05275 100644 --- a/rust/pv/src/uvattest/attest.rs +++ b/rust/pv/src/uvattest/attest.rs @@ -6,6 +6,7 @@ use std::mem::size_of; use openssl::hash::MessageDigest; use openssl::pkey::{PKeyRef, Private}; +use pv_core::misc::write_file; use zerocopy::{BigEndian, IntoBytes, U16, U32}; use super::AttNonce; @@ -101,6 +102,7 @@ impl AttestationItems { } items.extend_from_slice(additional); assert!(items.len() == size); + write_file("additional_data_parsed", &items, "blah").unwrap(); Self(items.into()) } } diff --git a/rust/pvattest/src/cli.rs b/rust/pvattest/src/cli.rs index c487dfc6..e3a84de5 100644 --- a/rust/pvattest/src/cli.rs +++ b/rust/pvattest/src/cli.rs @@ -5,7 +5,10 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}; -use utils::{CertificateOptions, DeprecatedVerbosityOptions}; +use utils::{ + AutoOrExplicit, AutoOrExplicitParser, CertificateOptions, DeprecatedVerbosityOptions, + HkdVersion, ValueEnumDisplay, ValueEnumFromStr, +}; /// create, perform, and verify attestation measurements /// @@ -67,6 +70,29 @@ pub enum Command { Version, } +/// Secure Execution attestation version for CLI +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, ValueEnumDisplay, ValueEnumFromStr)] +pub enum AttVersion { + #[value(name = "1")] + /// Version 1 - uses traditional cryptographic keys + V1, + #[value(name = "2")] + /// Version 2 - uses hybrid (post-quantum) cryptographic keys + V2, +} + +pub type AttVersionSelection = AutoOrExplicit; +pub type AttVersionSelectionParser = AutoOrExplicitParser; + +impl From for HkdVersion { + fn from(val: AttVersion) -> Self { + match val { + AttVersion::V1 => Self::Classical, + AttVersion::V2 => Self::Hybrid, + } + } +} + #[derive(Args, Debug)] pub struct CreateAttOpt { #[command(flatten)] @@ -94,6 +120,10 @@ pub struct CreateAttOpt { value_delimiter = ',' )] pub add_data: Vec, + + /// Specify the Attestation Request version to use. + #[arg(long = "att-version", value_name = "VERSION", default_value_t = AttVersionSelection::Explicit(AttVersion::V1), value_parser = AttVersionSelectionParser::default())] + pub att_version: AttVersionSelection, } #[derive(Debug, ValueEnum, Clone, Copy)] diff --git a/rust/pvattest/src/cmd.rs b/rust/pvattest/src/cmd.rs index ab5221fc..24991408 100644 --- a/rust/pvattest/src/cmd.rs +++ b/rust/pvattest/src/cmd.rs @@ -12,7 +12,7 @@ pub use check::check; pub use create::create; pub use verify::verify; -pub const CMD_FN: &[&str] = &["+create", "+verify"]; +pub const CMD_FN: &[&str] = &["+create", "+verify", "+quantumsafe"]; // s390 branch #[cfg(target_arch = "s390x")] mod uv_cmd { diff --git a/rust/pvattest/src/cmd/create.rs b/rust/pvattest/src/cmd/create.rs index 9f89139f..ba2b0b8f 100644 --- a/rust/pvattest/src/cmd/create.rs +++ b/rust/pvattest/src/cmd/create.rs @@ -10,7 +10,7 @@ use pv::attest::{AttestationFlags, AttestationMeasAlg, AttestationRequest, Attes use pv::misc::{create_file, write_file}; use pv::request::{HostKey, ReqEncrCtx, Request, SymKey, SymKeyType}; -use crate::cli::{AttAddFlags, CreateAttOpt}; +use crate::cli::{AttAddFlags, AttVersion, AttVersionSelection, CreateAttOpt}; use crate::exchange::{ExchangeFormatRequest, ExchangeFormatVersion}; fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags { @@ -26,18 +26,54 @@ fn flags(cli_flags: &[AttAddFlags]) -> AttestationFlags { att_flags } +/// Auto-detect the attestation version based on the host keys. +/// +/// Returns Two if any host key is a hybrid key, otherwise returns V1. +fn auto_detect_version(host_keys: &[HostKey]) -> AttestationVersion { + let use_hybrid_keys = host_keys.iter().any(|k: &HostKey| k.is_hybrid()); + if use_hybrid_keys { + AttestationVersion::Two + } else { + AttestationVersion::One + } +} + +impl From for AttestationVersion { + fn from(value: AttVersion) -> Self { + match value { + AttVersion::V1 => Self::One, + AttVersion::V2 => Self::Two, + } + } +} + +/// Determine the attestation version to use. +/// +/// If an explicit version is provided via CLI, use that. +/// Otherwise, auto-detect based on the host key types. +fn determine_version( + cli_version: AttVersionSelection, + host_keys: &[HostKey], +) -> AttestationVersion { + match cli_version { + AttVersionSelection::Auto => auto_detect_version(host_keys), + AttVersionSelection::Explicit(att_version) => att_version.into(), + } +} + pub fn create(opt: &CreateAttOpt) -> Result { - let att_version = AttestationVersion::One; + let hkds = opt + .certificate_args + .get_verified_hkds_new("attestation request", opt.att_version.map(|v| v.into()))?; + + let att_version = determine_version(opt.att_version, &hkds); let meas_alg = AttestationMeasAlg::HmacSha512; let mut arcb = AttestationRequest::new(att_version, meas_alg, flags(&opt.add_data))?; debug!("Generated Attestation request"); // Add host-key documents - opt.certificate_args - .get_verified_hkds("attestation request")? - .into_iter() - .for_each(|k| arcb.add_hostkey(HostKey::V1(k))); + hkds.into_iter().for_each(|k| arcb.add_hostkey(k)); debug!("Added all host-keys"); let encr_ctx = diff --git a/rust/pvattest/src/exchange.rs b/rust/pvattest/src/exchange.rs index f1080372..294cc449 100644 --- a/rust/pvattest/src/exchange.rs +++ b/rust/pvattest/src/exchange.rs @@ -11,6 +11,8 @@ 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)] @@ -99,10 +101,18 @@ impl ExchangeFormatV1Hdr { let measurement_entry = Entry::from_exp(Some(measurement)); let exp_add = match additional { 0 => None, - size => Some(size), + size => { + if size > AttestationCmd::ADDITIONAL_MAX_SIZE { + bail!( + "Additional data size ({}) exceeds maximum allowed size ({})", + size, + AttestationCmd::ADDITIONAL_MAX_SIZE + ); + } + Some(size) + } }; - // TODO min and max size check? - let additional_entry = Entry::from_exp(exp_add); //, AttestationCmd::ADDITIONAL_MAX_SIZE, &mut offset); + let additional_entry = Entry::from_exp(exp_add); let user_entry = Entry::from_none(); let cuid_entry = Entry::from_none(); @@ -530,7 +540,6 @@ impl ExchangeFormatResponse { "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation request.", ))?; - // TODO remove unwrap let measurement = hdr.measurement.read(reader)?.data().ok_or(anyhow!( "{INV_EXCHANGE_FMT_ERROR_TEXT} Contains no attestation response (Measurement missing).", ))?; @@ -693,6 +702,50 @@ mod test { 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(