diff --git a/api/tests/Cargo.toml b/api/tests/Cargo.toml index 59a4401ef..75a1b0d8d 100644 --- a/api/tests/Cargo.toml +++ b/api/tests/Cargo.toml @@ -22,6 +22,7 @@ xshell.workspace = true azihsm_api.workspace = true azihsm_api_tests_macro.workspace = true azihsm_crypto = { features = ["testvectors"], workspace = true } +azihsm_ddi_mbor_test_helpers.workspace = true azihsm_ddi_tbor_types.workspace = true azihsm_resiliency_test_helpers.workspace = true hex.workspace = true diff --git a/api/tests/src/sd/create_backup_tests.rs b/api/tests/src/sd/create_backup_tests.rs index 1876d4e24..82e83e737 100644 --- a/api/tests/src/sd/create_backup_tests.rs +++ b/api/tests/src/sd/create_backup_tests.rs @@ -33,7 +33,7 @@ fn sd_create_remote_backup_roundtrip() { let sata_key = CaKey::generate(); let (session, policy, pid_pub) = finalized_backing_session(&sata_key); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); let result = evidence .with_hsm_evidence(|receiver| session.sd_create_remote_backup(&masked, receiver, &policy)) @@ -72,7 +72,7 @@ fn sd_create_remote_backup_is_one_shot() { let sata_key = CaKey::generate(); let (session, policy, pid_pub) = finalized_backing_session(&sata_key); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); evidence diff --git a/api/tests/src/sd/create_peer_tests.rs b/api/tests/src/sd/create_peer_tests.rs index 16225cf62..c5193f9d8 100644 --- a/api/tests/src/sd/create_peer_tests.rs +++ b/api/tests/src/sd/create_peer_tests.rs @@ -27,7 +27,7 @@ fn sd_create_peer_backup_roundtrip() { let sata_key = CaKey::generate(); let (session, policy, pid_pub) = finalized_backing_session(&sata_key); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); // Create the security domain first to obtain the device-local backup @@ -60,7 +60,7 @@ fn sd_create_peer_backup_rerandomizes() { let sata_key = CaKey::generate(); let (session, policy, pid_pub) = finalized_backing_session(&sata_key); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); let created = evidence diff --git a/api/tests/src/sd/reseal_tests.rs b/api/tests/src/sd/reseal_tests.rs index c61b66b05..e35723f6c 100644 --- a/api/tests/src/sd/reseal_tests.rs +++ b/api/tests/src/sd/reseal_tests.rs @@ -48,9 +48,9 @@ fn sd_reseal_remote_backup_roundtrip() { // Receiver (unseals the source), sender (sealed the source), and // destination (the reseal target) SD sealing keys, each attested. - let (masked_rcvr, report_rcvr) = masked_key_and_report(&session); - let (masked_sndr, report_sndr) = masked_key_and_report(&session); - let (_masked_dst, report_dst) = masked_key_and_report(&session); + let (masked_rcvr, report_rcvr) = masked_key_and_report(&session, &policy); + let (masked_sndr, report_sndr) = masked_key_and_report(&session, &policy); + let (_masked_dst, report_dst) = masked_key_and_report(&session, &policy); let src_backup = create_source_backup( &session, @@ -94,9 +94,9 @@ fn sd_reseal_remote_backup_rerandomizes() { let sata_key = CaKey::generate(); let (session, policy, pid_pub) = finalized_backing_session(&sata_key); - let (masked_rcvr, report_rcvr) = masked_key_and_report(&session); - let (masked_sndr, report_sndr) = masked_key_and_report(&session); - let (_masked_dst, report_dst) = masked_key_and_report(&session); + let (masked_rcvr, report_rcvr) = masked_key_and_report(&session, &policy); + let (masked_sndr, report_sndr) = masked_key_and_report(&session, &policy); + let (_masked_dst, report_dst) = masked_key_and_report(&session, &policy); let src_backup = create_source_backup( &session, diff --git a/api/tests/src/sd/restore_local_tests.rs b/api/tests/src/sd/restore_local_tests.rs index afb2a7d98..8867ddb49 100644 --- a/api/tests/src/sd/restore_local_tests.rs +++ b/api/tests/src/sd/restore_local_tests.rs @@ -36,7 +36,7 @@ fn sd_restore_local_backup_roundtrip() { // Device 1: finalize + create the SD, capturing the device-local backups // and the local_mk backup needed to restore PartLocalMK after reboot. let (session1, policy, pid_pub, local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session1); + let (masked, report) = masked_key_and_report(&session1, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session1.sd_create_remote_backup(&masked, ev, &policy)) @@ -75,7 +75,7 @@ fn sd_restore_local_backup_is_one_shot() { let pota = CaKey::generate(); let (session, policy, pid_pub, _local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session.sd_create_remote_backup(&masked, ev, &policy)) diff --git a/api/tests/src/sd/restore_peer_tests.rs b/api/tests/src/sd/restore_peer_tests.rs index 52ef7510f..1e99da937 100644 --- a/api/tests/src/sd/restore_peer_tests.rs +++ b/api/tests/src/sd/restore_peer_tests.rs @@ -40,7 +40,7 @@ fn sd_restore_peer_backup_roundtrip() { // capturing the peer backup plus the sd_mk / local_mk backups device 2 // needs to restore. let (session1, policy, pid_pub, local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session1); + let (masked, report) = masked_key_and_report(&session1, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session1.sd_create_remote_backup(&masked, ev, &policy)) @@ -93,7 +93,7 @@ fn sd_restore_peer_backup_is_one_shot() { let pota = CaKey::generate(); let (session, policy, pid_pub, _local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session.sd_create_remote_backup(&masked, ev, &policy)) @@ -132,7 +132,7 @@ fn sd_restore_peer_backup_rejects_without_peer_cloning() { let (session, policy, pid_pub, _local_mk) = provision_backing_ex(&sata, &pota, None, None, false); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let pok_peer_backup = [0u8; POK_REMOTE_BACKUP_LEN]; diff --git a/api/tests/src/sd/restore_tests.rs b/api/tests/src/sd/restore_tests.rs index 4978d2232..2f8022e8b 100644 --- a/api/tests/src/sd/restore_tests.rs +++ b/api/tests/src/sd/restore_tests.rs @@ -36,7 +36,7 @@ fn sd_restore_remote_backup_roundtrip() { // Device 1: finalize + create, capturing the remote backup and the // local_mk backup needed to restore PartLocalMK after reboot. let (session1, policy, pid_pub, local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session1); + let (masked, report) = masked_key_and_report(&session1, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session1.sd_create_remote_backup(&masked, ev, &policy)) @@ -83,7 +83,7 @@ fn sd_restore_remote_backup_is_one_shot() { let pota = CaKey::generate(); let (session, policy, pid_pub, _local_mk) = provision_backing(&sata, &pota, None, None); - let (masked, report) = masked_key_and_report(&session); + let (masked, report) = masked_key_and_report(&session, &policy); let evidence = build_receiver_evidence(&pid_pub, &sata, &report); let created = evidence .with_hsm_evidence(|ev| session.sd_create_remote_backup(&masked, ev, &policy)) diff --git a/api/tests/src/utils/sd_provision.rs b/api/tests/src/utils/sd_provision.rs index e4e0ad49e..d54ecea72 100644 --- a/api/tests/src/utils/sd_provision.rs +++ b/api/tests/src/utils/sd_provision.rs @@ -15,9 +15,13 @@ use azihsm_api::*; use azihsm_crypto::EccCurve; use azihsm_crypto::EccKeyOp; use azihsm_crypto::EccPrivateKey; +#[cfg(not(feature = "emu"))] +use azihsm_crypto::EccPublicKey; use azihsm_crypto::EcdsaAlgo; use azihsm_crypto::HashAlgo; use azihsm_crypto::HashOp; +#[cfg(not(feature = "emu"))] +use azihsm_crypto::ImportableKey; use azihsm_crypto::SignOp; use azihsm_crypto::x509_builder::cert_builder; use azihsm_crypto::x509_builder::cert_builder::CN_LEN; @@ -29,6 +33,9 @@ use azihsm_crypto::x509_builder::cert_builder::SN_LEN; use azihsm_crypto::x509_builder::intermediate_cert; use azihsm_crypto::x509_builder::leaf_cert; use azihsm_crypto::x509_builder::root_cert; +#[cfg(not(feature = "emu"))] +use azihsm_ddi_mbor_test_helpers::fake_manticore_key_report_bytes; +#[cfg(feature = "emu")] use azihsm_ddi_tbor_types::KEY_REPORT_DATA_LEN; use azihsm_ddi_tbor_types::MACH_SEED_LEN; use azihsm_ddi_tbor_types::PART_POLICY_LEN; @@ -759,24 +766,50 @@ pub(crate) fn sealing_props() -> HsmKeyProps { .expect("build sealing props") } -/// Mint an SD sealing key on `session` and return its masked blob and a -/// COSE_Sign1 `KeyReport` attesting it (signed by the PID key). -pub(crate) fn masked_key_and_report(session: &HsmSession) -> (Vec, Vec) { +/// Mint an SD sealing key and return its masked blob and attestation report. +/// +/// Emulator runs request a signed KeyReport. Hardware runs synthesize the +/// policy-bound v2 report until Manticore implements `TborKeyReport`. +pub(crate) fn masked_key_and_report( + session: &HsmSession, + policy: &[u8; PART_POLICY_LEN], +) -> (Vec, Vec) { + #[cfg(feature = "emu")] + let _ = policy; let mut algo = HsmSealingKeyGenAlgo::default(); let key = HsmKeyManager::generate_key(session, &mut algo, sealing_props()) .expect("generate sealing key"); let masked = key.masked_key_vec().expect("masked key"); + #[cfg(feature = "emu")] let report_data = [0u8; KEY_REPORT_DATA_LEN]; + #[cfg(feature = "emu")] let report_len = key .generate_key_report(&report_data, None) .expect("key report size"); + #[cfg(feature = "emu")] let mut report = vec![0u8; report_len]; + #[cfg(feature = "emu")] let written = key .generate_key_report(&report_data, Some(&mut report)) .expect("key report"); + #[cfg(feature = "emu")] report.truncate(written); + #[cfg(not(feature = "emu"))] + let report = { + let der = key.pub_key_der_vec().expect("sealing public key"); + let public_key = EccPublicKey::from_bytes(&der).expect("P-384 public key DER"); + let (x, y) = public_key.coord_vec().expect("P-384 coordinates"); + assert_eq!(x.len(), 48, "P-384 X coordinate"); + assert_eq!(y.len(), 48, "P-384 Y coordinate"); + let mut sec1 = [0u8; SEC1_PUB_LEN]; + sec1[0] = 0x04; + sec1[1..49].copy_from_slice(&x); + sec1[49..].copy_from_slice(&y); + fake_manticore_key_report_bytes(&sec1, policy) + }; + (masked, report) } diff --git a/ddi/mbor/test_helpers/src/report.rs b/ddi/mbor/test_helpers/src/report.rs index 5e0eaa4bb..5f1919523 100644 --- a/ddi/mbor/test_helpers/src/report.rs +++ b/ddi/mbor/test_helpers/src/report.rs @@ -3,6 +3,8 @@ //! Key Attestation Report Format in COSE_Sign1. +use azihsm_crypto::HashAlgo; +use azihsm_crypto::HashOp; use bitfield_struct::bitfield; use minicbor::CborLen; use minicbor::Decode; @@ -362,6 +364,110 @@ pub fn encode_ecc_public( Ok(out_len - encoder.writer().len()) } +const MANTICORE_REPORT_VERSION_V2: u16 = 2; +const MANTICORE_PARTITION_ID_SIZE: usize = 16; +const MANTICORE_PID_PUB_KEY_SIZE: usize = 97; +const MANTICORE_POTA_PUB_KEY_SIZE: usize = 97; +const MANTICORE_POLICY_DIGEST_SIZE: usize = 48; +const P384_SEC1_PUB_LEN: usize = 97; +const P384_FE_LEN: usize = 48; +const COSE_ELLIPTIC_CURVES_P_384: i8 = 2; + +#[derive(Encode, CborLen)] +#[cbor(map)] +struct ManticoreKeyAttestationReportPayloadV2 { + #[n(0)] + version: u16, + #[n(1)] + #[cbor(with = "minicbor::bytes")] + public_key: [u8; PUBLIC_KEY_MAX_SIZE], + #[n(2)] + public_key_size: u16, + #[n(3)] + flags: u32, + #[n(4)] + #[cbor(with = "minicbor::bytes")] + app_uuid: [u8; APP_UUID_SIZE], + #[n(5)] + #[cbor(with = "minicbor::bytes")] + report_data: [u8; REPORT_DATA_SIZE], + #[n(6)] + #[cbor(with = "minicbor::bytes")] + vm_launch_id: [u8; VM_LAUNCH_ID_SIZE], + #[n(7)] + key_scope: u8, + #[n(8)] + #[cbor(with = "minicbor::bytes")] + partition_id: [u8; MANTICORE_PARTITION_ID_SIZE], + #[n(9)] + #[cbor(with = "minicbor::bytes")] + pid_pub_key: [u8; MANTICORE_PID_PUB_KEY_SIZE], + #[n(10)] + #[cbor(with = "minicbor::bytes")] + pota_pub_key: [u8; MANTICORE_POTA_PUB_KEY_SIZE], + #[n(11)] + #[cbor(with = "minicbor::bytes")] + policy_digest: [u8; MANTICORE_POLICY_DIGEST_SIZE], +} + +/// Build an unsigned Manticore v2 key report for hardware tests. +/// +/// The report carries a real P-384 public key and the SHA-384 digest of +/// `policy`; all other payload fields and the ES384 signature are zeroed. +pub fn fake_manticore_key_report_bytes( + public_key_sec1: &[u8; P384_SEC1_PUB_LEN], + policy: &[u8], +) -> Vec { + assert_eq!(public_key_sec1[0], 0x04, "expected uncompressed P-384 key"); + let mut public_key = [0u8; PUBLIC_KEY_MAX_SIZE]; + let public_key_size = encode_ecc_public( + COSE_ELLIPTIC_CURVES_P_384, + &public_key_sec1[1..1 + P384_FE_LEN], + &public_key_sec1[1 + P384_FE_LEN..], + &mut public_key, + ) + .expect("encode P-384 COSE key"); + let mut policy_digest = [0u8; MANTICORE_POLICY_DIGEST_SIZE]; + HashAlgo::sha384() + .hash(policy, Some(&mut policy_digest)) + .expect("SHA-384 policy digest"); + let payload = ManticoreKeyAttestationReportPayloadV2 { + version: MANTICORE_REPORT_VERSION_V2, + public_key, + public_key_size: u16::try_from(public_key_size).expect("COSE key length"), + flags: 0, + app_uuid: [0u8; APP_UUID_SIZE], + report_data: [0u8; REPORT_DATA_SIZE], + vm_launch_id: [0u8; VM_LAUNCH_ID_SIZE], + key_scope: 0, + partition_id: [0u8; MANTICORE_PARTITION_ID_SIZE], + pid_pub_key: [0u8; MANTICORE_PID_PUB_KEY_SIZE], + pota_pub_key: [0u8; MANTICORE_POTA_PUB_KEY_SIZE], + policy_digest, + }; + let mut payload_bytes = vec![0u8; minicbor::len(&payload)]; + minicbor::encode(&payload, payload_bytes.as_mut_slice()).expect("encode Manticore v2 report"); + let cose = CoseSign1Object { + protected_header: PROTECTED_HEADER, + unprotected_header: UnprotectedHeader {}, + payload: &payload_bytes, + signature: [0u8; SIGNATURE_SIZE], + }; + let mut output = vec![ + 0u8; + COSE_SIGN1_TAG_SIZE + + COSE_SIGN1_ENCODING_BYTES + + PROTECTED_HEADER_SIZE + + payload_bytes.len() + + SIGNATURE_SIZE + ]; + let written = cose + .encode(&mut output) + .expect("encode Manticore COSE_Sign1"); + output.truncate(written); + output +} + #[cfg(test)] mod tests { use super::*; diff --git a/ddi/nix/src/dev.rs b/ddi/nix/src/dev.rs index e99b32023..ae4d82d66 100644 --- a/ddi/nix/src/dev.rs +++ b/ddi/nix/src/dev.rs @@ -168,6 +168,11 @@ pub struct McrIoctlHeader { #[repr(u16)] pub enum McrCpCmdSet { Generic = 0, + /// `CP_CMD_SET_DATA_XFER` — selects the driver's data-transfer + /// ioctl, the only path that attaches an out-of-band Metadata Page. + /// The firmware routes purely on the SQE opcode, so this does not + /// change which handler runs. + DataXfer = 1, } #[bitfield(u8)] @@ -276,6 +281,58 @@ ioctl_readwrite!( McrCpGenericCmd ); +/// Maximum number of out-of-band buffers the data-transfer ioctl +/// accepts (`AZIHSM_MAX_DATA_XFER_BUFFERS`). Deliberately equal to the +/// firmware's `MAX_OOB_ITEMS`, so a request the host accepts is one the +/// device can also describe in a single Metadata Page. +const AZIHSM_MAX_DATA_XFER_BUFFERS: usize = 16; + +/// One entry of `struct azi_hsm_dataxfer_buffers::buffers`. +/// +/// The C layout is `{ __u32 xfer_length; __u8 *buf_addr; }`, i.e. a +/// 16-byte stride with 4 bytes of tail padding after `xfer_length`; +/// `#[repr(C)]` reproduces that exactly (verified with `offsetof`). +#[repr(C)] +#[derive(Clone, Copy)] +struct AzihsmDataXferBuffer { + xfer_length: u32, + buf_addr: *const u8, +} + +/// `struct azi_hsm_dataxfer_buffers` — the caller-supplied description +/// of the out-of-band items. +/// +/// The driver walks `buffers[..buffer_cnt]`, DMA-maps each one, and +/// builds the device-visible Metadata Page (a `buffer_count` header +/// plus one `{xfer_length, rsvd, hw_sgl_mem_paddr}` entry per item) +/// itself. The page's physical address lands in SQE DW13-14, which the +/// firmware reads as `oob_prp`. +#[repr(C)] +struct AzihsmDataXferBuffers { + buffer_cnt: u32, + buffers: [AzihsmDataXferBuffer; AZIHSM_MAX_DATA_XFER_BUFFERS], + rsvd: [u32; 128], +} + +/// `struct azihsm_ctrl_data_xfer_cmd` — the generic command plus the +/// out-of-band buffer list. The leading `generic_cmd` is layout- +/// identical to the plain generic ioctl, so opcode/session/src/dst are +/// populated exactly as on the non-OOB path. +#[repr(C)] +struct AzihsmCtrlDataXferCmd { + generic_cmd: McrCpGenericCmd, + dataxfer_buffers: AzihsmDataXferBuffers, +} + +/// `_IOWR('B', 0x7, struct azihsm_ctrl_data_xfer_cmd)` +const MCR_HSM_IOC_SEQ_DATA_XFER: u8 = 0x07; +ioctl_readwrite!( + azihsm_ctrl_data_xfer_ioctl, + MCR_HSM_IOC_MAGIC, + MCR_HSM_IOC_SEQ_DATA_XFER, + AzihsmCtrlDataXferCmd +); + // Fast path ioctl definitions and structures ///McrFpIoctlErrorKind @@ -905,12 +962,12 @@ impl DdiDev for DdiNixDev { /// TBOR dispatcher (see `fw/core/lib/src/op.rs::OP_TBOR`). const OP_TBOR: u16 = 2; - // The nix backend does not (yet) forward out-of-band items as - // SGL Data Block descriptors. Reject non-empty `oob_items` - // loudly rather than silently dropping payloads — any TBOR - // opcode that needs OOB data would otherwise misbehave in a - // hard-to-diagnose way. - if oob_items.is_some_and(|items| !items.is_empty()) { + // Out-of-band items are carried by the data-transfer ioctl, + // which takes the same generic command plus a buffer list. The + // driver DMA-maps each item and builds the device-visible + // Metadata Page, so nothing here needs physical addresses. + let oob = oob_items.unwrap_or(&[]); + if oob.len() > AZIHSM_MAX_DATA_XFER_BUFFERS { return Err(DdiError::InvalidParameter); } @@ -960,15 +1017,60 @@ impl DdiDev for DdiNixDev { cmd.in_data.dst_buf = resp_buf.as_mut_ptr(); // ── 3. Issue the ioctl ─────────────────────────────────── - // SAFETY: src/dst pointers above are valid for the duration - // of the ioctl call; the buffers outlive `cmd`. - let res = unsafe { mcr_ctrl_cmd_generic_ioctl(self.file.read().as_raw_fd(), &mut cmd) }; - if res.is_err() { - self.map_ioctl_status_tbor(cmd.out_data.ioctl_status)?; - res.map_err(DdiError::NixError)?; - } - if cmd.out_data.status != 0 { - return Err(DdiError::DdiError(cmd.out_data.status)); + let fd = self.file.read().as_raw_fd(); + let out_data = if oob.is_empty() { + // SAFETY: `cmd` is a valid, fully-initialised + // `McrCpGenericCmd`, and the src/dst pointers it holds stay + // valid for the duration of the call because `req_buf` and + // `resp_buf` outlive it. + let res = unsafe { mcr_ctrl_cmd_generic_ioctl(fd, &mut cmd) }; + if res.is_err() { + self.map_ioctl_status_tbor(cmd.out_data.ioctl_status)?; + res.map_err(DdiError::NixError)?; + } + cmd.out_data + } else { + let mut buffers = [AzihsmDataXferBuffer { + xfer_length: 0, + buf_addr: std::ptr::null(), + }; AZIHSM_MAX_DATA_XFER_BUFFERS]; + for (slot, item) in buffers.iter_mut().zip(oob.iter()) { + slot.xfer_length = item.len() as u32; + slot.buf_addr = item.as_ptr(); + } + + let mut xfer_cmd = AzihsmCtrlDataXferCmd { + generic_cmd: cmd, + dataxfer_buffers: AzihsmDataXferBuffers { + buffer_cnt: oob.len() as u32, + buffers, + rsvd: [0; 128], + }, + }; + // The driver validates `in.cmdset` against the ioctl it was + // reached through, so the OOB path must advertise + // `DataXfer`. Routing to the TBOR handler still comes from + // the opcode in `rsvd1`. + xfer_cmd.generic_cmd.in_data.command_set = McrCpCmdSet::DataXfer; + // The driver validates the header against the *whole* + // data-transfer command, not just the generic prefix. + xfer_cmd.generic_cmd.hdr.ioctl_data_size = + mem::size_of::() as u32; + + // SAFETY: `xfer_cmd` is a valid, fully-initialised + // `AzihsmCtrlDataXferCmd`. Its src/dst pointers and the + // borrowed `oob` item slices recorded in `buffers` all + // outlive this blocking call, so the driver only ever + // dereferences live memory. + let res = unsafe { azihsm_ctrl_data_xfer_ioctl(fd, &mut xfer_cmd) }; + if res.is_err() { + self.map_ioctl_status_tbor(xfer_cmd.generic_cmd.out_data.ioctl_status)?; + res.map_err(DdiError::NixError)?; + } + xfer_cmd.generic_cmd.out_data + }; + if out_data.status != 0 { + return Err(DdiError::DdiError(out_data.status)); } // ── 4. Decode the typed response ───────────────────────── @@ -976,7 +1078,7 @@ impl DdiDev for DdiNixDev { // therefore across a trust boundary — clamp it against the // allocated response buffer before indexing to avoid a // host-side panic on a bogus device value. - let resp_len = cmd.out_data.byte_count as usize; + let resp_len = out_data.byte_count as usize; if resp_len > RESP_BUF_LEN { return Err(DdiError::InvalidParameter); } diff --git a/ddi/tbor/types/tests/README.md b/ddi/tbor/types/tests/README.md index e87822cbf..ff1ae82a0 100644 --- a/ddi/tbor/types/tests/README.md +++ b/ddi/tbor/types/tests/README.md @@ -24,7 +24,8 @@ table. * `--features emu` — canonical configuration, runs the full suite. * `--features mock` — transport-contract probes only. -* No backend feature — pure host-side codec tests only. +* No backend feature — native OS backend; hardware-eligible tests run + against the attached device. The canonical command is: @@ -32,6 +33,27 @@ The canonical command is: cargo test -p azihsm_ddi_tbor_types --tests --features emu ``` +On a foundational HSM host with the native driver configured, run the +hardware reseal suite serially: + +```bash +cargo test -p azihsm_ddi_tbor_types \ + --test azihsm_ddi_tbor_tests \ + commands::sd_reseal_remote_backup_hw \ + -- --test-threads=1 +``` + +The public API reseal flow is exercised separately: + +```bash +cargo test -p azihsm_api_tests \ + sd::reseal_tests \ + -- --test-threads=1 +``` + +These tests provision and mutate a partition. Use a disposable test +partition and do not run the two commands concurrently. + ## Spec coverage matrix [`SPEC_COVERAGE.md`](SPEC_COVERAGE.md) maps each TBOR wire-protocol diff --git a/ddi/tbor/types/tests/SPEC_COVERAGE.md b/ddi/tbor/types/tests/SPEC_COVERAGE.md index ba2d9752b..3a712316f 100644 --- a/ddi/tbor/types/tests/SPEC_COVERAGE.md +++ b/ddi/tbor/types/tests/SPEC_COVERAGE.md @@ -133,6 +133,43 @@ All test names below are relative to the | `mach_seed` plaintext length ≠ `MACH_SEED_LEN` | ✅ 🔁 | `part_init::crypto_rejects::part_init_wrong_mach_seed_length` | Loop over `[MACH_SEED_LEN - 1, MACH_SEED_LEN + 1]`; one rotated-CO session reused across iterations (length check fires before any partition mutation) | | Malformed `pota_thumbprint` length | ⚠️ | — | Wire field is fixed-size; FW reaction not exercised | +## `SdCreateRemoteBackup` (opcode in-session, gated) + +Hardware tests use an unsigned synthetic KeyReport with a structurally valid +P-384 receiver key. This covers the command contract through report parsing, +point validation, sealing-key unmasking, HPKE, backup generation, OOB transport, +and lifecycle/policy gates. Certificate-chain and report-signature authenticity +remain emulator-only until Manticore enables those checks. + +| Requirement | Status | Test | Notes | +|---|---|---|---| +| Happy path returns non-zero remote POK, local POK, and SDMK backups at their exact wire lengths | ✅ | `sd_create_remote_backup::sd_create_remote_backup_roundtrip_emu`; `sd_create_remote_backup::sd_create_remote_backup_roundtrip_hw` | Hardware uses synthetic unsigned KeyReport | +| One-shot SD creation → `SdAlreadyInitialized` | ✅ | `sd_create_remote_backup::sd_create_remote_backup_is_one_shot_emu`; `sd_create_remote_backup::sd_create_remote_backup_is_one_shot_hw` | First create succeeds before retry | +| Missing OOB report payload → `InvalidArg` | ✅ | `sd_create_remote_backup::sd_create_remote_backup_rejects_missing_oob_emu`; `sd_create_remote_backup::sd_create_remote_backup_rejects_missing_oob_hw` | Descriptors are present; payload page is absent | +| Policy does not bind this partition as backup → `InvalidArg` | ✅ | `sd_create_remote_backup::sd_create_remote_backup_rejects_non_backing_policy_emu`; `sd_create_remote_backup::sd_create_remote_backup_rejects_non_backing_policy_hw` | Exact initialized policy is replayed | +| Malformed KeyReport framing → `InvalidArg` | ✅ (hw) | `sd_create_remote_backup::sd_create_remote_backup_rejects_malformed_report_hw` | Corrupts the report's leading COSE framing byte | +| Invalid receiver P-384 point → `EccPointValidationFailed` | ✅ (hw) | `sd_create_remote_backup::sd_create_remote_backup_rejects_invalid_receiver_point_hw` | Valid report structure with zero coordinates | +| Tampered masked sealing-key authentication tag → `AesGcmDecryptTagDoesNotMatch` | ✅ (hw) | `sd_create_remote_backup::sd_create_remote_backup_rejects_tampered_sealing_key_hw` | Preserves cleartext metadata and flips the GCM tag | +| Create before `PartFinal` → `InvalidArg` | ✅ (hw) | `sd_create_remote_backup::sd_create_remote_backup_rejects_before_finalize_hw` | Partition-state gate fires before evidence/key validation | +| Partition-owner chain is not rooted at policy SATA key → `InvalidArg` | ✅ (emu) | `sd_create_remote_backup::sd_create_remote_backup_rejects_wrong_sata_anchor_emu` | Authenticity path is not enabled on Manticore | +| Evidence chains disagree on receiver leaf key → `InvalidArg` | ✅ (emu) | `sd_create_remote_backup::sd_create_remote_backup_rejects_leaf_key_mismatch_emu` | Authenticity path is not enabled on Manticore | +| Tampered certificate signature → `X509SignatureInvalid` | ✅ (emu) | `sd_create_remote_backup::sd_create_remote_backup_rejects_tampered_cert_sig_emu` | Authenticity path is not enabled on Manticore | +| Tampered KeyReport signature → `InvalidArg` | ✅ (emu) | `sd_create_remote_backup::sd_create_remote_backup_rejects_tampered_report_emu` | Authenticity path is not enabled on Manticore | + +## `SdResealRemoteBackup` (opcode in-session, gated) + +Hardware tests create a real source backup through `SdCreateRemoteBackup`, +then reseal it using real masked sealing keys and policy-bound synthetic v2 +reports. Certificate-chain and report-signature authenticity remain outside +the hardware acceptance scope until Manticore enables those checks. + +| Requirement | Status | Test | Notes | +|---|---|---|---| +| Happy path returns a non-zero fresh remote backup | ✅ (hw) | `sd_reseal_remote_backup_hw::sd_reseal_remote_backup_roundtrip_hw` | Source backup is produced by CreateSD in the same initialized partition | +| Repeated reseal uses a fresh HPKE encapsulation | ✅ (hw) | `sd_reseal_remote_backup_hw::sd_reseal_remote_backup_rerandomizes_hw` | Two outputs for the same source must differ | +| Tampered source ciphertext is rejected | ✅ (hw) | `sd_reseal_remote_backup_hw::sd_reseal_remote_backup_rejects_tampered_source_hw` | Flips the final source-backup byte before HPKE open | +| Missing source/destination OOB payloads → `InvalidArg` | ✅ (hw) | `sd_reseal_remote_backup_hw::sd_reseal_remote_backup_rejects_missing_oob_hw` | Request descriptors are present but no OOB page is supplied | + ## Default-PSK dispatcher gate (cross-cutting) The gate (see `fw/core/lib/src/ddi/tbor/mod.rs::dispatch`) rejects diff --git a/ddi/tbor/types/tests/commands/mod.rs b/ddi/tbor/types/tests/commands/mod.rs index d1a891613..2ff6a1af8 100644 --- a/ddi/tbor/types/tests/commands/mod.rs +++ b/ddi/tbor/types/tests/commands/mod.rs @@ -32,6 +32,8 @@ pub mod rsa_mod_exp; pub mod sd_create_peer_backup; pub mod sd_create_remote_backup; pub mod sd_reseal_remote_backup; +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +pub mod sd_reseal_remote_backup_hw; pub mod sd_restore_local_backup; pub mod sd_restore_peer_backup; pub mod sd_restore_remote_backup; diff --git a/ddi/tbor/types/tests/commands/part_init/mod.rs b/ddi/tbor/types/tests/commands/part_init/mod.rs index 4cfe46cab..eb74306d5 100644 --- a/ddi/tbor/types/tests/commands/part_init/mod.rs +++ b/ddi/tbor/types/tests/commands/part_init/mod.rs @@ -104,11 +104,6 @@ pub(crate) fn known_good_part_policy() -> [u8; PART_POLICY_LEN] { /// `POTAPubKey` (raw P-384 `X ‖ Y`, 96 bytes), so `PartFinal` can validate /// a PTA certificate chain anchored to it. /// -/// TODO(hw): gated to `feature = "emu"` because the only consumers -/// (`part_final`, `sd_sealing_key_gen`, `sd_create_remote_backup`) -/// are not yet supported on hw. Remove this gate once those commands -/// land on hw. -#[cfg(feature = "emu")] pub(crate) fn part_policy_with_pota(pota_raw: &[u8; 96]) -> [u8; PART_POLICY_LEN] { const OFF_POTA: usize = 2; let mut bytes = known_good_part_policy(); diff --git a/ddi/tbor/types/tests/commands/sd_create_remote_backup.rs b/ddi/tbor/types/tests/commands/sd_create_remote_backup.rs index 8cbd26e10..4a85100cf 100644 --- a/ddi/tbor/types/tests/commands/sd_create_remote_backup.rs +++ b/ddi/tbor/types/tests/commands/sd_create_remote_backup.rs @@ -26,19 +26,30 @@ //! * Missing OOB evidence → `InvalidArg`. //! * Policy that does not name this partition as the backing partition //! (`backup_part_id` / `backup_part_pub_key` absent) → `InvalidArg`. +//! * Malformed receiver report → `InvalidArg`. +//! * Invalid receiver P-384 point → `EccPointValidationFailed`. +//! * Tampered masked sealing key → `AesGcmDecryptTagDoesNotMatch`. +//! * Create before partition finalization → `InvalidArg`. +//! +//! Hardware exercises the externally observable command contract with an +//! unsigned synthetic KeyReport. Certificate-chain, leaf-binding, and report +//! signature authenticity remain emulator-only until Manticore enables those +//! verification steps. -#![cfg(feature = "emu")] - +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +use azihsm_ddi_mbor_test_helpers::fake_manticore_key_report_bytes; use azihsm_ddi_tbor_types::tbor_int::U16; use azihsm_ddi_tbor_types::CertDescriptor; use azihsm_ddi_tbor_types::PartPolicy; use azihsm_ddi_tbor_types::PolicyKeyKind; use azihsm_ddi_tbor_types::ReportDescriptor; +#[cfg(feature = "emu")] use azihsm_ddi_tbor_types::TborKeyReportReq; use azihsm_ddi_tbor_types::TborPartInfoReq; use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; use azihsm_ddi_tbor_types::TborStatus; +#[cfg(feature = "emu")] use azihsm_ddi_tbor_types::KEY_REPORT_DATA_LEN; use azihsm_ddi_tbor_types::MASKED_SD_LEN; use azihsm_ddi_tbor_types::PART_POLICY_LEN; @@ -51,15 +62,17 @@ use crate::commands::part_init::bootstrap_rotated_co; use crate::commands::part_init::known_good_part_policy; use crate::commands::part_init::mach_seed; use crate::commands::part_init::part_policy_with_pota; -use crate::commands::part_init::pota_thumbprint; use crate::commands::part_init::ROTATED_CO_PSK; +#[cfg(feature = "emu")] use crate::commands::sd_sealing_key_gen::finalized_co_session; use crate::harness::x509_fixture::make_chain; -use crate::harness::x509_fixture::make_pta_chain; use crate::harness::x509_fixture::pta_pub_from_csr; use crate::harness::x509_fixture::CaKey; use crate::harness::x509_fixture::GeneratedChain; +use crate::harness::x509_fixture::PotaFixture; use crate::harness::x509_fixture::RAW_PUB_LEN; +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +use crate::harness::x509_fixture::SEC1_PUB_LEN; use crate::harness::SessionHandshake; use crate::harness::TestCtx; @@ -127,7 +140,7 @@ pub(crate) fn finalized_backing_session( pid_pub.copy_from_slice(&info.pid_pub_key); // POTA anchor for the PTA certificate chain `PartFinal` validates. - let pota = CaKey::generate(); + let pota = PotaFixture::generate(); let policy = backing_part_policy( &info.pid, &info.pid_pub_key, @@ -136,9 +149,9 @@ pub(crate) fn finalized_backing_session( ); let init = ctx - .part_init(&session, &mach_seed(), &policy, &pota_thumbprint()) + .part_init(&session, &mach_seed(), &policy, pota.thumbprint()) .expect("PartInit"); - let chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + let chain = pota.chain_for(&pta_pub_from_csr(&init.pta_csr)); ctx.part_final(&session, &policy, &[], &chain.der_items()) .expect("PartFinal"); @@ -149,6 +162,7 @@ pub(crate) fn finalized_backing_session( /// `(masked_sealing_key, key_report_bytes)`. In the self-backup tests /// this key is both the sender's authentication key and (via its report) /// the receiver's `RcvrPub` source; the report is signed by the PID key. +#[cfg(feature = "emu")] pub(crate) fn masked_key_and_report(ctx: &TestCtx, session_id: u16) -> (Vec, Vec) { let seal = ctx .tbor(&TborSdSealingKeyGenReq { @@ -287,6 +301,70 @@ fn dummy_evidence(report: &[u8]) -> ReceiverEvidence { } } +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +struct HardwareFixture { + session: SessionHandshake, + policy: [u8; PART_POLICY_LEN], + masked_sealing_key: Vec, + evidence: ReceiverEvidence, +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +impl HardwareFixture { + fn new(ctx: &TestCtx) -> Self { + let sata_key = CaKey::generate(); + let (session, policy, pid_pub) = finalized_backing_session(ctx, &sata_key); + let sealing_key = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: SCOPE_LOCAL, + }) + .expect("SdSealingKeyGen"); + let receiver_pub = sealing_pub_to_sec1(&sealing_key.pub_key); + let report = fake_manticore_key_report_bytes(&receiver_pub, &policy); + let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); + + Self { + session, + policy, + masked_sealing_key: sealing_key.masked_key.to_vec(), + evidence, + } + } + + fn request(&self) -> TborSdCreateRemoteBackupReq { + backup_request( + self.session.session_id, + self.masked_sealing_key.clone(), + &self.evidence, + &self.policy, + ) + } +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +pub(crate) fn sealing_pub_to_sec1(pub_key_le: &[u8]) -> [u8; SEC1_PUB_LEN] { + let mut receiver_pub = [0u8; SEC1_PUB_LEN]; + receiver_pub[0] = 0x04; + let coord_len = pub_key_le.len() / 2; + receiver_pub[1..1 + coord_len].copy_from_slice(&pub_key_le[..coord_len]); + receiver_pub[1..1 + coord_len].reverse(); + receiver_pub[1 + coord_len..].copy_from_slice(&pub_key_le[coord_len..]); + receiver_pub[1 + coord_len..].reverse(); + receiver_pub +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +fn assert_backup_response(resp: &azihsm_ddi_tbor_types::TborSdCreateRemoteBackupResp) { + assert_eq!(resp.pok_remote_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!(resp.pok_remote_backup.iter().any(|&byte| byte != 0)); + assert_eq!(resp.pok_local_backup.len(), MASKED_SD_LEN); + assert!(resp.pok_local_backup.iter().any(|&byte| byte != 0)); + assert_eq!(resp.sd_mk_backup.len(), SD_MK_BACKUP_LEN); + assert!(resp.sd_mk_backup.iter().any(|&byte| byte != 0)); +} + +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_roundtrip_emu() { let ctx = TestCtx::new(); @@ -323,6 +401,7 @@ fn sd_create_remote_backup_roundtrip_emu() { ); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_is_one_shot_emu() { let ctx = TestCtx::new(); @@ -343,6 +422,7 @@ fn sd_create_remote_backup_is_one_shot_emu() { ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::SdAlreadyInitialized); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_missing_oob_emu() { let ctx = TestCtx::new(); @@ -357,6 +437,7 @@ fn sd_create_remote_backup_rejects_missing_oob_emu() { ctx.expect_fw_reject(&req, TborStatus::InvalidArg); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_non_backing_policy_emu() { let ctx = TestCtx::new(); @@ -377,12 +458,14 @@ fn sd_create_remote_backup_rejects_non_backing_policy_emu() { /// Flip the final byte of a DER/COSE blob — corrupts the trailing ECDSA /// signature value without disturbing the outer length-prefixed structure. +#[cfg(feature = "emu")] fn flip_last_byte(mut bytes: Vec) -> Vec { let n = bytes.len(); bytes[n - 1] ^= 0xFF; bytes } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_wrong_sata_anchor_emu() { let ctx = TestCtx::new(); @@ -401,6 +484,7 @@ fn sd_create_remote_backup_rejects_wrong_sata_anchor_emu() { ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::InvalidArg); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_leaf_key_mismatch_emu() { let ctx = TestCtx::new(); @@ -420,6 +504,7 @@ fn sd_create_remote_backup_rejects_leaf_key_mismatch_emu() { ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::InvalidArg); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_tampered_cert_sig_emu() { let ctx = TestCtx::new(); @@ -439,6 +524,7 @@ fn sd_create_remote_backup_rejects_tampered_cert_sig_emu() { ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::X509SignatureInvalid); } +#[cfg(feature = "emu")] #[test] fn sd_create_remote_backup_rejects_tampered_report_emu() { let ctx = TestCtx::new(); @@ -454,3 +540,139 @@ fn sd_create_remote_backup_rejects_tampered_report_emu() { let req = backup_request(session.session_id, masked, &evidence, &policy); ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::InvalidArg); } + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_roundtrip_hw() { + let ctx = TestCtx::new(); + let fixture = HardwareFixture::new(&ctx); + let req = fixture.request(); + + let resp = ctx + .tbor_oob(&req, &fixture.evidence.oob()) + .expect("SdCreateRemoteBackup hardware roundtrip"); + assert_backup_response(&resp); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_is_one_shot_hw() { + let ctx = TestCtx::new(); + let fixture = HardwareFixture::new(&ctx); + let req = fixture.request(); + + ctx.tbor_oob(&req, &fixture.evidence.oob()) + .expect("first SdCreateRemoteBackup"); + ctx.expect_fw_reject_oob( + &req, + &fixture.evidence.oob(), + TborStatus::SdAlreadyInitialized, + ); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_missing_oob_hw() { + let ctx = TestCtx::new(); + let fixture = HardwareFixture::new(&ctx); + + ctx.expect_fw_reject(&fixture.request(), TborStatus::InvalidArg); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_non_backing_policy_hw() { + let ctx = TestCtx::new(); + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + let pota = PotaFixture::generate(); + let policy = part_policy_with_pota(&pota.raw_pub()); + let init = ctx + .part_init(&session, &mach_seed(), &policy, pota.thumbprint()) + .expect("PartInit"); + let chain = pota.chain_for(&pta_pub_from_csr(&init.pta_csr)); + ctx.part_final(&session, &policy, &[], &chain.der_items()) + .expect("PartFinal"); + let sealing_key = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: SCOPE_LOCAL, + }) + .expect("SdSealingKeyGen"); + let report = + fake_manticore_key_report_bytes(&sealing_pub_to_sec1(&sealing_key.pub_key), &policy); + let evidence = dummy_evidence(&report); + let req = backup_request( + session.session_id, + sealing_key.masked_key.to_vec(), + &evidence, + &policy, + ); + + ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::InvalidArg); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_malformed_report_hw() { + let ctx = TestCtx::new(); + let mut fixture = HardwareFixture::new(&ctx); + let report_index = fixture.evidence.report.index as usize; + fixture.evidence.oob_items[report_index][0] = 0; + let req = fixture.request(); + + ctx.expect_fw_reject_oob(&req, &fixture.evidence.oob(), TborStatus::InvalidArg); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_invalid_receiver_point_hw() { + let ctx = TestCtx::new(); + let mut fixture = HardwareFixture::new(&ctx); + let mut invalid_pub = [0u8; SEC1_PUB_LEN]; + invalid_pub[0] = 0x04; + let invalid_report = fake_manticore_key_report_bytes(&invalid_pub, &fixture.policy); + let report_index = fixture.evidence.report.index as usize; + fixture.evidence.oob_items[report_index] = invalid_report; + let req = fixture.request(); + + ctx.expect_fw_reject_oob( + &req, + &fixture.evidence.oob(), + TborStatus::EccPointValidationFailed, + ); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_tampered_sealing_key_hw() { + let ctx = TestCtx::new(); + let mut fixture = HardwareFixture::new(&ctx); + let last = fixture.masked_sealing_key.len() - 1; + fixture.masked_sealing_key[last] ^= 0x01; + + ctx.expect_fw_reject_oob( + &fixture.request(), + &fixture.evidence.oob(), + TborStatus::AesGcmDecryptTagDoesNotMatch, + ); +} + +#[cfg(all(not(feature = "emu"), not(feature = "mock"), not(feature = "sock")))] +#[test] +fn sd_create_remote_backup_rejects_before_finalize_hw() { + let ctx = TestCtx::new(); + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + let policy = known_good_part_policy(); + let mut receiver_pub = [0u8; SEC1_PUB_LEN]; + receiver_pub[0] = 0x04; + let report = fake_manticore_key_report_bytes(&receiver_pub, &policy); + let evidence = dummy_evidence(&report); + let req = backup_request( + session.session_id, + vec![0u8; azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN], + &evidence, + &policy, + ); + + ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::InvalidArg); +} diff --git a/ddi/tbor/types/tests/commands/sd_reseal_remote_backup_hw.rs b/ddi/tbor/types/tests/commands/sd_reseal_remote_backup_hw.rs new file mode 100644 index 000000000..e23f991e3 --- /dev/null +++ b/ddi/tbor/types/tests/commands/sd_reseal_remote_backup_hw.rs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Hardware-only `SdResealRemoteBackup` integration tests. +//! +//! Manticore does not yet implement `TborKeyReport`, so these tests build +//! unsigned policy-bound v2 reports from real `SdSealingKeyGen` public keys. + +use azihsm_crypto::HashAlgo; +use azihsm_crypto::HashOp; +use azihsm_ddi_mbor_test_helpers::fake_manticore_key_report_bytes; +use azihsm_ddi_tbor_types::tbor_int::U16; +use azihsm_ddi_tbor_types::CertDescriptor; +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::ReportDescriptor; +use azihsm_ddi_tbor_types::TborSdResealRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use zerocopy::TryFromBytes; + +use crate::commands::sd_create_remote_backup::backup_request; +use crate::commands::sd_create_remote_backup::build_receiver_evidence; +use crate::commands::sd_create_remote_backup::finalized_backing_session; +use crate::commands::sd_create_remote_backup::sealing_pub_to_sec1; +use crate::harness::x509_fixture::make_chain; +use crate::harness::x509_fixture::CaKey; +use crate::harness::x509_fixture::GeneratedChain; +use crate::harness::x509_fixture::RAW_PUB_LEN; +use crate::harness::SessionHandshake; +use crate::harness::TestCtx; + +const SCOPE_LOCAL: u8 = 0b011; + +#[test] +fn synthetic_report_binds_policy_hw() { + let mut public_key = [0x5A; 97]; + public_key[0] = 0x04; + let policy = [0xA5; PART_POLICY_LEN]; + let report = fake_manticore_key_report_bytes(&public_key, &policy); + let mut digest = [0u8; 48]; + HashAlgo::sha384() + .hash(&policy, Some(&mut digest)) + .expect("SHA-384 policy digest"); + + assert!(report.windows(digest.len()).any(|window| window == digest)); +} + +struct ResealEvidence { + oob_items: Vec>, + src_mfgr: Vec, + src_owner: Vec, + src_part_owner: Vec, + src_report: ReportDescriptor, + dest_mfgr: Vec, + dest_owner: Vec, + dest_part_owner: Vec, + dest_report: ReportDescriptor, +} + +impl ResealEvidence { + fn oob(&self) -> Vec<&[u8]> { + self.oob_items.iter().map(Vec::as_slice).collect() + } +} + +fn push_item(items: &mut Vec>, bytes: &[u8]) -> CertDescriptor { + let index = items.len() as u8; + items.push(bytes.to_vec()); + CertDescriptor { + index, + length: U16::new(bytes.len() as u16), + } +} + +fn push_evidence( + items: &mut Vec>, + mfgr: &GeneratedChain, + owner: &GeneratedChain, + part_owner: &GeneratedChain, + report: &[u8], +) -> ( + Vec, + Vec, + Vec, + ReportDescriptor, +) { + let mfgr = vec![ + push_item(items, &mfgr.root_der), + push_item(items, &mfgr.leaf_der), + ]; + let owner = vec![ + push_item(items, &owner.root_der), + push_item(items, &owner.leaf_der), + ]; + let part_owner = vec![ + push_item(items, &part_owner.root_der), + push_item(items, &part_owner.leaf_der), + ]; + let report = push_item(items, report); + ( + mfgr, + owner, + part_owner, + ReportDescriptor { + index: report.index, + length: report.length, + }, + ) +} + +fn build_reseal_evidence( + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + src_report: &[u8], + dest_report: &[u8], +) -> ResealEvidence { + let mut items = Vec::new(); + let (src_mfgr, src_owner, src_part_owner, src_report) = push_evidence( + &mut items, + &make_chain(&CaKey::generate(), pid_pub), + &make_chain(&CaKey::generate(), pid_pub), + &make_chain(sata_key, pid_pub), + src_report, + ); + let (dest_mfgr, dest_owner, dest_part_owner, dest_report) = push_evidence( + &mut items, + &make_chain(&CaKey::generate(), pid_pub), + &make_chain(&CaKey::generate(), pid_pub), + &make_chain(sata_key, pid_pub), + dest_report, + ); + ResealEvidence { + oob_items: items, + src_mfgr, + src_owner, + src_part_owner, + src_report, + dest_mfgr, + dest_owner, + dest_part_owner, + dest_report, + } +} + +fn sealing_key_and_report( + ctx: &TestCtx, + session_id: u16, + policy: &[u8; PART_POLICY_LEN], +) -> (Vec, Vec) { + let sealing_key = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id, + scope: SCOPE_LOCAL, + }) + .expect("SdSealingKeyGen"); + let report = + fake_manticore_key_report_bytes(&sealing_pub_to_sec1(&sealing_key.pub_key), policy); + (sealing_key.masked_key.to_vec(), report) +} + +fn create_source_backup( + ctx: &TestCtx, + session_id: u16, + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + masked_sender_key: Vec, + receiver_report: &[u8], + policy: &[u8; PART_POLICY_LEN], +) -> [u8; POK_REMOTE_BACKUP_LEN] { + let receiver = build_receiver_evidence(pid_pub, sata_key, receiver_report); + let request = backup_request(session_id, masked_sender_key, &receiver, policy); + ctx.tbor_oob(&request, &receiver.oob()) + .expect("SdCreateRemoteBackup source backup") + .pok_remote_backup +} + +fn reseal_request( + session_id: u16, + masked_receiver_key: &[u8], + evidence: &ResealEvidence, + policy: &[u8; PART_POLICY_LEN], + src_remote_backup: &[u8; POK_REMOTE_BACKUP_LEN], +) -> TborSdResealRemoteBackupReq { + TborSdResealRemoteBackupReq { + session_id, + masked_sealing_key: masked_receiver_key + .try_into() + .expect("masked receiver key length"), + policy: PartPolicy::try_read_from_bytes(policy).expect("canonical policy"), + src_mfgr_cert_chain: evidence.src_mfgr.clone(), + src_owner_cert_chain: evidence.src_owner.clone(), + src_part_owner_cert_chain: evidence.src_part_owner.clone(), + src_report: evidence.src_report, + dest_mfgr_cert_chain: evidence.dest_mfgr.clone(), + dest_owner_cert_chain: evidence.dest_owner.clone(), + dest_part_owner_cert_chain: evidence.dest_part_owner.clone(), + dest_report: evidence.dest_report, + src_remote_backup: *src_remote_backup, + } +} + +struct HardwareFixture { + ctx: TestCtx, + session: SessionHandshake, + policy: [u8; PART_POLICY_LEN], + masked_receiver_key: Vec, + source_backup: [u8; POK_REMOTE_BACKUP_LEN], + evidence: ResealEvidence, +} + +impl HardwareFixture { + fn new() -> Self { + let ctx = TestCtx::new(); + let sata_key = CaKey::generate(); + let (session, policy, pid_pub) = finalized_backing_session(&ctx, &sata_key); + let session_id = session.session_id; + let (masked_receiver_key, receiver_report) = + sealing_key_and_report(&ctx, session_id, &policy); + let (masked_sender_key, sender_report) = sealing_key_and_report(&ctx, session_id, &policy); + let (_masked_destination_key, destination_report) = + sealing_key_and_report(&ctx, session_id, &policy); + let source_backup = create_source_backup( + &ctx, + session_id, + &pid_pub, + &sata_key, + masked_sender_key, + &receiver_report, + &policy, + ); + let evidence = + build_reseal_evidence(&pid_pub, &sata_key, &sender_report, &destination_report); + Self { + ctx, + session, + policy, + masked_receiver_key, + source_backup, + evidence, + } + } + + fn request(&self) -> TborSdResealRemoteBackupReq { + reseal_request( + self.session.session_id, + &self.masked_receiver_key, + &self.evidence, + &self.policy, + &self.source_backup, + ) + } +} + +#[test] +fn sd_reseal_remote_backup_roundtrip_hw() { + let fixture = HardwareFixture::new(); + let response = fixture + .ctx + .tbor_oob(&fixture.request(), &fixture.evidence.oob()) + .expect("SdResealRemoteBackup hardware roundtrip"); + + assert!(response.dst_remote_backup.iter().any(|&byte| byte != 0)); + assert_ne!(response.dst_remote_backup, fixture.source_backup); +} + +#[test] +fn sd_reseal_remote_backup_rerandomizes_hw() { + let fixture = HardwareFixture::new(); + let request = fixture.request(); + let first = fixture + .ctx + .tbor_oob(&request, &fixture.evidence.oob()) + .expect("first reseal"); + let second = fixture + .ctx + .tbor_oob(&request, &fixture.evidence.oob()) + .expect("second reseal"); + + assert_ne!(first.dst_remote_backup, second.dst_remote_backup); +} + +#[test] +fn sd_reseal_remote_backup_rejects_tampered_source_hw() { + let mut fixture = HardwareFixture::new(); + fixture.source_backup[POK_REMOTE_BACKUP_LEN - 1] ^= 0xFF; + + fixture + .ctx + .tbor_oob(&fixture.request(), &fixture.evidence.oob()) + .expect_err("tampered source backup must fail"); +} + +#[test] +fn sd_reseal_remote_backup_rejects_missing_oob_hw() { + let fixture = HardwareFixture::new(); + fixture + .ctx + .expect_fw_reject(&fixture.request(), TborStatus::InvalidArg); +} diff --git a/ddi/tbor/types/tests/commands/sd_restore_local_backup.rs b/ddi/tbor/types/tests/commands/sd_restore_local_backup.rs index 79aa5f936..f7415ddba 100644 --- a/ddi/tbor/types/tests/commands/sd_restore_local_backup.rs +++ b/ddi/tbor/types/tests/commands/sd_restore_local_backup.rs @@ -32,15 +32,14 @@ use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; use crate::commands::part_init::bootstrap_rotated_co; use crate::commands::part_init::mach_seed; -use crate::commands::part_init::pota_thumbprint; use crate::commands::part_init::ROTATED_CO_PSK; use crate::commands::sd_create_remote_backup::backing_part_policy; use crate::commands::sd_create_remote_backup::backup_request; use crate::commands::sd_create_remote_backup::build_receiver_evidence; use crate::commands::sd_create_remote_backup::masked_key_and_report; -use crate::harness::x509_fixture::make_pta_chain; use crate::harness::x509_fixture::pta_pub_from_csr; use crate::harness::x509_fixture::CaKey; +use crate::harness::x509_fixture::PotaFixture; use crate::harness::x509_fixture::RAW_PUB_LEN; use crate::harness::TestCtx; @@ -62,7 +61,7 @@ struct CreatedSd { /// `pota` / `sata` trust anchors and machine `seed` are supplied by the /// caller so the second device can re-finalize with an identical policy / /// certificate chain. -fn create_sd_on_first_device(seed: &[u8], sata: &CaKey, pota: &CaKey) -> CreatedSd { +fn create_sd_on_first_device(seed: &[u8], sata: &CaKey, pota: &PotaFixture) -> CreatedSd { let ctx = TestCtx::new(); let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); @@ -77,9 +76,9 @@ fn create_sd_on_first_device(seed: &[u8], sata: &CaKey, pota: &CaKey) -> Created ); let init = ctx - .part_init(&session, seed, &policy, &pota_thumbprint()) + .part_init(&session, seed, &policy, pota.thumbprint()) .expect("PartInit"); - let chain = make_pta_chain(pota, &pta_pub_from_csr(&init.pta_csr)); + let chain = pota.chain_for(&pta_pub_from_csr(&init.pta_csr)); let local_mk_backup = ctx .part_final(&session, &policy, &[], &chain.der_items()) .expect("PartFinal") @@ -105,14 +104,14 @@ fn create_sd_on_first_device(seed: &[u8], sata: &CaKey, pota: &CaKey) -> Created fn reboot_and_restore_part_local_mk( ctx: &TestCtx, seed: &[u8], - pota: &CaKey, + pota: &PotaFixture, created: &CreatedSd, ) -> crate::harness::SessionHandshake { let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); let init = ctx - .part_init(&session, seed, &created.policy, &pota_thumbprint()) + .part_init(&session, seed, &created.policy, pota.thumbprint()) .expect("PartInit (device 2)"); - let chain = make_pta_chain(pota, &pta_pub_from_csr(&init.pta_csr)); + let chain = pota.chain_for(&pta_pub_from_csr(&init.pta_csr)); ctx.part_final( &session, &created.policy, @@ -127,7 +126,7 @@ fn reboot_and_restore_part_local_mk( fn sd_restore_local_backup_roundtrip_emu() { let seed = mach_seed(); let sata = CaKey::generate(); - let pota = CaKey::generate(); + let pota = PotaFixture::generate(); // Device 1: finalize + CreateSD, capturing the local backups. let created = create_sd_on_first_device(&seed, &sata, &pota); @@ -162,7 +161,7 @@ fn sd_restore_local_backup_roundtrip_emu() { fn sd_restore_local_backup_is_one_shot_emu() { let seed = mach_seed(); let sata = CaKey::generate(); - let pota = CaKey::generate(); + let pota = PotaFixture::generate(); // A single device that has just created its SD is already // SD-initialized, so a local restore on the same incarnation is @@ -179,9 +178,9 @@ fn sd_restore_local_backup_is_one_shot_emu() { &pota.raw_pub(), ); let init = ctx - .part_init(&session, &seed, &policy, &pota_thumbprint()) + .part_init(&session, &seed, &policy, pota.thumbprint()) .expect("PartInit"); - let chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + let chain = pota.chain_for(&pta_pub_from_csr(&init.pta_csr)); ctx.part_final(&session, &policy, &[], &chain.der_items()) .expect("PartFinal"); @@ -222,7 +221,7 @@ fn sd_restore_local_backup_rejects_before_finalize_emu() { fn sd_restore_local_backup_rejects_tampered_pok_emu() { let seed = mach_seed(); let sata = CaKey::generate(); - let pota = CaKey::generate(); + let pota = PotaFixture::generate(); let created = create_sd_on_first_device(&seed, &sata, &pota); diff --git a/ddi/tbor/types/tests/harness/mod.rs b/ddi/tbor/types/tests/harness/mod.rs index 65dd35621..359ac036b 100644 --- a/ddi/tbor/types/tests/harness/mod.rs +++ b/ddi/tbor/types/tests/harness/mod.rs @@ -40,7 +40,6 @@ pub mod ctx; pub mod fixture; pub mod session; pub mod session_guard; -#[cfg(feature = "emu")] pub mod x509_fixture; // Re-export commonly-used schema items so test code doesn't have to diff --git a/ddi/tbor/types/tests/harness/x509_fixture.rs b/ddi/tbor/types/tests/harness/x509_fixture.rs index ada15d4b8..412036073 100644 --- a/ddi/tbor/types/tests/harness/x509_fixture.rs +++ b/ddi/tbor/types/tests/harness/x509_fixture.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Cross-platform X.509 PTA certificate-chain fixtures for `PartFinal`. +//! Cross-platform X.509 certificate-chain fixtures for TBOR integration tests. //! //! `PartFinal` validates that the supplied PTA certificate chain is //! anchored to the policy `POTAPubKey` and that its leaf public key is the @@ -26,6 +26,7 @@ use azihsm_crypto::EcdsaAlgo; use azihsm_crypto::HashAlgo; use azihsm_crypto::HashOp; use azihsm_crypto::SignOp; +use azihsm_ddi_tbor_types::POTA_THUMBPRINT_LEN; /// Length of a SEC1 uncompressed P-384 point (`0x04 ‖ X ‖ Y`). pub const SEC1_PUB_LEN: usize = 97; @@ -97,6 +98,56 @@ impl CaKey { } } +/// Reusable POTA root fixture for tests that provision a partition. +/// +/// Owns the POTA signing key, its self-signed root certificate, and the +/// certificate thumbprint supplied to `PartInit`. Command tests remain +/// responsible for constructing their command-specific partition policy. +pub struct PotaFixture { + ca: CaKey, + root_der: Vec, + thumbprint: [u8; POTA_THUMBPRINT_LEN], +} + +impl PotaFixture { + /// Generate a P-384 POTA key and its self-signed root certificate. + pub fn generate() -> Self { + let ca = CaKey::generate(); + let root_der = build_root(&ca); + let thumbprint = sha384(&root_der); + Self { + ca, + root_der, + thumbprint, + } + } + + /// Raw POTA public coordinates for the partition policy. + pub fn raw_pub(&self) -> [u8; RAW_PUB_LEN] { + self.ca.raw_pub() + } + + /// SHA-384 thumbprint of the self-signed POTA root certificate. + pub fn thumbprint(&self) -> &[u8; POTA_THUMBPRINT_LEN] { + &self.thumbprint + } + + /// Issue a root-to-PTA chain for the partition PTA public key. + pub fn chain_for(&self, pta_pub_sec1: &[u8; SEC1_PUB_LEN]) -> PtaChain { + PtaChain { + root_der: self.root_der.clone(), + pta_der: build_pta_intermediate(pta_pub_sec1, &self.ca), + } + } +} + +fn sha384(input: &[u8]) -> [u8; POTA_THUMBPRINT_LEN] { + let mut hash = HashAlgo::sha384(); + let mut out = [0u8; POTA_THUMBPRINT_LEN]; + hash.hash(input, Some(&mut out)).expect("SHA-384"); + out +} + /// SHA-1 of a SEC1 public key (Subject / Authority Key Identifier). fn sha1_ski(sec1: &[u8; SEC1_PUB_LEN]) -> [u8; 20] { let mut algo = HashAlgo::sha1();