diff --git a/api/lib/src/algo/sealing/key.rs b/api/lib/src/algo/sealing/key.rs index c9fe2f15d..0d5164940 100644 --- a/api/lib/src/algo/sealing/key.rs +++ b/api/lib/src/algo/sealing/key.rs @@ -78,6 +78,22 @@ impl HsmSecretKey for HsmSealingKey {} impl HsmDerivationKey for HsmSealingKey {} +impl HsmKeyReportOp for HsmSealingKey { + type Error = HsmError; + + /// Attests this non-resident sealing key via TBOR `KeyReport`, + /// routing on its masked-key envelope since there is no device + /// handle to reference. + fn generate_key_report( + &self, + report_data: &[u8], + report: Option<&mut [u8]>, + ) -> Result { + let masked_key = self.masked_key_vec()?; + ddi::masked_key_report(&self.session(), &masked_key, report_data, report) + } +} + #[derive(Default)] pub struct HsmSealingKeyGenAlgo {} diff --git a/api/lib/src/ddi/descriptor_utils.rs b/api/lib/src/ddi/descriptor_utils.rs new file mode 100644 index 000000000..0f5086151 --- /dev/null +++ b/api/lib/src/ddi/descriptor_utils.rs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Out-of-band SGL descriptor packing shared by TBOR commands that ship +//! DER certificate chains / reports out of band (`PartFinal`, the SD +//! backup family). +//! +//! Each item ships as its own OOB SGL Data Block; the firmware locates it +//! by the descriptor's `index` (its position in the shared `oob` item +//! list) and reads `length` bytes from it. + +use azihsm_ddi_tbor_types::*; + +use super::*; + +/// Appends a certificate chain's DER bytes to `oob`, returning the +/// matching `(index, length)` descriptors. Rejects an empty chain, a +/// chain longer than `max_certs`, an empty (zero-length) cert, or a cert +/// whose length overflows the 16-bit descriptor field. +pub(crate) fn push_cert_chain<'a>( + chain: &'a [HsmCert<'a>], + oob: &mut Vec<&'a [u8]>, + max_certs: usize, +) -> HsmResult> { + // Firmware evidence verification rejects an empty chain as InvalidArg; + // fail fast rather than round-trip a guaranteed rejection. + if chain.is_empty() || chain.len() > max_certs { + return Err(HsmError::InvalidArgument); + } + let mut descriptors = Vec::with_capacity(chain.len()); + for cert in chain { + let der = cert.cert; + if der.is_empty() || der.len() > u16::MAX as usize { + return Err(HsmError::InvalidArgument); + } + // Descriptor index = position in the shared OOB list (must fit u8). + let index = u8::try_from(oob.len()).map_err(|_| HsmError::InvalidArgument)?; + descriptors.push(CertDescriptor { + index, + length: tbor_int::U16::new(der.len() as u16), + }); + oob.push(der); + } + Ok(descriptors) +} + +/// Appends a COSE_Sign1 report DER to `oob`, returning its descriptor. +/// Rejects an empty report (firmware verification rejects `report_len == +/// 0` as InvalidArg) or one exceeding the 16-bit length. +pub(crate) fn push_report<'a>( + report: &'a [u8], + oob: &mut Vec<&'a [u8]>, +) -> HsmResult { + if report.is_empty() || report.len() > u16::MAX as usize { + return Err(HsmError::InvalidArgument); + } + let index = u8::try_from(oob.len()).map_err(|_| HsmError::InvalidArgument)?; + let descriptor = ReportDescriptor { + index, + length: tbor_int::U16::new(report.len() as u16), + }; + oob.push(report); + Ok(descriptor) +} diff --git a/api/lib/src/ddi/mod.rs b/api/lib/src/ddi/mod.rs index 348026e14..0ae7697e3 100644 --- a/api/lib/src/ddi/mod.rs +++ b/api/lib/src/ddi/mod.rs @@ -3,6 +3,7 @@ mod aes; mod aes_xts_key; +mod descriptor_utils; mod dev; mod ecc; mod hkdf; @@ -13,6 +14,9 @@ mod masked_key; mod partition; mod partition_ex; mod rsa; +mod sd_create_remote_backup; +mod sd_evidence; +mod sd_reseal_remote_backup; mod sd_sealing_key_gen; mod session; mod session_ex; @@ -23,14 +27,24 @@ pub(crate) use aes_xts_key::*; use azihsm_ddi::*; use azihsm_ddi_mbor_codec::*; use azihsm_ddi_mbor_types::*; +/// Maximum number of certificates in one SD-evidence certificate chain. +pub use azihsm_ddi_tbor_types::EVIDENCE_CHAIN_MAX_CERTS; /// Size, in bytes, of the `part_final` `local_mk_backup` envelope. pub use azihsm_ddi_tbor_types::LOCAL_MK_BACKUP_LEN; +/// Exact length of the local (masked) security-domain backup. +pub use azihsm_ddi_tbor_types::MASKED_SD_LEN; /// Maximum number of certificates in a `part_final` PTA chain. pub use azihsm_ddi_tbor_types::MAX_CERTS; +/// Exact length of the remote partition-owner-key backup (`SdCreate`/`SdReseal`). +pub use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; /// Maximum size, in bytes, of the `part_init` `pta_csr` buffer. pub use azihsm_ddi_tbor_types::PTA_CSR_MAX_LEN; /// Maximum size, in bytes, of the `part_init` `pta_report` buffer. pub use azihsm_ddi_tbor_types::PTA_REPORT_MAX_LEN; +/// Exact length of the security-domain masking-key backup envelope. +pub use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; +use azihsm_ddi_tbor_types::TborStatus; +pub(crate) use descriptor_utils::*; pub(crate) use dev::*; pub(crate) use ecc::*; pub(crate) use hkdf::*; @@ -41,6 +55,9 @@ pub(crate) use masked_key::*; pub(crate) use partition::*; pub(crate) use partition_ex::*; pub(crate) use rsa::*; +pub(crate) use sd_create_remote_backup::*; +pub(crate) use sd_evidence::*; +pub(crate) use sd_reseal_remote_backup::*; pub(crate) use sd_sealing_key_gen::*; pub(crate) use session::*; pub(crate) use session_ex::*; @@ -103,6 +120,13 @@ impl From for HsmError { DdiError::DdiStatus(DdiStatus::CannotDeleteInternalKeys) => { HsmError::CannotDeleteInternalKeys } + DdiError::TborStatus(TborStatus::SdAlreadyInitialized) => { + HsmError::SdAlreadyInitialized + } + // Map the firmware's contract-level `InvalidArg` to the same + // `InvalidArgument` the host guards return, so callers see a + // consistent argument-rejection error across transports. + DdiError::TborStatus(TborStatus::InvalidArg) => HsmError::InvalidArgument, _ => { tracing::error!(?err, hsm_error = ?HsmError::DdiCmdFailure, "Unmapped DDI error"); HsmError::DdiCmdFailure diff --git a/api/lib/src/ddi/partition_ex.rs b/api/lib/src/ddi/partition_ex.rs index 16a037f87..a8f8d5235 100644 --- a/api/lib/src/ddi/partition_ex.rs +++ b/api/lib/src/ddi/partition_ex.rs @@ -234,9 +234,6 @@ pub(crate) fn part_final_ex( if part_policy.len() != PART_POLICY_LEN { return Err(HsmError::InvalidArgument); } - if pta_cert_chain.is_empty() || pta_cert_chain.len() > MAX_CERTS { - return Err(HsmError::InvalidArgument); - } // The firmware treats a non-empty `prev_local_mk_backup` as a // fixed-size envelope of exactly `LOCAL_MK_BACKUP_LEN` bytes, so // reject any other present length up front (deterministic guard). @@ -244,27 +241,9 @@ pub(crate) fn part_final_ex( return Err(HsmError::InvalidArgument); } - // Each DER cert ships as its own out-of-band SGL Data Block; the - // firmware locates each one by the descriptor's `index` (its position - // in the OOB item list) and reads `length` bytes from it. + // Each DER cert ships as its own out-of-band SGL Data Block. let mut oob: Vec<&[u8]> = Vec::with_capacity(pta_cert_chain.len()); - let mut cert_descriptors = Vec::with_capacity(pta_cert_chain.len()); - for (i, desc) in pta_cert_chain.iter().enumerate() { - let cert = desc.cert; - let length = cert.len(); - // An empty cert is not valid DER and would yield a zero-length - // descriptor; reject it up front alongside the other - // deterministic host-side guards. - if length == 0 || length > u16::MAX as usize { - return Err(HsmError::InvalidArgument); - } - // `i` is bounded by the `MAX_CERTS` check above, so it fits `u8`. - cert_descriptors.push(CertDescriptor { - index: i as u8, - length: tbor_int::U16::new(length as u16), - }); - oob.push(cert); - } + let cert_descriptors = push_cert_chain(pta_cert_chain, &mut oob, MAX_CERTS)?; let mut req = TborPartFinalReq { session_id, @@ -293,6 +272,48 @@ pub(crate) fn part_final_ex( Ok(HsmPartFinalExResult::from(resp)) } +/// Partition identity returned by a `PartInfo` query: the stable PID plus +/// the raw ECC-P384 identity public key. +/// +/// DDI-internal carrier only. Public callers reach these fields through +/// the [`HsmPartition::pid`] / [`HsmPartition::ex_pub_key`] getters, so the +/// compound type never surfaces in the public API. +pub(crate) struct HsmPartInfo { + /// 16-byte partition identity (PID). + pub pid: Vec, + /// Raw ECC-P384 identity public-key coordinates (`x ‖ y`, 96 B). + pub pid_pub_key: Vec, +} + +/// Converts the DDI/wire `PartInfo` response into the DDI-internal +/// [`HsmPartInfo`] with owned bytes, keeping the wire response type +/// confined to the DDI layer. +impl From for HsmPartInfo { + fn from(resp: TborPartInfoResp) -> Self { + Self { + pid: resp.pid.to_vec(), + pid_pub_key: resp.pid_pub_key.to_vec(), + } + } +} + +/// Issue `PartInfo` on the partition, returning its identity (PID) and +/// raw identity public key. +/// +/// This is a partition-level query and carries no session id. +/// +/// # Errors +/// +/// Surfaces DDI/device failures from the round-trip. +pub(crate) fn part_info(partition: &HsmPartition) -> HsmResult { + let inner = partition.inner().read(); + let dev = inner.dev(); + let mut cookie = None; + dev.exec_op_tbor(&TborPartInfoReq::new(), None, &mut cookie) + .map(HsmPartInfo::from) + .map_err(HsmError::from) +} + #[cfg(test)] mod tests { use super::*; diff --git a/api/lib/src/ddi/sd_create_remote_backup.rs b/api/lib/src/ddi/sd_create_remote_backup.rs new file mode 100644 index 000000000..57923f525 --- /dev/null +++ b/api/lib/src/ddi/sd_create_remote_backup.rs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! `SdCreateRemoteBackup` (opcode `0x0A`) over the TBOR transport at the +//! DDI layer. +//! +//! Creates a new security domain from a caller-supplied unified +//! `PartPolicy`, returning the remote partition-owner-key backup together +//! with the device-local partition-owner-key and security-domain +//! masking-key backups. Runs **inside an already-open session**; the +//! request carries the active session id, which the firmware dispatcher +//! cross-checks against the SQE-carried session id. Attestation evidence +//! travels out of band (see [`super::sd_evidence`]). + +use azihsm_ddi_tbor_types::*; + +use super::*; + +/// Converts the wire `SdCreateRemoteBackup` response into the owned +/// API-layer [`HsmSdRemoteBackupResult`]. +impl From for HsmSdRemoteBackupResult { + fn from(resp: TborSdCreateRemoteBackupResp) -> Self { + Self { + pok_remote_backup: resp.pok_remote_backup.to_vec(), + pok_local_backup: resp.pok_local_backup.to_vec(), + sd_mk_backup: resp.sd_mk_backup.to_vec(), + } + } +} + +/// Issue `SdCreateRemoteBackup` (opcode `0x0A`) on the active session. +/// +/// Creates a new security domain from the caller-supplied unified +/// `policy`, using the sender's masked sealing key and the receiver's +/// attestation evidence, and returns the remote backup together with the +/// device-local backups. +/// +/// # Arguments +/// +/// * `partition` - The HSM partition handle. +/// * `session_id` - The active session id this request binds to. +/// * `masked_sealing_key` - The sender's masked SD-sealing key (from +/// `SdSealingKeyGen`), exactly [`MASKED_SEALING_KEY_LEN`] bytes. +/// * `receiver_evidence` - Receiver attestation evidence (cert chains and +/// report), transmitted out of band. +/// * `policy` - Unified [`PartPolicy`] image ([`PART_POLICY_LEN`] bytes). +/// +/// # Errors +/// +/// Returns [`HsmError::InvalidArgument`] for a wrong-length +/// `masked_sealing_key` or a malformed `policy`, and surfaces DDI/device +/// failures from the round-trip. +pub(crate) fn sd_create_remote_backup_ex( + partition: &HsmPartition, + session_id: u16, + masked_sealing_key: &[u8], + receiver_evidence: &HsmSdEvidence<'_>, + policy: &[u8], +) -> HsmResult { + // Exact-length array conversion enforces MASKED_SEALING_KEY_LEN. + let masked_sealing_key: [u8; MASKED_SEALING_KEY_LEN] = masked_sealing_key + .try_into() + .map_err(|_| HsmError::InvalidArgument)?; + let policy = decode_policy(policy)?; + + // Flatten the receiver evidence into descriptors + shared OOB items. + let mut oob: Vec<&[u8]> = Vec::new(); + let receiver = push_evidence(receiver_evidence, &mut oob)?; + + let req = TborSdCreateRemoteBackupReq { + session_id, + masked_sealing_key, + receiver_mfgr_cert_chain: receiver.mfgr, + receiver_owner_cert_chain: receiver.owner, + receiver_part_owner_cert_chain: receiver.part_owner, + receiver_report: receiver.report, + policy, + }; + + let inner = partition.inner().read(); + let dev = inner.dev(); + let mut cookie = None; + let oob_items = (!oob.is_empty()).then_some(oob.as_slice()); + dev.exec_op_tbor(&req, oob_items, &mut cookie) + .map(HsmSdRemoteBackupResult::from) + .map_err(HsmError::from) +} diff --git a/api/lib/src/ddi/sd_evidence.rs b/api/lib/src/ddi/sd_evidence.rs new file mode 100644 index 000000000..03351ed27 --- /dev/null +++ b/api/lib/src/ddi/sd_evidence.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared helpers for the security-domain backup commands +//! (`SdCreateRemoteBackup`, `SdResealRemoteBackup`). +//! +//! # Out-of-band evidence +//! +//! Bulk attestation evidence (DER cert chains and a COSE_Sign1 report) +//! travels out of band: each [`HsmSdEvidence`] is flattened into +//! `(index, length)` descriptors (via [`super::descriptor_utils`]) and its +//! DER bytes +//! are shipped as `oob_items`, mirroring the `PartFinal` cert-chain +//! transport. + +use azihsm_ddi_tbor_types::*; + +use super::*; + +/// Decodes a caller-supplied unified `PartPolicy` image +/// ([`PART_POLICY_LEN`] bytes), failing fast with +/// [`HsmError::InvalidArgument`] on a wrong length or malformed image. +pub(crate) fn decode_policy(policy: &[u8]) -> HsmResult { + if policy.len() != PART_POLICY_LEN { + return Err(HsmError::InvalidArgument); + } + ::try_read_from_bytes(policy) + .map_err(|_| HsmError::InvalidArgument) +} + +/// Wire descriptors for one attestation party: the three cert-chain +/// lists plus the report descriptor. Produced by [`push_evidence`]. +pub(crate) struct EvidenceDescriptors { + pub(crate) mfgr: Vec, + pub(crate) owner: Vec, + pub(crate) part_owner: Vec, + pub(crate) report: ReportDescriptor, +} + +/// Flattens one [`HsmSdEvidence`] party into its wire descriptors, +/// appending all referenced DER bytes (the three cert chains, then the +/// report) to the shared `oob` list so their descriptor indices are +/// contiguous. +pub(crate) fn push_evidence<'a>( + evidence: &HsmSdEvidence<'a>, + oob: &mut Vec<&'a [u8]>, +) -> HsmResult { + Ok(EvidenceDescriptors { + mfgr: push_cert_chain(evidence.mfgr_cert_chain, oob, EVIDENCE_CHAIN_MAX_CERTS)?, + owner: push_cert_chain(evidence.owner_cert_chain, oob, EVIDENCE_CHAIN_MAX_CERTS)?, + part_owner: push_cert_chain( + evidence.part_owner_cert_chain, + oob, + EVIDENCE_CHAIN_MAX_CERTS, + )?, + report: push_report(evidence.report, oob)?, + }) +} diff --git a/api/lib/src/ddi/sd_reseal_remote_backup.rs b/api/lib/src/ddi/sd_reseal_remote_backup.rs new file mode 100644 index 000000000..d7d073064 --- /dev/null +++ b/api/lib/src/ddi/sd_reseal_remote_backup.rs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! `SdResealRemoteBackup` (opcode `0x0B`) over the TBOR transport at the +//! DDI layer. +//! +//! Reseals an existing remote backup from a source recipient to a +//! destination recipient. Runs **inside an already-open session**; the +//! request carries the active session id, which the firmware dispatcher +//! cross-checks against the SQE-carried session id. Attestation evidence +//! travels out of band (see [`super::sd_evidence`]). + +use azihsm_ddi_tbor_types::*; + +use super::*; + +/// Issue `SdResealRemoteBackup` (opcode `0x0B`) on the active session. +/// +/// HPKE-opens `src_remote_backup` with the receiver's `masked_sealing_key` +/// (authenticated by the source sender key in `src_evidence`) and reseals +/// the recovered backup to the destination receiver (`dest_evidence`), +/// returning the resealed remote backup. +/// +/// # Arguments +/// +/// * `partition` - The HSM partition handle. +/// * `session_id` - The active session id this request binds to. +/// * `masked_sealing_key` - The receiver's masked SD-sealing key (from +/// `SdSealingKeyGen`) that unseals the source backup, exactly +/// [`MASKED_SEALING_KEY_LEN`] bytes. +/// * `src_evidence` - Source (sender) attestation evidence, transmitted +/// out of band. +/// * `dest_evidence` - Destination (receiver) attestation evidence. +/// * `policy` - Unified [`PartPolicy`] image ([`PART_POLICY_LEN`] bytes). +/// * `src_remote_backup` - The source remote backup to reseal, exactly +/// [`POK_REMOTE_BACKUP_LEN`] bytes. +/// +/// # Errors +/// +/// Returns [`HsmError::InvalidArgument`] for a wrong-length +/// `masked_sealing_key` or `src_remote_backup`, or a malformed `policy`, +/// and surfaces DDI/device failures from the round-trip. +pub(crate) fn sd_reseal_remote_backup_ex( + partition: &HsmPartition, + session_id: u16, + masked_sealing_key: &[u8], + src_evidence: &HsmSdEvidence<'_>, + dest_evidence: &HsmSdEvidence<'_>, + policy: &[u8], + src_remote_backup: &[u8], +) -> HsmResult> { + let masked_sealing_key: [u8; MASKED_SEALING_KEY_LEN] = masked_sealing_key + .try_into() + .map_err(|_| HsmError::InvalidArgument)?; + let src_remote_backup: [u8; POK_REMOTE_BACKUP_LEN] = src_remote_backup + .try_into() + .map_err(|_| HsmError::InvalidArgument)?; + let policy = decode_policy(policy)?; + + // Both parties' evidence share one OOB list; push the source first, + // then the destination, so descriptor indices stay contiguous. + let mut oob: Vec<&[u8]> = Vec::new(); + let src = push_evidence(src_evidence, &mut oob)?; + let dest = push_evidence(dest_evidence, &mut oob)?; + + let req = TborSdResealRemoteBackupReq { + session_id, + masked_sealing_key, + policy, + src_mfgr_cert_chain: src.mfgr, + src_owner_cert_chain: src.owner, + src_part_owner_cert_chain: src.part_owner, + src_report: src.report, + dest_mfgr_cert_chain: dest.mfgr, + dest_owner_cert_chain: dest.owner, + dest_part_owner_cert_chain: dest.part_owner, + dest_report: dest.report, + src_remote_backup, + }; + + let inner = partition.inner().read(); + let dev = inner.dev(); + let mut cookie = None; + let oob_items = (!oob.is_empty()).then_some(oob.as_slice()); + dev.exec_op_tbor(&req, oob_items, &mut cookie) + .map(|resp| resp.dst_remote_backup.to_vec()) + .map_err(HsmError::from) +} diff --git a/api/lib/src/ddi/sd_sealing_key_gen.rs b/api/lib/src/ddi/sd_sealing_key_gen.rs index 232ca13d8..f4fba484c 100644 --- a/api/lib/src/ddi/sd_sealing_key_gen.rs +++ b/api/lib/src/ddi/sd_sealing_key_gen.rs @@ -13,6 +13,10 @@ //! not stored on the device; the masked blob is returned to the host //! and unmasked on-use by the security-domain backup commands. //! +//! It also hosts the companion **`KeyReport`** dispatch, which attests a +//! non-resident masked key (the sealing key) by its masked-key envelope +//! rather than a device handle. +//! //! It runs **inside an already-open session** established by //! [`super::session_ex::open_session_ex`]: the request carries the //! active session id, which the firmware dispatcher cross-checks @@ -136,6 +140,74 @@ pub(crate) fn sd_sealing_key_gen( Ok((resp.masked_key, pub_key_der)) } +/// Issue `KeyReport` on the active session to attest a non-resident +/// masked key (such as the sealing key produced by +/// [`sd_sealing_key_gen`]). +/// +/// The key is attested by its masked-key envelope rather than a device +/// handle. Ships the active session id, the masked-key envelope, and the +/// caller's [`KEY_REPORT_DATA_LEN`]-byte report data; returns the tagged +/// COSE_Sign1 attestation report signed by the PID key. +/// +/// Follows the size-query convention: a `None` `report` returns the +/// maximum report size ([`KEY_REPORT_MAX_LEN`]) without a round-trip. +/// +/// # Arguments +/// +/// * `session` - The active security-domain (V2) session. +/// * `masked_key` - The masked-key envelope to attest (at most +/// [`KEY_REPORT_MASKED_KEY_MAX_LEN`] bytes). +/// * `report_data` - Caller-supplied [`KEY_REPORT_DATA_LEN`]-byte report +/// data bound into the report. +/// * `report` - Optional output buffer for the report; `None` returns the +/// maximum report size. +/// +/// # Errors +/// +/// Returns [`HsmError::InvalidArgument`] when `report_data` is not +/// [`KEY_REPORT_DATA_LEN`] bytes or `masked_key` is empty or exceeds +/// [`KEY_REPORT_MASKED_KEY_MAX_LEN`], [`HsmError::BufferTooSmall`] when +/// the supplied buffer is shorter than the returned report, and surfaces +/// DDI/device failures from the round-trip. +pub(crate) fn masked_key_report( + session: &HsmSession, + masked_key: &[u8], + report_data: &[u8], + report: Option<&mut [u8]>, +) -> HsmResult { + if report_data.len() != KEY_REPORT_DATA_LEN + || masked_key.is_empty() + || masked_key.len() > KEY_REPORT_MASKED_KEY_MAX_LEN + { + return Err(HsmError::InvalidArgument); + } + + let Some(report) = report else { + return Ok(KEY_REPORT_MAX_LEN); + }; + + let mut report_data_arr = [0u8; KEY_REPORT_DATA_LEN]; + report_data_arr.copy_from_slice(report_data); + + let req = TborKeyReportReq { + session_id: session.ex_session_id()?, + masked_key: masked_key.to_vec(), + report_data: report_data_arr, + }; + + let mut cookie = None; + let resp = session.with_dev(|dev| { + dev.exec_op_tbor(&req, None, &mut cookie) + .map_err(HsmError::from) + })?; + + if report.len() < resp.report.len() { + return Err(HsmError::BufferTooSmall); + } + report[..resp.report.len()].copy_from_slice(&resp.report); + Ok(resp.report.len()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/api/lib/src/error.rs b/api/lib/src/error.rs index 80607fea1..091bd0824 100644 --- a/api/lib/src/error.rs +++ b/api/lib/src/error.rs @@ -65,6 +65,7 @@ pub enum HsmError { InvalidContextState = -40, Bk3AlreadyInitialized = -41, InvalidSession = -42, + SdAlreadyInitialized = -43, Panic = i32::MIN, } diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 426fb673f..d99655047 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -12,10 +12,14 @@ mod shared_types; pub mod traits; pub use algo::*; +pub use ddi::EVIDENCE_CHAIN_MAX_CERTS; pub use ddi::LOCAL_MK_BACKUP_LEN; +pub use ddi::MASKED_SD_LEN; pub use ddi::MAX_CERTS; +pub use ddi::POK_REMOTE_BACKUP_LEN; pub use ddi::PTA_CSR_MAX_LEN; pub use ddi::PTA_REPORT_MAX_LEN; +pub use ddi::SD_MK_BACKUP_LEN; pub use error::*; pub use op::*; pub use partition::*; diff --git a/api/lib/src/partition.rs b/api/lib/src/partition.rs index dce52844f..299fd440c 100644 --- a/api/lib/src/partition.rs +++ b/api/lib/src/partition.rs @@ -841,6 +841,26 @@ impl HsmPartition { ddi::get_cert_chain(self, slot) } + /// Queries the partition's stable identity (PID) via `PartInfo`. + /// + /// Returns the 16-byte PID, which a caller uses to name this + /// partition as the backing partition when building a + /// security-domain backup policy. + pub fn pid(&self) -> HsmResult> { + Ok(ddi::part_info(self)?.pid) + } + + /// Queries the partition's raw identity public key via `PartInfo`. + /// + /// Returns the raw ECC-P384 identity public-key coordinates + /// (`x ‖ y`, 96 B). Unlike [`Self::pub_key`] — which returns the + /// DER-encoded public key parsed from the PID certificate — this + /// comes straight from the `PartInfo` query and is available before + /// the partition is endorsed. + pub fn ex_pub_key(&self) -> HsmResult> { + Ok(ddi::part_info(self)?.pid_pub_key) + } + /// Retrieves the public key of the partition identity (PID) certificate. /// /// # Returns diff --git a/api/lib/src/session.rs b/api/lib/src/session.rs index b50b1e84e..9be7061e4 100644 --- a/api/lib/src/session.rs +++ b/api/lib/src/session.rs @@ -220,6 +220,66 @@ impl HsmSession { SessionKind::Ver1 { .. } => Err(HsmError::InvalidSession), } } + + /// Issues TBOR `SdCreateRemoteBackup` (opcode `0x0A`) on this CO + /// session. + /// + /// Creates a new security domain from the caller-supplied unified + /// `policy`, using the sender's `masked_sealing_key` (from + /// `SdSealingKeyGen`) and the receiver's attestation `evidence`. + /// Returns the remote backup together with the device-local backups. + /// Only valid on a V2 session; a V1 session returns + /// [`HsmError::InvalidSession`]. + pub fn sd_create_remote_backup( + &self, + masked_sealing_key: &[u8], + receiver_evidence: &HsmSdEvidence<'_>, + policy: &[u8], + ) -> HsmResult { + let inner = self.inner.read(); + match &inner.kind { + SessionKind::Ver2 { .. } => ddi::sd_create_remote_backup_ex( + &inner.partition, + inner.id, + masked_sealing_key, + receiver_evidence, + policy, + ), + SessionKind::Ver1 { .. } => Err(HsmError::InvalidSession), + } + } + + /// Issues TBOR `SdResealRemoteBackup` (opcode `0x0B`) on this CO + /// session. + /// + /// HPKE-opens `src_remote_backup` with the receiver's + /// `masked_sealing_key` (authenticated by the source sender in + /// `src_evidence`) and reseals the recovered backup to the destination + /// receiver (`dest_evidence`), returning the resealed remote backup. + /// Only valid on a V2 session; a V1 session returns + /// [`HsmError::InvalidSession`]. + pub fn sd_reseal_remote_backup( + &self, + masked_sealing_key: &[u8], + src_evidence: &HsmSdEvidence<'_>, + dest_evidence: &HsmSdEvidence<'_>, + policy: &[u8], + src_remote_backup: &[u8], + ) -> HsmResult> { + let inner = self.inner.read(); + match &inner.kind { + SessionKind::Ver2 { .. } => ddi::sd_reseal_remote_backup_ex( + &inner.partition, + inner.id, + masked_sealing_key, + src_evidence, + dest_evidence, + policy, + src_remote_backup, + ), + SessionKind::Ver1 { .. } => Err(HsmError::InvalidSession), + } + } } /// Transport-specific session state. diff --git a/api/lib/src/shared_types.rs b/api/lib/src/shared_types.rs index b60d59b5f..413b52665 100644 --- a/api/lib/src/shared_types.rs +++ b/api/lib/src/shared_types.rs @@ -39,6 +39,40 @@ pub struct HsmPartFinalExResult { pub local_mk_backup: Vec, } +/// Borrowed attestation evidence for one party in a security-domain +/// backup: the three DER certificate chains and the COSE_Sign1 report. +/// +/// The DER bytes travel out of band; the SDK builds the wire descriptors +/// internally. +#[derive(Debug, Clone, Copy)] +pub struct HsmSdEvidence<'a> { + /// Manufacturer certificate chain. + pub mfgr_cert_chain: &'a [HsmCert<'a>], + /// Owner certificate chain. + pub owner_cert_chain: &'a [HsmCert<'a>], + /// Partition-owner certificate chain. + pub part_owner_cert_chain: &'a [HsmCert<'a>], + /// COSE_Sign1 attestation report (DER/COSE bytes). + pub report: &'a [u8], +} + +/// Result of `sd_create_remote_backup`: the three backups the device +/// returns after creating a security domain. +/// +/// API-layer type with owned bytes. The DDI/wire response type +/// (`TborSdCreateRemoteBackupResp`) is converted into it inside the DDI +/// layer, so the wire type never surfaces to public callers. +#[derive(Debug, Clone, Default)] +pub struct HsmSdRemoteBackupResult { + /// Remote partition-owner-key backup (HPKE-Auth seal of BKS3). + pub pok_remote_backup: Vec, + /// Local partition-owner-key backup (BKS3 masked under the + /// partition-local masking key). + pub pok_local_backup: Vec, + /// Security-domain masking-key backup envelope. + pub sd_mk_backup: Vec, +} + /// Cryptographic key class. /// /// Defines the fundamental category of a cryptographic key. diff --git a/api/native/cbindgen.toml b/api/native/cbindgen.toml index 7e59ffd84..da5044e38 100644 --- a/api/native/cbindgen.toml +++ b/api/native/cbindgen.toml @@ -49,8 +49,12 @@ include = [ "AzihsmResiliencyConfig", "AzihsmResiliencyLockOps", "AzihsmResiliencyStorageOps", + "AzihsmSdCertChain", + "AzihsmSdEvidence", "AzihsmSessExPartFinalParams", "AzihsmSessExPartInitParams", + "AzihsmSessExSdCreateRemoteBackupParams", + "AzihsmSessExSdResealRemoteBackupParams", "AzihsmSessionPsk", "HsmEccCurve", "HsmError", @@ -102,8 +106,12 @@ item_types = ["constants", "enums", "functions", "structs", "typedefs"] "AzihsmResiliencyConfig" = "azihsm_resiliency_config" "AzihsmResiliencyLockOps" = "azihsm_resiliency_lock_ops" "AzihsmResiliencyStorageOps" = "azihsm_resiliency_storage_ops" +"AzihsmSdCertChain" = "azihsm_sd_cert_chain" +"AzihsmSdEvidence" = "azihsm_sd_evidence" "AzihsmSessExPartFinalParams" = "azihsm_sess_ex_part_final_params" "AzihsmSessExPartInitParams" = "azihsm_sess_ex_part_init_params" +"AzihsmSessExSdCreateRemoteBackupParams" = "azihsm_sess_ex_sd_create_remote_backup_params" +"AzihsmSessExSdResealRemoteBackupParams" = "azihsm_sess_ex_sd_reseal_remote_backup_params" "AzihsmSessionExType" = "azihsm_session_ex_type" "AzihsmSessionProp" = "azihsm_session_prop" "AzihsmSessionPropId" = "azihsm_session_prop_id" diff --git a/api/native/doc/chapter_09_sd.md b/api/native/doc/chapter_09_sd.md index b9678311c..e6069389b 100644 --- a/api/native/doc/chapter_09_sd.md +++ b/api/native/doc/chapter_09_sd.md @@ -143,7 +143,7 @@ Finalize a partition's security domain over a security-domain session. Completes provisioning started by [`azihsm_sess_ex_part_init`](#azihsm_sess_ex_part_init): re-supplies the -unified partition policy and the PTA certificate chain (leaf to root), +unified partition policy and the PTA certificate chain (root to leaf), optionally restoring a prior `local_mk` backup, and returns the current `local_mk` backup envelope the firmware produced. @@ -182,7 +182,7 @@ azihsm_status azihsm_sess_ex_part_final( Finalization input buffers for [`azihsm_sess_ex_part_final`](#azihsm_sess_ex_part_final). `pta_cert_chain` points to an array of `pta_cert_chain_len` [azihsm_buffer](#azihsm_buffer)s, -each holding one DER-encoded PTA certificate (leaf to root; at most +each holding one DER-encoded PTA certificate (root to leaf; at most `MAX_CERTS`). `prev_local_mk_backup` is optional and may be NULL to omit it. ```cpp @@ -197,7 +197,7 @@ struct azihsm_sess_ex_part_final_params { | Field | Name | Description | | -------------------- | -------------------------------- | ----------------------------------------------- | | part_policy | [azihsm_buffer*](#azihsm_buffer) | unified partition policy (must match part_init) | - | pta_cert_chain | [azihsm_buffer*](#azihsm_buffer) | array of DER PTA certificates (leaf to root) | + | pta_cert_chain | [azihsm_buffer*](#azihsm_buffer) | array of DER PTA certificates (root to leaf) | | pta_cert_chain_len | uint32_t | number of certificates in the chain | | prev_local_mk_backup | [azihsm_buffer*](#azihsm_buffer) | optional prior local_mk backup (may be NULL) | @@ -228,3 +228,168 @@ azihsm_status azihsm_sess_ex_psk_change( **Returns** `AZIHSM_STATUS_SUCCESS` on success, error code otherwise + +## azihsm_sess_ex_sd_create_remote_backup + +Create a new security domain and its remote backup over a security-domain +session. + +Creates a security domain under the calling session's partition from the +unified partition policy, using the sender's masked SD-sealing key (from +`azihsm_key_gen`) and the receiver's attestation evidence, and returns the +three backups the firmware produces: the remote partition-owner-key backup +(an HPKE-Auth seal of BKS3, 161 bytes), the local partition-owner-key backup +(180 bytes), and the security-domain masking-key backup (164 bytes). + +The inputs are grouped into an +[`azihsm_sess_ex_sd_create_remote_backup_params`](#azihsm_sess_ex_sd_create_remote_backup_params) +structure. All three output buffers follow the two-call size-probe contract: +an undersized buffer (or a NULL `ptr` with `len == 0`) is rejected with +`AZIHSM_STATUS_BUFFER_TOO_SMALL` and `len` set to the required size, and +every output buffer is validated **before** the one-shot domain-creation +command is issued, so a too-small buffer never consumes it. A NULL `params` +pointer is rejected with `AZIHSM_STATUS_INVALID_ARGUMENT`. Creating a domain +is a once-per-partition operation; a second create on an initialized +partition returns `AZIHSM_STATUS_SD_ALREADY_INITIALIZED`. + +```cpp +azihsm_status azihsm_sess_ex_sd_create_remote_backup( + azihsm_handle sess_handle, + const struct azihsm_sess_ex_sd_create_remote_backup_params *params, + struct azihsm_buffer *pok_remote_backup, + struct azihsm_buffer *pok_local_backup, + struct azihsm_buffer *sd_mk_backup + ); +``` + +**Parameters** + + | Parameter | Name | Description | + | --------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | + | [in] sess_handle | [azihsm_handle](#azihsm_handle) | security-domain session handle | + | [in] params | [azihsm_sess_ex_sd_create_remote_backup_params*](#azihsm_sess_ex_sd_create_remote_backup_params) | create-backup input buffers | + | [in, out] pok_remote_backup | [azihsm_buffer *](#azihsm_buffer) | output buffer for the remote pok backup (161 B) | + | [in, out] pok_local_backup | [azihsm_buffer *](#azihsm_buffer) | output buffer for the local pok backup (180 B) | + | [in, out] sd_mk_backup | [azihsm_buffer *](#azihsm_buffer) | output buffer for the sd masking-key backup (164 B)   | + +**Returns** + +`AZIHSM_STATUS_SUCCESS` on success, error code otherwise + +### azihsm_sess_ex_sd_create_remote_backup_params + +Input buffers for +[`azihsm_sess_ex_sd_create_remote_backup`](#azihsm_sess_ex_sd_create_remote_backup). + +```cpp +struct azihsm_sess_ex_sd_create_remote_backup_params { + const struct azihsm_buffer *masked_sealing_key; + const struct azihsm_sd_evidence *receiver_evidence; + const struct azihsm_buffer *policy; +}; +``` + + | Field | Name | Description | + | ------------------ | ------------------------------------------ | -------------------------------------------------- | + | masked_sealing_key | [azihsm_buffer*](#azihsm_buffer) | sender's masked SD-sealing key (180 B) | + | receiver_evidence | [azihsm_sd_evidence*](#azihsm_sd_evidence) | receiver attestation evidence | + | policy | [azihsm_buffer*](#azihsm_buffer) | unified partition-policy image (484 B) | + +## azihsm_sess_ex_sd_reseal_remote_backup + +Reseal an existing remote backup to a new recipient over a security-domain +session. + +HPKE-opens the source remote backup with the receiver's masked SD-sealing +key (authenticated by the source sender in `src_evidence`) and reseals the +recovered backup to the destination receiver (`dest_evidence`), returning the +resealed remote backup (161 bytes). + +The inputs are grouped into an +[`azihsm_sess_ex_sd_reseal_remote_backup_params`](#azihsm_sess_ex_sd_reseal_remote_backup_params) +structure. `dst_remote_backup` follows the same two-call size-probe contract +as the create outputs and is validated **before** the reseal is performed. A +NULL `params` pointer is rejected with `AZIHSM_STATUS_INVALID_ARGUMENT`. + +```cpp +azihsm_status azihsm_sess_ex_sd_reseal_remote_backup( + azihsm_handle sess_handle, + const struct azihsm_sess_ex_sd_reseal_remote_backup_params *params, + struct azihsm_buffer *dst_remote_backup + ); +``` + +**Parameters** + + | Parameter | Name | Description | + | --------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | + | [in] sess_handle | [azihsm_handle](#azihsm_handle) | security-domain session handle | + | [in] params | [azihsm_sess_ex_sd_reseal_remote_backup_params*](#azihsm_sess_ex_sd_reseal_remote_backup_params) | reseal-backup input buffers | + | [in, out] dst_remote_backup | [azihsm_buffer *](#azihsm_buffer) | output buffer for the resealed remote backup (161 B)   | + +**Returns** + +`AZIHSM_STATUS_SUCCESS` on success, error code otherwise + +### azihsm_sess_ex_sd_reseal_remote_backup_params + +Input buffers for +[`azihsm_sess_ex_sd_reseal_remote_backup`](#azihsm_sess_ex_sd_reseal_remote_backup). + +```cpp +struct azihsm_sess_ex_sd_reseal_remote_backup_params { + const struct azihsm_buffer *masked_sealing_key; + const struct azihsm_sd_evidence *src_evidence; + const struct azihsm_sd_evidence *dest_evidence; + const struct azihsm_buffer *policy; + const struct azihsm_buffer *src_remote_backup; +}; +``` + + | Field | Name | Description | + | ------------------ | ------------------------------------------ | -------------------------------------------------- | + | masked_sealing_key | [azihsm_buffer*](#azihsm_buffer) | receiver's masked SD-sealing key (180 B) | + | src_evidence | [azihsm_sd_evidence*](#azihsm_sd_evidence) | source (sender) attestation evidence | + | dest_evidence | [azihsm_sd_evidence*](#azihsm_sd_evidence) | destination (receiver) attestation evidence | + | policy | [azihsm_buffer*](#azihsm_buffer) | unified partition-policy image (484 B) | + | src_remote_backup | [azihsm_buffer*](#azihsm_buffer) | source remote backup to reseal (161 B) | + +### azihsm_sd_evidence + +Attestation evidence for one security-domain-backup party: three certificate +chains (manufacturer, owner, partition-owner) and a COSE_Sign1 attestation +report. The DER bytes are borrowed, not copied, and must outlive the call. + +```cpp +struct azihsm_sd_evidence { + struct azihsm_sd_cert_chain mfgr_cert_chain; + struct azihsm_sd_cert_chain owner_cert_chain; + struct azihsm_sd_cert_chain part_owner_cert_chain; + const struct azihsm_buffer *report; +}; +``` + + | Field | Name | Description | + | --------------------- | -------------------------------------------- | -------------------------------------------- | + | mfgr_cert_chain | [azihsm_sd_cert_chain](#azihsm_sd_cert_chain) | manufacturer certificate chain | + | owner_cert_chain | [azihsm_sd_cert_chain](#azihsm_sd_cert_chain) | owner certificate chain | + | part_owner_cert_chain | [azihsm_sd_cert_chain](#azihsm_sd_cert_chain) | partition-owner certificate chain | + | report | [azihsm_buffer*](#azihsm_buffer) | COSE_Sign1 attestation report | + +### azihsm_sd_cert_chain + +One certificate chain in an SD attestation-evidence party: an array of `len` +[azihsm_buffer](#azihsm_buffer)s, each holding one DER-encoded certificate +ordered root to leaf (at most `EVIDENCE_CHAIN_MAX_CERTS`). + +```cpp +struct azihsm_sd_cert_chain { + const struct azihsm_buffer *certs; + uint32_t len; +}; +``` + + | Field | Name | Description | + | ----- | -------------------------------- | --------------------------------------------- | + | certs | [azihsm_buffer*](#azihsm_buffer) | array of DER certificates (root to leaf) | + | len | uint32_t | number of certificates in the chain | diff --git a/api/native/src/algo/sealing/key.rs b/api/native/src/algo/sealing/key.rs index 024f89428..f405c0f9a 100644 --- a/api/native/src/algo/sealing/key.rs +++ b/api/native/src/algo/sealing/key.rs @@ -4,11 +4,13 @@ use azihsm_api::*; use super::*; +use crate::AzihsmBuffer; use crate::AzihsmHandle; use crate::AzihsmStatus; use crate::HANDLE_TABLE; use crate::handle_table::HandleType; use crate::utils::validate_algo_params_absent; +use crate::utils::validate_output_buffer; impl TryFrom<&AzihsmAlgo> for HsmSealingKeyGenAlgo { type Error = AzihsmStatus; @@ -42,3 +44,21 @@ pub(crate) fn sealing_generate_key( let key = HsmKeyManager::generate_key(session, &mut sealing_algo, key_props)?; Ok(HANDLE_TABLE.alloc_handle(HandleType::SealingKey, Box::new(key))) } + +/// Generates a key-attestation report for a non-resident sealing key. +/// +/// The report is produced over the key's masked-key envelope via TBOR +/// `KeyReport`. Follows the probe/fill convention: sizes `output` on the +/// first pass, then fills it. +pub(crate) fn sealing_generate_key_report( + key_handle: AzihsmHandle, + report_data: &[u8], + output: &mut AzihsmBuffer, +) -> Result<(), AzihsmStatus> { + let key = HsmSealingKey::try_from(key_handle)?; + let required_size = HsmKeyManager::generate_key_report(&key, report_data, None)?; + let output_data = validate_output_buffer(output, required_size)?; + let report_len = HsmKeyManager::generate_key_report(&key, report_data, Some(output_data))?; + output.len = report_len as u32; + Ok(()) +} diff --git a/api/native/src/key_mgmt.rs b/api/native/src/key_mgmt.rs index 581de5687..104e4e3ca 100644 --- a/api/native/src/key_mgmt.rs +++ b/api/native/src/key_mgmt.rs @@ -483,6 +483,9 @@ pub unsafe extern "C" fn azihsm_generate_key_report( HandleType::RsaPrivKey => { rsa_generate_key_report(key_handle, report_data_buf, report_buf)?; } + HandleType::SealingKey => { + sealing_generate_key_report(key_handle, report_data_buf, report_buf)?; + } _ => Err(AzihsmStatus::UnsupportedKeyKind)?, } diff --git a/api/native/src/partition_props.rs b/api/native/src/partition_props.rs index 56f5cf069..2b21b7c31 100644 --- a/api/native/src/partition_props.rs +++ b/api/native/src/partition_props.rs @@ -68,6 +68,14 @@ pub enum AzihsmPartPropId { /// Partition identity (PID) public key in DER format. // Corresponds to AZIHSM_PART_PROP_ID_PART_PUB_KEY PartPubKey = 12, + + /// 16-byte partition identity (PID). + // Corresponds to AZIHSM_PART_PROP_ID_PART_EX_PID + PartExPid = 13, + + /// Raw ECC-P384 partition identity public key (`x ‖ y`, 96 bytes). + // Corresponds to AZIHSM_PART_PROP_ID_PART_EX_PUB_KEY + PartExPubKey = 14, } /// UUID structure. @@ -195,6 +203,14 @@ fn get_partition_prop( let pub_key = partition.pub_key()?; copy_to_part_prop(part_prop, &pub_key) } + AzihsmPartPropId::PartExPid => { + let pid = partition.pid()?; + copy_to_part_prop(part_prop, &pid) + } + AzihsmPartPropId::PartExPubKey => { + let ex_pub_key = partition.ex_pub_key()?; + copy_to_part_prop(part_prop, &ex_pub_key) + } _ => Err(AzihsmStatus::UnsupportedProperty), } } diff --git a/api/native/src/session_ex.rs b/api/native/src/session_ex.rs index 27f6accbc..7a550e347 100644 --- a/api/native/src/session_ex.rs +++ b/api/native/src/session_ex.rs @@ -243,7 +243,7 @@ pub unsafe extern "C" fn azihsm_sess_ex_part_init( /// Groups the security-domain finalization inputs into a single struct so /// the call site does not pass them as separate arguments. `pta_cert_chain` /// points to an array of `pta_cert_chain_len` `azihsm_buffer`s, each holding -/// one DER-encoded PTA certificate (leaf to root). `prev_local_mk_backup` is +/// one DER-encoded PTA certificate (root to leaf). `prev_local_mk_backup` is /// optional and may be NULL to omit it. #[repr(C)] pub struct AzihsmSessExPartFinalParams { @@ -251,7 +251,7 @@ pub struct AzihsmSessExPartFinalParams { /// recovery; must match the policy given to `part_init`. pub part_policy: *const AzihsmBuffer, /// Pointer to an array of `pta_cert_chain_len` `azihsm_buffer`s, each a - /// DER-encoded PTA certificate (leaf to root). + /// DER-encoded PTA certificate (root to leaf). pub pta_cert_chain: *const AzihsmBuffer, /// Number of certificates in `pta_cert_chain`. pub pta_cert_chain_len: u32, @@ -262,7 +262,7 @@ pub struct AzihsmSessExPartFinalParams { /// @brief Finalize a partition's security domain /// /// Completes provisioning started by `azihsm_sess_ex_part_init`: re-supplies -/// the unified partition policy and the PTA certificate chain (leaf to root), +/// the unified partition policy and the PTA certificate chain (root to leaf), /// optionally restoring a prior `local_mk` backup, and returns the current /// `local_mk` backup envelope the firmware produced. /// @@ -317,9 +317,10 @@ pub unsafe extern "C" fn azihsm_sess_ex_part_final( Err(AzihsmStatus::InvalidArgument)?; } validate_ptr(params.pta_cert_chain)?; - // SAFETY: the caller guarantees `pta_cert_chain` points to - // `chain_len` valid `azihsm_buffer`s (documented above), and - // `chain_len` is bounded by `MAX_CERTS` above. + // SAFETY: `pta_cert_chain` is non-null and aligned for + // `AzihsmBuffer` (checked by `validate_ptr`), the caller guarantees + // it points to `chain_len` valid `azihsm_buffer`s (documented + // above), and `chain_len` is bounded by `MAX_CERTS`. let raw = unsafe { std::slice::from_raw_parts(params.pta_cert_chain, chain_len) }; let mut certs: Vec> = Vec::with_capacity(chain_len); for buf in raw { @@ -377,3 +378,240 @@ pub unsafe extern "C" fn azihsm_sess_ex_psk_change( Ok(()) }) } + +/// One certificate chain in an SD attestation-evidence party: an array of +/// `len` `azihsm_buffer`s, each a DER-encoded certificate (root to leaf). +#[repr(C)] +pub struct AzihsmSdCertChain { + /// Pointer to an array of `len` DER certificate `azihsm_buffer`s. + pub certs: *const AzihsmBuffer, + /// Number of certificates in `certs`. + pub len: u32, +} + +/// Attestation evidence for one SD-backup party: three certificate chains +/// (manufacturer, owner, partition-owner) and a COSE_Sign1 report. The DER +/// bytes are borrowed, not copied. +#[repr(C)] +pub struct AzihsmSdEvidence { + /// Manufacturer certificate chain. + pub mfgr_cert_chain: AzihsmSdCertChain, + /// Owner certificate chain. + pub owner_cert_chain: AzihsmSdCertChain, + /// Partition-owner certificate chain. + pub part_owner_cert_chain: AzihsmSdCertChain, + /// COSE_Sign1 attestation-report buffer. + pub report: *const AzihsmBuffer, +} + +/// Input buffers for [`azihsm_sess_ex_sd_create_remote_backup`]. +#[repr(C)] +pub struct AzihsmSessExSdCreateRemoteBackupParams { + /// Sender's masked SD-sealing key (from `azihsm_key_gen`), exactly + /// `MASKED_SEALING_KEY_LEN` (180 B). + pub masked_sealing_key: *const AzihsmBuffer, + /// Receiver attestation evidence. + pub receiver_evidence: *const AzihsmSdEvidence, + /// Unified partition-policy image (484 B) describing the domain. + pub policy: *const AzihsmBuffer, +} + +/// Borrows one C [`AzihsmSdCertChain`] into a `Vec`, rejecting an +/// empty or oversized chain up front so a bogus `len` cannot trigger an +/// unbounded allocation ahead of the host validation. +#[allow(unsafe_code)] +fn unpack_cert_chain(chain: &AzihsmSdCertChain) -> Result>, AzihsmStatus> { + let len = chain.len as usize; + if len == 0 || len > api::EVIDENCE_CHAIN_MAX_CERTS { + Err(AzihsmStatus::InvalidArgument)?; + } + validate_ptr(chain.certs)?; + // SAFETY: `certs` is non-null and aligned for `AzihsmBuffer` (checked + // by `validate_ptr`), the caller guarantees it points to `len` valid + // `azihsm_buffer`s, and `len` is bounded by `EVIDENCE_CHAIN_MAX_CERTS`. + let raw = unsafe { std::slice::from_raw_parts(chain.certs, len) }; + let mut certs = Vec::with_capacity(len); + for buf in raw { + let der: &[u8] = buf.try_into()?; + certs.push(api::HsmCert { cert: der }); + } + Ok(certs) +} + +/// @brief Create a new security domain and its remote backup +/// +/// Creates a security domain under the calling session's partition from +/// `params.policy`, using the sender's masked sealing key and the +/// receiver's attestation evidence, and returns the three backups the +/// firmware produces. +/// +/// @param[in] sess_handle Handle to the security-domain session +/// @param[in] params Create-backup input buffers +/// @param[in,out] pok_remote_backup Output buffer for the remote +/// partition-owner-key backup (161 B). +/// @param[in,out] pok_local_backup Output buffer for the local +/// partition-owner-key backup (180 B). +/// @param[in,out] sd_mk_backup Output buffer for the security-domain +/// masking-key backup (164 B). +/// +/// All three output buffers follow the probe/fill convention and are +/// validated **before** the domain is created, so the one-shot command is +/// not consumed when a buffer is too small. +/// +/// @return `AzihsmStatus` indicating the result of the operation +/// +/// # Safety +/// +/// - `sess_handle` must be a valid security-domain session handle. +/// - `params` and each of its buffer/evidence pointers must be valid; each +/// `AzihsmSdCertChain.certs` must point to `len` valid `azihsm_buffer`s. +/// - Each output buffer must be a valid `azihsm_buffer` with writable +/// backing storage of the advertised length. +#[unsafe(no_mangle)] +#[allow(unsafe_code)] +pub unsafe extern "C" fn azihsm_sess_ex_sd_create_remote_backup( + sess_handle: AzihsmHandle, + params: *const AzihsmSessExSdCreateRemoteBackupParams, + pok_remote_backup: *mut AzihsmBuffer, + pok_local_backup: *mut AzihsmBuffer, + sd_mk_backup: *mut AzihsmBuffer, +) -> AzihsmStatus { + abi_boundary(|| { + let session = api::HsmSession::try_from(sess_handle)?; + let params = deref_ptr(params)?; + + let masked_sealing_key: &[u8] = deref_ptr(params.masked_sealing_key)?.try_into()?; + let policy: &[u8] = deref_ptr(params.policy)?.try_into()?; + + let ev = deref_ptr(params.receiver_evidence)?; + let mfgr = unpack_cert_chain(&ev.mfgr_cert_chain)?; + let owner = unpack_cert_chain(&ev.owner_cert_chain)?; + let part_owner = unpack_cert_chain(&ev.part_owner_cert_chain)?; + let report: &[u8] = deref_ptr(ev.report)?.try_into()?; + let receiver = api::HsmSdEvidence { + mfgr_cert_chain: &mfgr, + owner_cert_chain: &owner, + part_owner_cert_chain: &part_owner, + report, + }; + + // Validate all three output buffers before creating the domain so + // the one-shot command is not consumed on a too-small buffer. + validate_ptr(pok_remote_backup)?; + let pok_remote_backup = deref_mut_ptr(pok_remote_backup)?; + validate_output_buffer(pok_remote_backup, api::POK_REMOTE_BACKUP_LEN)?; + validate_ptr(pok_local_backup)?; + let pok_local_backup = deref_mut_ptr(pok_local_backup)?; + validate_output_buffer(pok_local_backup, api::MASKED_SD_LEN)?; + validate_ptr(sd_mk_backup)?; + let sd_mk_backup = deref_mut_ptr(sd_mk_backup)?; + validate_output_buffer(sd_mk_backup, api::SD_MK_BACKUP_LEN)?; + + let result = session.sd_create_remote_backup(masked_sealing_key, &receiver, policy)?; + + copy_to_buffer(pok_remote_backup, &result.pok_remote_backup)?; + copy_to_buffer(pok_local_backup, &result.pok_local_backup)?; + copy_to_buffer(sd_mk_backup, &result.sd_mk_backup)?; + + Ok(()) + }) +} + +/// Input buffers for [`azihsm_sess_ex_sd_reseal_remote_backup`]. +#[repr(C)] +pub struct AzihsmSessExSdResealRemoteBackupParams { + /// Receiver's masked SD-sealing key (from `azihsm_key_gen`) that + /// unseals the source backup, exactly `MASKED_SEALING_KEY_LEN` (180 B). + pub masked_sealing_key: *const AzihsmBuffer, + /// Source (sender) attestation evidence. + pub src_evidence: *const AzihsmSdEvidence, + /// Destination (receiver) attestation evidence. + pub dest_evidence: *const AzihsmSdEvidence, + /// Unified partition-policy image (484 B) describing the domain. + pub policy: *const AzihsmBuffer, + /// Source remote backup to reseal, exactly `POK_REMOTE_BACKUP_LEN` + /// (161 B). + pub src_remote_backup: *const AzihsmBuffer, +} + +/// @brief Reseal an existing remote backup to a new recipient +/// +/// HPKE-opens `params.src_remote_backup` with the receiver's masked +/// sealing key (authenticated by the source sender in `params.src_evidence`) +/// and reseals the recovered backup to the destination receiver +/// (`params.dest_evidence`), returning the resealed remote backup. +/// +/// @param[in] sess_handle Handle to the security-domain session +/// @param[in] params Reseal-backup input buffers +/// @param[in,out] dst_remote_backup Output buffer for the resealed remote +/// partition-owner-key backup (161 B). +/// +/// The output buffer follows the probe/fill convention and is validated +/// **before** the reseal is performed. +/// +/// @return `AzihsmStatus` indicating the result of the operation +/// +/// # Safety +/// +/// - `sess_handle` must be a valid security-domain session handle. +/// - `params` and each of its buffer/evidence pointers must be valid; each +/// `AzihsmSdCertChain.certs` must point to `len` valid `azihsm_buffer`s. +/// - `dst_remote_backup` must be a valid `azihsm_buffer` with writable +/// backing storage of the advertised length. +#[unsafe(no_mangle)] +#[allow(unsafe_code)] +pub unsafe extern "C" fn azihsm_sess_ex_sd_reseal_remote_backup( + sess_handle: AzihsmHandle, + params: *const AzihsmSessExSdResealRemoteBackupParams, + dst_remote_backup: *mut AzihsmBuffer, +) -> AzihsmStatus { + abi_boundary(|| { + let session = api::HsmSession::try_from(sess_handle)?; + let params = deref_ptr(params)?; + + let masked_sealing_key: &[u8] = deref_ptr(params.masked_sealing_key)?.try_into()?; + let policy: &[u8] = deref_ptr(params.policy)?.try_into()?; + let src_remote_backup: &[u8] = deref_ptr(params.src_remote_backup)?.try_into()?; + + let src_ev = deref_ptr(params.src_evidence)?; + let src_mfgr = unpack_cert_chain(&src_ev.mfgr_cert_chain)?; + let src_owner = unpack_cert_chain(&src_ev.owner_cert_chain)?; + let src_part_owner = unpack_cert_chain(&src_ev.part_owner_cert_chain)?; + let src_report: &[u8] = deref_ptr(src_ev.report)?.try_into()?; + let src_evidence = api::HsmSdEvidence { + mfgr_cert_chain: &src_mfgr, + owner_cert_chain: &src_owner, + part_owner_cert_chain: &src_part_owner, + report: src_report, + }; + + let dest_ev = deref_ptr(params.dest_evidence)?; + let dest_mfgr = unpack_cert_chain(&dest_ev.mfgr_cert_chain)?; + let dest_owner = unpack_cert_chain(&dest_ev.owner_cert_chain)?; + let dest_part_owner = unpack_cert_chain(&dest_ev.part_owner_cert_chain)?; + let dest_report: &[u8] = deref_ptr(dest_ev.report)?.try_into()?; + let dest_evidence = api::HsmSdEvidence { + mfgr_cert_chain: &dest_mfgr, + owner_cert_chain: &dest_owner, + part_owner_cert_chain: &dest_part_owner, + report: dest_report, + }; + + // Validate the output buffer before resealing. + validate_ptr(dst_remote_backup)?; + let dst_remote_backup = deref_mut_ptr(dst_remote_backup)?; + validate_output_buffer(dst_remote_backup, api::POK_REMOTE_BACKUP_LEN)?; + + let result = session.sd_reseal_remote_backup( + masked_sealing_key, + &src_evidence, + &dest_evidence, + policy, + src_remote_backup, + )?; + + copy_to_buffer(dst_remote_backup, &result)?; + + Ok(()) + }) +} diff --git a/api/native/src/utils.rs b/api/native/src/utils.rs index 23465681b..6eda4b044 100644 --- a/api/native/src/utils.rs +++ b/api/native/src/utils.rs @@ -5,8 +5,16 @@ use std::ffi::c_void; use super::*; +/// Validates that `ptr` is non-null **and** correctly aligned for `T`. +/// +/// Both properties are required before the pointer is dereferenced (via +/// [`deref_ptr`] / [`deref_mut_ptr`]) or used to form a slice with +/// [`std::slice::from_raw_parts`]: a null or misaligned pointer from a C +/// caller is undefined behavior to dereference, so reject it up front with +/// `InvalidArgument` rather than rely solely on the caller's `# Safety` +/// contract. pub(crate) fn validate_ptr(ptr: *const T) -> Result<(), AzihsmStatus> { - if ptr.is_null() { + if ptr.is_null() || !(ptr as usize).is_multiple_of(std::mem::align_of::()) { Err(AzihsmStatus::InvalidArgument) } else { Ok(()) @@ -17,7 +25,10 @@ pub(crate) fn validate_algo_params(algo: &AzihsmAlgo) -> Result<(), AzihsmSta if algo.len != std::mem::size_of::() as u32 { Err(AzihsmStatus::InvalidArgument)?; } - validate_ptr(algo.params) + // Validate against the concrete `T` (not the `*const c_void` field type) + // so the alignment check uses `align_of::()`, matching the `&T` that + // `cast_ptr` / `deref_mut_ptr` will form from this pointer. + validate_ptr(algo.params as *const T) } /// Validates that an algorithm descriptor intentionally carries no parameter payload. @@ -53,25 +64,27 @@ pub(crate) fn validate_and_cast_algo_params_mut( /// Safely dereference a mutable pointer /// /// # Safety -/// The function validates that the pointer is non-null before dereferencing. +/// The function validates that the pointer is non-null and aligned before +/// dereferencing. #[allow(unsafe_code)] #[allow(unused)] pub(crate) fn deref_mut_ptr<'a, T>(ptr: *mut T) -> Result<&'a mut T, AzihsmStatus> { validate_ptr(ptr)?; - // SAFETY: Pointer has been validated as non-null above + // SAFETY: Pointer has been validated as non-null and aligned above Ok(unsafe { &mut *ptr }) } /// Safely dereference a constant pointer /// /// # Safety -/// The function validates that the pointer is non-null before dereferencing. +/// The function validates that the pointer is non-null and aligned before +/// dereferencing. #[allow(unsafe_code)] pub(crate) fn deref_ptr<'a, T>(ptr: *const T) -> Result<&'a T, AzihsmStatus> { validate_ptr(ptr)?; - // SAFETY: Pointer has been validated as non-null above + // SAFETY: Pointer has been validated as non-null and aligned above Ok(unsafe { &*ptr }) } @@ -153,12 +166,15 @@ pub(crate) fn validate_output_buffer( /// * `Err(AzihsmError::NullPointer)` - If the pointer is null #[allow(unsafe_code)] pub(crate) fn cast_ptr<'a, T>(ptr: *const c_void) -> Result<&'a T, AzihsmStatus> { + // Cast to `*const T` before validating so the check uses `align_of::()`; + // forming `&T` from a misaligned address is undefined behavior. + let ptr = ptr as *const T; validate_ptr(ptr)?; - // SAFETY: We have validated that the pointer is not null. - // The caller is responsible for ensuring the pointer points to valid memory - // containing a properly initialized value of type T. - Ok(unsafe { &*(ptr as *const T) }) + // SAFETY: `ptr` has been validated as non-null and aligned for `T`. The + // caller is responsible for ensuring it points to valid memory containing + // a properly initialized value of type `T`. + Ok(unsafe { &*ptr }) } /// Copy a byte slice into a key property buffer diff --git a/api/tests/cpp/CMakeLists.txt b/api/tests/cpp/CMakeLists.txt index 8329d525d..b65ead86f 100644 --- a/api/tests/cpp/CMakeLists.txt +++ b/api/tests/cpp/CMakeLists.txt @@ -97,6 +97,8 @@ set(SOURCE algo/kdf/hkdf_tests.cpp algo/kdf/kbkdf_tests.cpp algo/sealing/keygen_tests.cpp + sd/create_backup_tests.cpp + sd/reseal_tests.cpp resiliency/init_stress_tests.cpp resiliency/aes_stress_tests.cpp resiliency/ecc_stress_tests.cpp diff --git a/api/tests/cpp/sd/create_backup_tests.cpp b/api/tests/cpp/sd/create_backup_tests.cpp new file mode 100644 index 000000000..ab72aebd1 --- /dev/null +++ b/api/tests/cpp/sd/create_backup_tests.cpp @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// api-level `SdCreateRemoteBackup` round trip against the emulator. +// +// Provisions a partition whose policy names itself as the backup backing +// partition (`provision_sd_backing_co_session`), mints an SD sealing key, +// attests it via the masked-blob `KeyReport`, builds the receiver's +// three-chain attestation evidence, then calls +// `azihsm_sess_ex_sd_create_remote_backup` and validates the three returned +// backups. This is a self-backup (sender == receiver): the partition seals +// to its own attested identity key. +// +// The whole file needs the two-phase TBOR HPKE handshake and a fully +// provisioned partition, which only the emu (in-process firmware) backend +// provides, so it is gated to `AZIHSM_FEATURE_EMU`. +#if defined(AZIHSM_FEATURE_EMU) + +#include +#include +#include +#include +#include +#include +#include + +#include "handle/part_list_handle.hpp" +#include "utils/sd_provision.hpp" +#include "utils/utils.hpp" + +namespace +{ +// Pinned wire lengths of the create-backup outputs. Mirror the +// `azihsm_ddi_tbor_types` constants (`POK_REMOTE_BACKUP_LEN`, +// `MASKED_SD_LEN`, `SD_MK_BACKUP_LEN`, `MASKED_SEALING_KEY_LEN`), which are +// not exposed in the C header. +constexpr uint32_t kPokRemoteBackupLen = 161; +constexpr uint32_t kPokLocalBackupLen = 180; +constexpr uint32_t kSdMkBackupLen = 164; +constexpr uint32_t kMaskedSealingKeyLen = 180; + +bool any_nonzero(const std::vector &bytes) +{ + for (uint8_t b : bytes) + { + if (b != 0) + { + return true; + } + } + return false; +} + +// Run the create-backup call, sizing the three output buffers via the +// probe/fill convention. The FFI validates each output buffer in sequence +// and reports the first that is too small, so a single len=0 probe only +// sizes the first buffer; loop until every buffer is large enough. Buffer +// validation runs before the domain is created, so a too-small buffer never +// consumes the one-shot command. Returns the final status and sizes the +// caller vectors to the bytes written on success. +azihsm_status create_backup_fill( + azihsm_handle session, + const azihsm_sess_ex_sd_create_remote_backup_params *params, + std::vector &remote, + std::vector &local, + std::vector &mk +) +{ + azihsm_buffer remote_buf{ nullptr, 0 }; + azihsm_buffer local_buf{ nullptr, 0 }; + azihsm_buffer mk_buf{ nullptr, 0 }; + azihsm_status err = AZIHSM_STATUS_BUFFER_TOO_SMALL; + for (int attempt = 0; attempt < 4; ++attempt) + { + err = azihsm_sess_ex_sd_create_remote_backup( + session, + params, + &remote_buf, + &local_buf, + &mk_buf + ); + if (err != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + break; + } + if (remote_buf.len > remote.size()) + { + remote.resize(remote_buf.len); + } + if (local_buf.len > local.size()) + { + local.resize(local_buf.len); + } + if (mk_buf.len > mk.size()) + { + mk.resize(mk_buf.len); + } + remote_buf = { remote.data(), static_cast(remote.size()) }; + local_buf = { local.data(), static_cast(local.size()) }; + mk_buf = { mk.data(), static_cast(mk.size()) }; + } + if (err == AZIHSM_STATUS_SUCCESS) + { + remote.resize(remote_buf.len); + local.resize(local_buf.len); + mk.resize(mk_buf.len); + } + return err; +} +} // namespace + +/// Test fixture for security-domain create-remote-backup +/// (`azihsm_sess_ex_sd_create_remote_backup`). +class azihsm_sd_create_backup : public ::testing::Test +{ + protected: + PartitionListHandle part_list_ = PartitionListHandle{}; + + // Open and factory-reset a partition into a clean state. Records a + // gtest failure and returns 0 on error; the returned handle must be + // closed by the caller. + static azihsm_handle open_reset_partition(std::vector &path) + { + azihsm_str path_str; + path_str.str = path.data(); + path_str.len = static_cast(path.size()); + + azihsm_handle part_handle = 0; + auto err = azihsm_part_open(&path_str, &part_handle, test_api_rev()); + if (err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "azihsm_part_open failed: " << err; + return 0; + } + + err = azihsm_part_reset(part_handle); + if (err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "azihsm_part_reset failed: " << err; + azihsm_part_close(part_handle); + return 0; + } + + return part_handle; + } + + // Open a Crypto-Officer security-domain session on an already-open + // partition handle. Records a gtest failure and returns 0 on error; + // the returned handle must be closed by the caller. + static azihsm_handle open_sd_session(azihsm_handle part_handle) + { + azihsm_handle sess_handle = 0; + azihsm_session_psk psk{ 0, nullptr }; + auto err = azihsm_sess_ex_open( + part_handle, + &psk, + AZIHSM_SESSION_EX_TYPE_AUTHENTICATED, + &sess_handle + ); + if (err != AZIHSM_STATUS_SUCCESS || sess_handle == 0) + { + ADD_FAILURE() << "azihsm_sess_ex_open failed: " << err; + return 0; + } + return sess_handle; + } +}; + +// Happy path: creating a security domain on a partition that names itself +// as the backing partition returns three non-zero backups of the pinned +// wire lengths. +TEST_F(azihsm_sd_create_backup, create_backup_roundtrip) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + SdBackingContext ctx = provision_sd_backing_co_session(part_handle); + if (ctx.session == 0) + { + return; // provisioning recorded its own failure + } + auto sess_guard = scope_guard::make_scope_exit([&ctx] { azihsm_sess_close(ctx.session); }); + + SealingKeyMaterial sealing = sealing_key_and_report(ctx.session); + ASSERT_EQ(sealing.masked.size(), kMaskedSealingKeyLen); + ASSERT_FALSE(sealing.report.empty()); + + SdEvidenceHolder evidence = build_receiver_evidence(ctx, sealing.report); + + azihsm_buffer masked_buf{ sealing.masked.data(), + static_cast(sealing.masked.size()) }; + azihsm_buffer policy_buf{ ctx.policy.data(), static_cast(ctx.policy.size()) }; + azihsm_sess_ex_sd_create_remote_backup_params params{ + &masked_buf, + &evidence.get(), + &policy_buf, + }; + + std::vector pok_remote; + std::vector pok_local; + std::vector sd_mk; + auto err = create_backup_fill(ctx.session, ¶ms, pok_remote, pok_local, sd_mk); + ASSERT_EQ(err, AZIHSM_STATUS_SUCCESS); + + // Remote backup: HPKE-Auth seal of BKS3, 161 B, non-zero. + ASSERT_EQ(pok_remote.size(), kPokRemoteBackupLen); + ASSERT_TRUE(any_nonzero(pok_remote)) << "pok_remote_backup must not be all-zero"; + + // Local backup: BKS3 masked under the partition-local key, 180 B, + // non-zero. + ASSERT_EQ(pok_local.size(), kPokLocalBackupLen); + ASSERT_TRUE(any_nonzero(pok_local)) << "pok_local_backup must not be all-zero"; + + // Masking-key backup: SDMK masked under the derived SDBMK, 164 B, + // non-zero. + ASSERT_EQ(sd_mk.size(), kSdMkBackupLen); + ASSERT_TRUE(any_nonzero(sd_mk)) << "sd_mk_backup must not be all-zero"; + }); +} + +// One-shot: creating a security domain is a once-per-partition operation, +// so a second create on the now-initialized partition is rejected by the +// firmware with `SD_ALREADY_INITIALIZED`. +TEST_F(azihsm_sd_create_backup, create_backup_is_one_shot) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + SdBackingContext ctx = provision_sd_backing_co_session(part_handle); + if (ctx.session == 0) + { + return; + } + auto sess_guard = scope_guard::make_scope_exit([&ctx] { azihsm_sess_close(ctx.session); }); + + SealingKeyMaterial sealing = sealing_key_and_report(ctx.session); + ASSERT_EQ(sealing.masked.size(), kMaskedSealingKeyLen); + ASSERT_FALSE(sealing.report.empty()); + + SdEvidenceHolder evidence = build_receiver_evidence(ctx, sealing.report); + + azihsm_buffer masked_buf{ sealing.masked.data(), + static_cast(sealing.masked.size()) }; + azihsm_buffer policy_buf{ ctx.policy.data(), static_cast(ctx.policy.size()) }; + azihsm_sess_ex_sd_create_remote_backup_params params{ + &masked_buf, + &evidence.get(), + &policy_buf, + }; + + // First create succeeds and sizes the output vectors. + std::vector pok_remote; + std::vector pok_local; + std::vector sd_mk; + ASSERT_EQ( + create_backup_fill(ctx.session, ¶ms, pok_remote, pok_local, sd_mk), + AZIHSM_STATUS_SUCCESS + ); + + // A second create on the same (now initialized) partition must + // fail. The buffers are already large enough, so the request clears + // validation and reaches the firmware's one-shot guard. + azihsm_buffer remote_buf{ pok_remote.data(), static_cast(pok_remote.size()) }; + azihsm_buffer local_buf{ pok_local.data(), static_cast(pok_local.size()) }; + azihsm_buffer mk_buf{ sd_mk.data(), static_cast(sd_mk.size()) }; + auto second = azihsm_sess_ex_sd_create_remote_backup( + ctx.session, + ¶ms, + &remote_buf, + &local_buf, + &mk_buf + ); + ASSERT_EQ(second, AZIHSM_STATUS_SD_ALREADY_INITIALIZED); + }); +} + +// A NULL params pointer is rejected with `INVALID_ARGUMENT` after the +// session resolves and before the domain is created. +TEST_F(azihsm_sd_create_backup, create_backup_null_params) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + azihsm_handle sess_handle = open_sd_session(part_handle); + if (sess_handle == 0) + { + return; + } + auto sess_guard = + scope_guard::make_scope_exit([&sess_handle] { azihsm_sess_close(sess_handle); }); + + azihsm_buffer out{ nullptr, 0 }; + auto err = azihsm_sess_ex_sd_create_remote_backup(sess_handle, nullptr, &out, &out, &out); + ASSERT_EQ(err, AZIHSM_STATUS_INVALID_ARGUMENT); + }); +} + +#endif // defined(AZIHSM_FEATURE_EMU) diff --git a/api/tests/cpp/sd/reseal_tests.cpp b/api/tests/cpp/sd/reseal_tests.cpp new file mode 100644 index 000000000..f9cfae165 --- /dev/null +++ b/api/tests/cpp/sd/reseal_tests.cpp @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// api-level `SdResealRemoteBackup` round trip against the emulator. +// +// Self-reseal on one partition: mint receiver / sender / destination SD +// sealing keys, use `azihsm_sess_ex_sd_create_remote_backup` to produce a +// real source backup (BKS3 sealed to the receiver by the sender), then +// reseal it to the destination via +// `azihsm_sess_ex_sd_reseal_remote_backup`. A successful reseal is itself +// the correctness check: the HPKE open only succeeds if the receiver key +// and the attested sender key match those that sealed the source. +// +// Like the create-backup tests, this needs the two-phase TBOR HPKE +// handshake and a fully provisioned partition, which only the emu backend +// provides, so it is gated to `AZIHSM_FEATURE_EMU`. +#if defined(AZIHSM_FEATURE_EMU) + +#include +#include +#include +#include +#include +#include +#include + +#include "handle/part_list_handle.hpp" +#include "utils/sd_provision.hpp" +#include "utils/utils.hpp" + +namespace +{ +// Pinned wire lengths. Mirror the `azihsm_ddi_tbor_types` constants +// (`POK_REMOTE_BACKUP_LEN`, `MASKED_SEALING_KEY_LEN`), which are not +// exposed in the C header. +constexpr uint32_t kPokRemoteBackupLen = 161; +constexpr uint32_t kMaskedSealingKeyLen = 180; + +bool any_nonzero(const std::vector &bytes) +{ + for (uint8_t b : bytes) + { + if (b != 0) + { + return true; + } + } + return false; +} + +// Create a real source backup: a fresh BKS3 sealed to the receiver's +// attested public key (`receiver_report`) by `masked_sender`. Returns the +// 161-byte remote backup, or an empty vector on failure (recording a gtest +// failure). Sizes the three output buffers via the probe/fill convention. +std::vector create_source_backup( + SdBackingContext &ctx, + std::vector &masked_sender, + const std::vector &receiver_report +) +{ + SdEvidenceHolder receiver = build_receiver_evidence(ctx, receiver_report); + + azihsm_buffer masked_buf{ masked_sender.data(), static_cast(masked_sender.size()) }; + azihsm_buffer policy_buf{ ctx.policy.data(), static_cast(ctx.policy.size()) }; + azihsm_sess_ex_sd_create_remote_backup_params params{ + &masked_buf, + &receiver.get(), + &policy_buf, + }; + + std::vector remote; + std::vector local; + std::vector mk; + azihsm_buffer remote_buf{ nullptr, 0 }; + azihsm_buffer local_buf{ nullptr, 0 }; + azihsm_buffer mk_buf{ nullptr, 0 }; + azihsm_status err = AZIHSM_STATUS_BUFFER_TOO_SMALL; + for (int attempt = 0; attempt < 4; ++attempt) + { + err = azihsm_sess_ex_sd_create_remote_backup( + ctx.session, + ¶ms, + &remote_buf, + &local_buf, + &mk_buf + ); + if (err != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + break; + } + if (remote_buf.len > remote.size()) + { + remote.resize(remote_buf.len); + } + if (local_buf.len > local.size()) + { + local.resize(local_buf.len); + } + if (mk_buf.len > mk.size()) + { + mk.resize(mk_buf.len); + } + remote_buf = { remote.data(), static_cast(remote.size()) }; + local_buf = { local.data(), static_cast(local.size()) }; + mk_buf = { mk.data(), static_cast(mk.size()) }; + } + if (err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "create source backup failed: " << err; + return {}; + } + remote.resize(remote_buf.len); + return remote; +} + +// Run the reseal call, sizing the single output buffer via the probe/fill +// convention. The FFI validates the output buffer before resealing, so a +// too-small buffer never performs the reseal. Returns the final status and +// sizes `dst` to the bytes written on success. +azihsm_status reseal_fill( + azihsm_handle session, + const azihsm_sess_ex_sd_reseal_remote_backup_params *params, + std::vector &dst +) +{ + azihsm_buffer dst_buf{ nullptr, 0 }; + azihsm_status err = AZIHSM_STATUS_BUFFER_TOO_SMALL; + for (int attempt = 0; attempt < 4; ++attempt) + { + err = azihsm_sess_ex_sd_reseal_remote_backup(session, params, &dst_buf); + if (err != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + break; + } + if (dst_buf.len > dst.size()) + { + dst.resize(dst_buf.len); + } + dst_buf = { dst.data(), static_cast(dst.size()) }; + } + if (err == AZIHSM_STATUS_SUCCESS) + { + dst.resize(dst_buf.len); + } + return err; +} +} // namespace + +/// Test fixture for security-domain reseal-remote-backup +/// (`azihsm_sess_ex_sd_reseal_remote_backup`). +class azihsm_sd_reseal_backup : public ::testing::Test +{ + protected: + PartitionListHandle part_list_ = PartitionListHandle{}; + + // Open and factory-reset a partition into a clean state. Records a + // gtest failure and returns 0 on error; the returned handle must be + // closed by the caller. + static azihsm_handle open_reset_partition(std::vector &path) + { + azihsm_str path_str; + path_str.str = path.data(); + path_str.len = static_cast(path.size()); + + azihsm_handle part_handle = 0; + auto err = azihsm_part_open(&path_str, &part_handle, test_api_rev()); + if (err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "azihsm_part_open failed: " << err; + return 0; + } + + err = azihsm_part_reset(part_handle); + if (err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "azihsm_part_reset failed: " << err; + azihsm_part_close(part_handle); + return 0; + } + + return part_handle; + } +}; + +// Happy path: resealing a real source backup yields a fresh 161-byte, +// non-zero backup distinct from the source ciphertext. +TEST_F(azihsm_sd_reseal_backup, reseal_backup_roundtrip) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + SdBackingContext ctx = provision_sd_backing_co_session(part_handle); + if (ctx.session == 0) + { + return; // provisioning recorded its own failure + } + auto sess_guard = scope_guard::make_scope_exit([&ctx] { azihsm_sess_close(ctx.session); }); + + // Receiver (unseals the source), sender (sealed the source), and + // destination (the reseal target) SD sealing keys, each attested. + SealingKeyMaterial rcvr = sealing_key_and_report(ctx.session); + SealingKeyMaterial sndr = sealing_key_and_report(ctx.session); + SealingKeyMaterial dst = sealing_key_and_report(ctx.session); + ASSERT_EQ(rcvr.masked.size(), kMaskedSealingKeyLen); + ASSERT_FALSE(rcvr.report.empty()); + ASSERT_FALSE(sndr.report.empty()); + ASSERT_FALSE(dst.report.empty()); + + std::vector src_backup = create_source_backup(ctx, sndr.masked, rcvr.report); + ASSERT_EQ(src_backup.size(), kPokRemoteBackupLen); + + // Reseal: open with the receiver key (auth = sender), reseal to the + // destination receiver. + SdEvidenceHolder src_ev = build_receiver_evidence(ctx, sndr.report); + SdEvidenceHolder dst_ev = build_receiver_evidence(ctx, dst.report); + + azihsm_buffer masked_buf{ rcvr.masked.data(), static_cast(rcvr.masked.size()) }; + azihsm_buffer policy_buf{ ctx.policy.data(), static_cast(ctx.policy.size()) }; + azihsm_buffer src_buf{ src_backup.data(), static_cast(src_backup.size()) }; + azihsm_sess_ex_sd_reseal_remote_backup_params params{ + &masked_buf, &src_ev.get(), &dst_ev.get(), &policy_buf, &src_buf, + }; + + std::vector dst_backup; + ASSERT_EQ(reseal_fill(ctx.session, ¶ms, dst_backup), AZIHSM_STATUS_SUCCESS); + + // A successful HPKE open -> seal yields a 161-byte, non-zero backup. + ASSERT_EQ(dst_backup.size(), kPokRemoteBackupLen); + ASSERT_TRUE(any_nonzero(dst_backup)) << "dst_remote_backup must not be all-zero"; + // The resealed backup is a fresh HPKE encapsulation, not the source. + ASSERT_NE(dst_backup, src_backup) + << "reseal must produce a fresh encapsulation, not echo the source"; + }); +} + +// Re-randomization: two reseals of the same source produce distinct +// ciphertexts (a fresh HPKE ephemeral each call). +TEST_F(azihsm_sd_reseal_backup, reseal_backup_rerandomizes) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + SdBackingContext ctx = provision_sd_backing_co_session(part_handle); + if (ctx.session == 0) + { + return; + } + auto sess_guard = scope_guard::make_scope_exit([&ctx] { azihsm_sess_close(ctx.session); }); + + SealingKeyMaterial rcvr = sealing_key_and_report(ctx.session); + SealingKeyMaterial sndr = sealing_key_and_report(ctx.session); + SealingKeyMaterial dst = sealing_key_and_report(ctx.session); + ASSERT_FALSE(rcvr.report.empty()); + ASSERT_FALSE(sndr.report.empty()); + ASSERT_FALSE(dst.report.empty()); + + std::vector src_backup = create_source_backup(ctx, sndr.masked, rcvr.report); + ASSERT_EQ(src_backup.size(), kPokRemoteBackupLen); + + SdEvidenceHolder src_ev = build_receiver_evidence(ctx, sndr.report); + SdEvidenceHolder dst_ev = build_receiver_evidence(ctx, dst.report); + + azihsm_buffer masked_buf{ rcvr.masked.data(), static_cast(rcvr.masked.size()) }; + azihsm_buffer policy_buf{ ctx.policy.data(), static_cast(ctx.policy.size()) }; + azihsm_buffer src_buf{ src_backup.data(), static_cast(src_backup.size()) }; + azihsm_sess_ex_sd_reseal_remote_backup_params params{ + &masked_buf, &src_ev.get(), &dst_ev.get(), &policy_buf, &src_buf, + }; + + std::vector first; + std::vector second; + ASSERT_EQ(reseal_fill(ctx.session, ¶ms, first), AZIHSM_STATUS_SUCCESS); + ASSERT_EQ(reseal_fill(ctx.session, ¶ms, second), AZIHSM_STATUS_SUCCESS); + ASSERT_NE(first, second) << "each reseal must re-randomize the HPKE encapsulation"; + }); +} + +// A NULL params pointer is rejected with `INVALID_ARGUMENT` after the +// session resolves and before the reseal is performed. +TEST_F(azihsm_sd_reseal_backup, reseal_backup_null_params) +{ + part_list_.for_each_part([](std::vector &path) { + azihsm_handle part_handle = open_reset_partition(path); + if (part_handle == 0) + { + return; + } + auto part_guard = + scope_guard::make_scope_exit([&part_handle] { azihsm_part_close(part_handle); }); + + SdBackingContext ctx = provision_sd_backing_co_session(part_handle); + if (ctx.session == 0) + { + return; + } + auto sess_guard = scope_guard::make_scope_exit([&ctx] { azihsm_sess_close(ctx.session); }); + + azihsm_buffer out{ nullptr, 0 }; + auto err = azihsm_sess_ex_sd_reseal_remote_backup(ctx.session, nullptr, &out); + ASSERT_EQ(err, AZIHSM_STATUS_INVALID_ARGUMENT); + }); +} + +#endif // defined(AZIHSM_FEATURE_EMU) diff --git a/api/tests/cpp/utils/sd_provision.cpp b/api/tests/cpp/utils/sd_provision.cpp index b4838b3a1..20a368988 100644 --- a/api/tests/cpp/utils/sd_provision.cpp +++ b/api/tests/cpp/utils/sd_provision.cpp @@ -13,6 +13,7 @@ #include #include +#include #ifdef _WIN32 #define NOMINMAX @@ -202,9 +203,40 @@ Bytes der_ca_extensions(const uint8_t ski[20], const uint8_t *aki) return tlv(0xA3, tlv(0x30, list)); } +/// The `[3] EXPLICIT` extensions block for an **end-entity** leaf +/// certificate: BasicConstraints (empty SEQUENCE, so `cA` defaults FALSE), +/// KeyUsage (`digitalSignature` only), SubjectKeyIdentifier, and (for a +/// non-self-signed cert) an AuthorityKeyIdentifier referencing the issuer +/// SKI. +Bytes der_leaf_extensions(const uint8_t ski[20], const uint8_t *aki) +{ + Bytes list; + // BasicConstraints: empty SEQUENCE (cA defaults to FALSE). + append(list, der_ext(kOidBasicConstraints, true, tlv(0x30, Bytes{}))); + // KeyUsage: BIT STRING with digitalSignature (bit 0) → { 0x07, 0x80 } + // (7 unused trailing bits, bit 0 set). + append(list, der_ext(kOidKeyUsage, true, tlv(0x03, Bytes{ 0x07, 0x80 }))); + // SubjectKeyIdentifier: OCTET STRING of the 20-byte key id. + append(list, der_ext(kOidSki, false, tlv(0x04, Bytes(ski, ski + 20)))); + if (aki != nullptr) + { + // AuthorityKeyIdentifier: SEQUENCE { [0] keyIdentifier }. + Bytes akid = tlv(0x30, tlv(0x80, Bytes(aki, aki + 20))); + append(list, der_ext(kOidAki, false, akid)); + } + return tlv(0xA3, tlv(0x30, list)); +} + +} // namespace + // ── Platform crypto backend (keygen / ECDSA sign / SHA-1) ──────────────────── // The only platform-specific code: OpenSSL on Linux, BCrypt on Windows. - +// +// `CaKey` is defined in the **global** namespace (not the anonymous one +// above) so it matches the `class CaKey;` forward declaration in the +// header: the public API hands the SATA anchor back through an opaque +// `std::shared_ptr`. Its members are still only referenced from +// this file. #ifdef _WIN32 /// SHA-1 of `data` (RFC 5280 key-identifier method 1). @@ -444,6 +476,9 @@ class CaKey #endif // _WIN32 +namespace +{ + // ── Portable X.509 assembly + CSR parsing ──────────────────────────────────── // Structure and identity mirror the Rust // `ddi/tbor/types/tests/harness/x509_fixture.rs` (which itself hand-assembles @@ -455,6 +490,8 @@ constexpr const char *kRootCn = "AZIHSM POTA Root CA"; constexpr const char *kRootSn = "POTAROOT1"; constexpr const char *kPtaCn = "AZIHSM PTA Intermediate CA"; constexpr const char *kPtaSn = "PTAINT001"; +constexpr const char *kLeafCn = "AZIHSM Evidence Leaf"; +constexpr const char *kLeafSn = "EVLEAF001"; /// A 20-byte positive DER serial number seeded from `tag`. Bytes serial(uint8_t tag) @@ -474,21 +511,20 @@ std::array sha1_ski(const uint8_t sec1[kSec1PubLen]) return sha1(sec1, kSec1PubLen); } -/// Assemble a signed CA certificate (`cA=TRUE`, `keyCertSign`) from its parts. -/// `aki` is the issuer's SKI for a non-self-signed cert, or null for a root. -Bytes build_ca_cert( +/// Assemble and sign a certificate from its parts and a ready-made +/// `[3] EXPLICIT` extensions block (`extensions`), returning the DER +/// `Certificate`. Shared by the CA and leaf builders. +Bytes build_cert_der( const CaKey &ca, const uint8_t subject_sec1[kSec1PubLen], const char *subject_cn, const char *subject_sn, const char *issuer_cn, const char *issuer_sn, - const uint8_t *aki, - uint8_t serial_tag + uint8_t serial_tag, + const Bytes &extensions ) { - std::array ski = sha1_ski(subject_sec1); - Bytes sig_alg = tlv(0x30, der_oid(kOidEcdsaSha384)); Bytes tbs = tlv(0x30, @@ -500,7 +536,7 @@ Bytes build_ca_cert( tlv(0x30, concat({ der_gtime(kNotBefore), der_gtime(kNotAfter) })), der_name(subject_cn, subject_sn), der_spki(subject_sec1), - der_ca_extensions(ski.data(), aki), + extensions, })); Bytes sig = ca.sign(tbs); @@ -512,6 +548,55 @@ Bytes build_ca_cert( return tlv(0x30, concat({ tbs, sig_alg, sig_value })); } +/// Assemble a signed CA certificate (`cA=TRUE`, `keyCertSign`) from its parts. +/// `aki` is the issuer's SKI for a non-self-signed cert, or null for a root. +Bytes build_ca_cert( + const CaKey &ca, + const uint8_t subject_sec1[kSec1PubLen], + const char *subject_cn, + const char *subject_sn, + const char *issuer_cn, + const char *issuer_sn, + const uint8_t *aki, + uint8_t serial_tag +) +{ + std::array ski = sha1_ski(subject_sec1); + return build_cert_der( + ca, + subject_sec1, + subject_cn, + subject_sn, + issuer_cn, + issuer_sn, + serial_tag, + der_ca_extensions(ski.data(), aki) + ); +} + +/// Assemble a signed **end-entity** leaf certificate (`cA=false`, +/// `digitalSignature`) carrying `subject_sec1`, subject `kLeafCn/kLeafSn`, +/// issued by `kRootCn/kRootSn`. `aki` references the issuer's SKI. +Bytes build_leaf_cert( + const CaKey &ca, + const uint8_t subject_sec1[kSec1PubLen], + const uint8_t *aki, + uint8_t serial_tag +) +{ + std::array ski = sha1_ski(subject_sec1); + return build_cert_der( + ca, + subject_sec1, + kLeafCn, + kLeafSn, + kRootCn, + kRootSn, + serial_tag, + der_leaf_extensions(ski.data(), aki) + ); +} + /// Build a self-signed POTA Root CA certificate (DER). Bytes build_root(const CaKey &ca) { @@ -540,6 +625,29 @@ PtaChain make_pta_chain(const CaKey &pota_ca, const uint8_t pta_sec1[kSec1PubLen return PtaChain{ build_root(pota_ca), build_pta_intermediate(pta_sec1, pota_ca) }; } +/// A generated root -> leaf attestation-evidence chain, DER-encoded. +/// `root_der` is a self-signed CA certificate; `leaf_der` is the +/// end-entity certificate signed by that root whose subject public key is +/// the report signer. +struct GeneratedChain +{ + Bytes root_der; + Bytes leaf_der; +}; + +/// Build a root -> leaf chain: a self-signed root CA (`ca`) certifying an +/// end-entity leaf carrying `leaf_pub_raw` (raw `X ‖ Y`, the report signer). +/// Pass a caller-controlled `ca` (e.g. the SATA anchor key) to anchor the +/// chain to a known public key; otherwise pass a fresh `CaKey::generate()`. +GeneratedChain make_chain(const CaKey &ca, const uint8_t leaf_pub_raw[kRawPubLen]) +{ + uint8_t leaf_sec1[kSec1PubLen]; + leaf_sec1[0] = 0x04; + std::memcpy(leaf_sec1 + 1, leaf_pub_raw, kRawPubLen); + std::array aki = sha1_ski(ca.sec1().data()); + return GeneratedChain{ build_root(ca), build_leaf_cert(ca, leaf_sec1, aki.data(), 3) }; +} + /// Read one DER TLV: on success advances `pos` past it and yields the tag + /// content span. bool der_read( @@ -676,6 +784,34 @@ std::vector build_part_policy(const uint8_t pota_raw[kRawPubLen]) return policy; } +/// Build a 484-byte backing-partition `PartPolicy` image: start from the +/// unified POTA-anchored policy, then overwrite the SATA slot with the real +/// anchor key and record this partition (`pid` / `pid_pub`) as the backup +/// backing partition. Mirrors the Rust `backing_part_policy` fixture. +std::vector build_backing_part_policy( + const uint8_t pid[16], + const uint8_t pid_pub[kRawPubLen], + const uint8_t sata_raw[kRawPubLen], + const uint8_t pota_raw[kRawPubLen] +) +{ + constexpr size_t kOffSata = 102; + constexpr size_t kOffBackupPartId = 302; + constexpr size_t kOffBackupPartPubKey = 318; + constexpr size_t kBackupPartIdLen = 16; + + std::vector policy = build_part_policy(pota_raw); + + // Overwrite the placeholder SATA key with the anchor's real P-384 key. + write_key_slot(policy, kOffSata, sata_raw); + + // Name this partition as the backup backing partition. + std::memcpy(&policy[kOffBackupPartId], pid, kBackupPartIdLen); + write_key_slot(policy, kOffBackupPartPubKey, pid_pub); + + return policy; +} + // ── Session helpers ────────────────────────────────────────────────────────── azihsm_handle open_co_session(azihsm_handle part_handle, const azihsm_buffer *psk_buf) @@ -817,4 +953,294 @@ azihsm_handle provision_sd_co_session(azihsm_handle part_handle) return session; } +SdBackingContext provision_sd_backing_co_session(azihsm_handle part_handle) +{ + // 1. Bootstrap a CO session under the default PSK and rotate it. + azihsm_handle bootstrap = open_co_session(part_handle, nullptr); + if (bootstrap == 0) + { + return {}; + } + azihsm_buffer psk_buf{ const_cast(kRotatedCoPsk.data()), + static_cast(kRotatedCoPsk.size()) }; + auto rotate_err = azihsm_sess_ex_psk_change(bootstrap, &psk_buf); + azihsm_sess_close(bootstrap); + if (rotate_err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "azihsm_sess_ex_psk_change failed: " << rotate_err; + return {}; + } + + // 2. Reopen the CO session under the rotated PSK for provisioning. + azihsm_handle session = open_co_session(part_handle, &psk_buf); + if (session == 0) + { + return {}; + } + auto fail = [&session](const char *msg, azihsm_status err) -> SdBackingContext { + ADD_FAILURE() << msg << ": " << err; + azihsm_sess_close(session); + return {}; + }; + + // 3. Materialize the partition identity (PID + raw public key) before + // PartInit; the backing policy names this partition as the backup + // backing partition. Both properties use the standard size probe. + auto read_part_prop = + [part_handle](azihsm_part_prop_id id, std::vector &out) -> azihsm_status { + azihsm_part_prop prop{ id, nullptr, 0 }; + auto err = azihsm_part_get_prop(part_handle, &prop); + if (err != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + return err; + } + out.resize(prop.len); + prop.val = out.data(); + return azihsm_part_get_prop(part_handle, &prop); + }; + std::vector pid; + std::vector pid_pub; + if (read_part_prop(AZIHSM_PART_PROP_ID_PART_EX_PID, pid) != AZIHSM_STATUS_SUCCESS || + pid.size() != 16) + { + return fail("PartInfo PID query failed", AZIHSM_STATUS_INTERNAL_ERROR); + } + if (read_part_prop(AZIHSM_PART_PROP_ID_PART_EX_PUB_KEY, pid_pub) != AZIHSM_STATUS_SUCCESS || + pid_pub.size() != kRawPubLen) + { + return fail("PartInfo PID public key query failed", AZIHSM_STATUS_INTERNAL_ERROR); + } + + // 4. Mint the POTA anchor (for the PTA chain) and the SATA anchor (which + // roots the partition-owner evidence chain), then build the backing + // policy. The SATA key is handed back to the caller as an opaque handle. + std::unique_ptr pota_ca = CaKey::generate(); + if (!pota_ca) + { + return fail("POTA CA key generation failed", AZIHSM_STATUS_INTERNAL_ERROR); + } + std::shared_ptr sata_key = CaKey::generate(); + if (!sata_key) + { + return fail("SATA CA key generation failed", AZIHSM_STATUS_INTERNAL_ERROR); + } + uint8_t pota_raw[kRawPubLen]; + std::memcpy(pota_raw, pota_ca->sec1().data() + 1, kRawPubLen); + uint8_t sata_raw[kRawPubLen]; + std::memcpy(sata_raw, sata_key->sec1().data() + 1, kRawPubLen); + std::vector policy = + build_backing_part_policy(pid.data(), pid_pub.data(), sata_raw, pota_raw); + + // Deterministic provisioning fixtures (thumbprints are stored, not + // chain-validated in this flow). + std::array mach_seed{}; + for (size_t i = 0; i < mach_seed.size(); ++i) + { + mach_seed[i] = static_cast(0x40 + i); + } + std::array pota_tp{}; + std::array sata_tp{}; + for (size_t i = 0; i < kThumbprintLen; ++i) + { + pota_tp[i] = static_cast(0x80 ^ i); + sata_tp[i] = static_cast(0x40 ^ i); + } + + azihsm_buffer mach_buf{ mach_seed.data(), static_cast(mach_seed.size()) }; + azihsm_buffer policy_buf{ policy.data(), static_cast(policy.size()) }; + azihsm_buffer pota_buf{ pota_tp.data(), static_cast(pota_tp.size()) }; + azihsm_buffer sata_buf{ sata_tp.data(), static_cast(sata_tp.size()) }; + azihsm_sess_ex_part_init_params init_params{}; + init_params.mach_seed = &mach_buf; + init_params.part_policy = &policy_buf; + init_params.pota_thumbprint = &pota_buf; + init_params.sata_thumbprint = &sata_buf; + init_params.sapota_thumbprint = nullptr; + + // 5. PartInit: probe for the CSR/report sizes, then retrieve them. + azihsm_buffer csr{ nullptr, 0 }; + azihsm_buffer report{ nullptr, 0 }; + auto probe = azihsm_sess_ex_part_init(session, &init_params, &csr, &report); + if (probe != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + return fail("PartInit size probe unexpected status", probe); + } + std::vector csr_bytes(csr.len); + std::vector report_bytes(report.len); + csr = { csr_bytes.data(), static_cast(csr_bytes.size()) }; + report = { report_bytes.data(), static_cast(report_bytes.size()) }; + auto init_err = azihsm_sess_ex_part_init(session, &init_params, &csr, &report); + if (init_err != AZIHSM_STATUS_SUCCESS) + { + return fail("PartInit failed", init_err); + } + csr_bytes.resize(csr.len); // shrink to the bytes actually written + + // 6. Build the POTA-anchored root -> PTA chain from the CSR public key. + uint8_t pta_sec1[kSec1PubLen]; + if (!pta_pub_from_csr(csr_bytes.data(), csr_bytes.size(), pta_sec1)) + { + return fail("failed to parse PTA public key from CSR", AZIHSM_STATUS_INTERNAL_ERROR); + } + PtaChain chain = make_pta_chain(*pota_ca, pta_sec1); + if (chain.root_der.empty() || chain.pta_der.empty()) + { + return fail("failed to build PTA certificate chain", AZIHSM_STATUS_INTERNAL_ERROR); + } + + // 7. PartFinal: re-supply the policy + chain (root -> PTA) out of band. + azihsm_buffer chain_bufs[2] = { + { chain.root_der.data(), static_cast(chain.root_der.size()) }, + { chain.pta_der.data(), static_cast(chain.pta_der.size()) }, + }; + azihsm_sess_ex_part_final_params final_params{}; + final_params.part_policy = &policy_buf; + final_params.pta_cert_chain = chain_bufs; + final_params.pta_cert_chain_len = 2; + final_params.prev_local_mk_backup = nullptr; + + azihsm_buffer mk_backup{ nullptr, 0 }; + auto fin_probe = azihsm_sess_ex_part_final(session, &final_params, &mk_backup); + if (fin_probe != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + return fail("PartFinal size probe unexpected status", fin_probe); + } + std::vector mk_bytes(mk_backup.len); + mk_backup = { mk_bytes.data(), static_cast(mk_bytes.size()) }; + auto final_err = azihsm_sess_ex_part_final(session, &final_params, &mk_backup); + if (final_err != AZIHSM_STATUS_SUCCESS) + { + return fail("PartFinal failed", final_err); + } + + return SdBackingContext{ session, std::move(policy), std::move(pid_pub), std::move(sata_key) }; +} + +SealingKeyMaterial sealing_key_and_report(azihsm_handle session) +{ + // Sealing-key props: a `Sealing`-kind P-384 secret key permitted for + // derivation only, matching the wire contract. + azihsm_key_kind kind = AZIHSM_KEY_KIND_SEALING; + azihsm_key_class key_class = AZIHSM_KEY_CLASS_SECRET; + uint32_t bits = 384; + uint8_t derive_val = 1; + azihsm_key_prop props[] = { + { AZIHSM_KEY_PROP_ID_KIND, &kind, sizeof(kind) }, + { AZIHSM_KEY_PROP_ID_CLASS, &key_class, sizeof(key_class) }, + { AZIHSM_KEY_PROP_ID_BIT_LEN, &bits, sizeof(bits) }, + { AZIHSM_KEY_PROP_ID_DERIVE, &derive_val, sizeof(derive_val) }, + }; + azihsm_key_prop_list prop_list{ props, + static_cast(sizeof(props) / sizeof(props[0])) }; + azihsm_algo algo{}; + algo.id = AZIHSM_ALGO_ID_SD_SEALING_KEY_GEN; + algo.params = nullptr; + algo.len = 0; + + azihsm_handle key = 0; + auto err = azihsm_key_gen(session, &algo, &prop_list, &key); + if (err != AZIHSM_STATUS_SUCCESS || key == 0) + { + ADD_FAILURE() << "azihsm_key_gen(sealing) failed: " << err; + return {}; + } + auto key_guard = scope_guard::make_scope_exit([&key] { azihsm_key_delete(key); }); + + SealingKeyMaterial material; + + // Masked private-key blob (standard size probe/fill). + azihsm_key_prop masked_prop{ AZIHSM_KEY_PROP_ID_MASKED_KEY, nullptr, 0 }; + auto masked_probe = azihsm_key_get_prop(key, &masked_prop); + if (masked_probe != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + ADD_FAILURE() << "masked key size probe unexpected status: " << masked_probe; + return {}; + } + material.masked.resize(masked_prop.len); + masked_prop.val = material.masked.data(); + auto masked_err = azihsm_key_get_prop(key, &masked_prop); + if (masked_err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "masked key fetch failed: " << masked_err; + return {}; + } + + // COSE_Sign1 KeyReport over 128 zero report-data bytes (probe/fill). + std::array report_data{}; + azihsm_buffer report_data_buf{ report_data.data(), static_cast(report_data.size()) }; + azihsm_buffer report_buf{ nullptr, 0 }; + auto report_probe = azihsm_generate_key_report(key, &report_data_buf, &report_buf); + if (report_probe != AZIHSM_STATUS_BUFFER_TOO_SMALL) + { + ADD_FAILURE() << "key report size probe unexpected status: " << report_probe; + return {}; + } + material.report.resize(report_buf.len); + report_buf = { material.report.data(), static_cast(material.report.size()) }; + auto report_err = azihsm_generate_key_report(key, &report_data_buf, &report_buf); + if (report_err != AZIHSM_STATUS_SUCCESS) + { + ADD_FAILURE() << "key report fetch failed: " << report_err; + return {}; + } + material.report.resize(report_buf.len); // shrink to the bytes written + + return material; +} + +void SdEvidenceHolder::wire() +{ + mfgr_bufs_[0] = { mfgr_root_.data(), static_cast(mfgr_root_.size()) }; + mfgr_bufs_[1] = { mfgr_leaf_.data(), static_cast(mfgr_leaf_.size()) }; + owner_bufs_[0] = { owner_root_.data(), static_cast(owner_root_.size()) }; + owner_bufs_[1] = { owner_leaf_.data(), static_cast(owner_leaf_.size()) }; + po_bufs_[0] = { po_root_.data(), static_cast(po_root_.size()) }; + po_bufs_[1] = { po_leaf_.data(), static_cast(po_leaf_.size()) }; + report_buf_ = { report_.data(), static_cast(report_.size()) }; + + ev_.mfgr_cert_chain = { mfgr_bufs_, 2 }; + ev_.owner_cert_chain = { owner_bufs_, 2 }; + ev_.part_owner_cert_chain = { po_bufs_, 2 }; + ev_.report = &report_buf_; +} + +SdEvidenceHolder build_receiver_evidence( + const SdBackingContext &ctx, + const std::vector &report +) +{ + SdEvidenceHolder holder; + if (ctx.pid_pub.size() != kRawPubLen) + { + ADD_FAILURE() << "backing context PID public key must be " << kRawPubLen << " bytes"; + return holder; + } + + // Manufacturer and owner chains root at fresh CAs; the partition-owner + // chain roots at the policy SATA anchor. Every leaf certifies the same + // partition-identity key (the report signer). + std::unique_ptr mfgr_ca = CaKey::generate(); + std::unique_ptr owner_ca = CaKey::generate(); + if (!mfgr_ca || !owner_ca || !ctx.sata_key) + { + ADD_FAILURE() << "evidence CA key generation failed"; + return holder; + } + + GeneratedChain mfgr = make_chain(*mfgr_ca, ctx.pid_pub.data()); + GeneratedChain owner = make_chain(*owner_ca, ctx.pid_pub.data()); + GeneratedChain part_owner = make_chain(*ctx.sata_key, ctx.pid_pub.data()); + + holder.mfgr_root_ = std::move(mfgr.root_der); + holder.mfgr_leaf_ = std::move(mfgr.leaf_der); + holder.owner_root_ = std::move(owner.root_der); + holder.owner_leaf_ = std::move(owner.leaf_der); + holder.po_root_ = std::move(part_owner.root_der); + holder.po_leaf_ = std::move(part_owner.leaf_der); + holder.report_ = report; + holder.wire(); + + return holder; +} + #endif // defined(AZIHSM_FEATURE_EMU) diff --git a/api/tests/cpp/utils/sd_provision.hpp b/api/tests/cpp/utils/sd_provision.hpp index 794c189a5..f3d4b1fd0 100644 --- a/api/tests/cpp/utils/sd_provision.hpp +++ b/api/tests/cpp/utils/sd_provision.hpp @@ -5,6 +5,11 @@ #include +#include +#include +#include +#include + // Security-domain provisioning helper for the sealing round-trip test. // // `SdSealingKeyGen` needs a partition in the `Initialized` state on a CO @@ -28,4 +33,119 @@ /// @return A provisioned CO session handle, or 0 on failure. azihsm_handle provision_sd_co_session(azihsm_handle part_handle); +/// Opaque host CA key (defined in `sd_provision.cpp`). Exposed only as a +/// `std::shared_ptr` handle so a caller can keep the SATA anchor alive +/// across the evidence-building step without seeing the definition. +class CaKey; + +/// A provisioned backing-partition security-domain context: the live CO +/// session plus the exact policy image, partition-identity public key, and +/// SATA anchor key that the create-backup call and its evidence depend on. +struct SdBackingContext +{ + /// Provisioned CO session handle (`Initialized`); 0 on failure. + azihsm_handle session = 0; + /// Exact 484-byte backing-partition policy image. + std::vector policy; + /// Raw P-384 partition-identity public key (`X ‖ Y`, 96 B). + std::vector pid_pub; + /// SATA anchor key that roots the partition-owner evidence chain. + std::shared_ptr sata_key; +}; + +/// Provision `part_handle` with a backing-partition policy that names this +/// partition (via its `PartInfo` PID / raw public key) as the backup +/// backing partition, anchored to a freshly-generated internal SATA key. +/// +/// Records a gtest failure and returns a default `SdBackingContext` +/// (`session == 0`) on error. The caller owns `session` and must close it +/// with `azihsm_sess_close`. +SdBackingContext provision_sd_backing_co_session(azihsm_handle part_handle); + +/// A minted SD sealing key: its masked private-key blob and a COSE_Sign1 +/// `KeyReport` attesting it. +struct SealingKeyMaterial +{ + /// Masked sealing-key blob (180 B). + std::vector masked; + /// COSE_Sign1 `KeyReport` DER bytes. + std::vector report; +}; + +/// Mint an SD sealing key on `session` and return its 180-byte masked blob +/// plus a `KeyReport` built over 128 zero report-data bytes. Records a +/// gtest failure and returns empty vectors on error. +SealingKeyMaterial sealing_key_and_report(azihsm_handle session); + +/// Owns the DER bytes and `azihsm_buffer` arrays backing one party's SD +/// attestation evidence and exposes a wired-up `azihsm_sd_evidence` that +/// points into them. The holder must outlive the create-backup call. +/// +/// Copies are forbidden (the internal `azihsm_buffer` / `azihsm_sd_evidence` +/// pointers are self-referential); moves re-point them at the moved-to +/// storage. +class SdEvidenceHolder +{ + public: + SdEvidenceHolder() = default; + SdEvidenceHolder(const SdEvidenceHolder &) = delete; + SdEvidenceHolder &operator=(const SdEvidenceHolder &) = delete; + SdEvidenceHolder(SdEvidenceHolder &&other) noexcept + { + *this = std::move(other); + } + SdEvidenceHolder &operator=(SdEvidenceHolder &&other) noexcept + { + mfgr_root_ = std::move(other.mfgr_root_); + mfgr_leaf_ = std::move(other.mfgr_leaf_); + owner_root_ = std::move(other.owner_root_); + owner_leaf_ = std::move(other.owner_leaf_); + po_root_ = std::move(other.po_root_); + po_leaf_ = std::move(other.po_leaf_); + report_ = std::move(other.report_); + wire(); + return *this; + } + + /// The wired-up evidence view; valid for this holder's lifetime. + const azihsm_sd_evidence &get() const + { + return ev_; + } + + private: + friend SdEvidenceHolder build_receiver_evidence( + const SdBackingContext &ctx, + const std::vector &report + ); + + /// Re-point the `azihsm_buffer` arrays and `azihsm_sd_evidence` at the + /// currently-held vectors. Called after population and every move. + void wire(); + + std::vector mfgr_root_; + std::vector mfgr_leaf_; + std::vector owner_root_; + std::vector owner_leaf_; + std::vector po_root_; + std::vector po_leaf_; + std::vector report_; + azihsm_buffer mfgr_bufs_[2]{}; + azihsm_buffer owner_bufs_[2]{}; + azihsm_buffer po_bufs_[2]{}; + azihsm_buffer report_buf_{}; + azihsm_sd_evidence ev_{}; +}; + +/// Build the receiver's three-chain SD attestation evidence for +/// `ctx.pid_pub`: the manufacturer and owner chains are rooted at fresh +/// CAs, and the partition-owner chain is rooted at `ctx.sata_key`. Every +/// leaf certifies `ctx.pid_pub` (the report signer); `report` is the +/// attestation report. Records a gtest failure and returns an empty holder +/// on error. +SdEvidenceHolder build_receiver_evidence( + const SdBackingContext &ctx, + const std::vector &report +); + #endif // defined(AZIHSM_FEATURE_EMU) diff --git a/api/tests/src/algo/sealing/key_gen_tests.rs b/api/tests/src/algo/sealing/key_gen_tests.rs index ea7e26ca3..46cd47000 100644 --- a/api/tests/src/algo/sealing/key_gen_tests.rs +++ b/api/tests/src/algo/sealing/key_gen_tests.rs @@ -7,14 +7,14 @@ //! //! Property-validation guards run before the device round-trip, so they are //! deterministic. The `roundtrip_*` tests provision the partition to -//! `Initialized` via [`super::provision::finalized_co_session`], then +//! `Initialized` via [`crate::utils::sd_provision::finalized_co_session`], then //! generate a sealing key end to end and validate the masked blob and //! public key. use azihsm_api::*; use azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN; -use crate::emu_helpers::*; +use crate::utils::emu_helpers::*; /// Well-formed sealing key props: a `Sealing`-kind P-384 secret key /// permitted for derivation only, matching the wire contract. @@ -157,7 +157,7 @@ fn sealing_key_gen_valid_props_pass_host_guards() { #[test] fn sealing_key_gen_roundtrip_generates_usable_sealing_key() { let _guard = EMU_LOCK.lock(); - let session = super::provision::finalized_co_session(); + let session = crate::utils::sd_provision::finalized_co_session(); let mut algo = HsmSealingKeyGenAlgo::default(); let key = HsmKeyManager::generate_key(&session, &mut algo, sealing_props()) @@ -188,7 +188,7 @@ fn sealing_key_gen_roundtrip_generates_usable_sealing_key() { #[test] fn sealing_key_gen_roundtrip_yields_distinct_keys() { let _guard = EMU_LOCK.lock(); - let session = super::provision::finalized_co_session(); + let session = crate::utils::sd_provision::finalized_co_session(); let generate = || { let mut algo = HsmSealingKeyGenAlgo::default(); diff --git a/api/tests/src/algo/sealing/mod.rs b/api/tests/src/algo/sealing/mod.rs index 2ed55e550..fa835486c 100644 --- a/api/tests/src/algo/sealing/mod.rs +++ b/api/tests/src/algo/sealing/mod.rs @@ -2,4 +2,3 @@ // Licensed under the MIT License. mod key_gen_tests; -mod provision; diff --git a/api/tests/src/lib.rs b/api/tests/src/lib.rs index 298970fa5..2f7acaebb 100644 --- a/api/tests/src/lib.rs +++ b/api/tests/src/lib.rs @@ -12,11 +12,11 @@ mod resiliency_tests; mod session_tests; mod utils; -#[cfg(feature = "emu")] -mod emu_helpers; #[cfg(feature = "emu")] mod partition_ex_tests; #[cfg(feature = "emu")] +mod sd; +#[cfg(feature = "emu")] mod session_ex_tests; use azihsm_api::*; diff --git a/api/tests/src/partition_ex_tests.rs b/api/tests/src/partition_ex_tests.rs index 8be0c9e88..d84a1a45b 100644 --- a/api/tests/src/partition_ex_tests.rs +++ b/api/tests/src/partition_ex_tests.rs @@ -20,7 +20,7 @@ use azihsm_ddi_tbor_types::POTA_THUMBPRINT_LEN; use azihsm_ddi_tbor_types::SAPOTA_THUMBPRINT_LEN; use azihsm_ddi_tbor_types::SATA_THUMBPRINT_LEN; -use crate::emu_helpers::*; +use crate::utils::emu_helpers::*; /// Well-formed fixed-size inputs for the non-`part_policy` `PartInit` /// fields. diff --git a/api/tests/src/sd/create_backup_tests.rs b/api/tests/src/sd/create_backup_tests.rs new file mode 100644 index 000000000..8e55d765f --- /dev/null +++ b/api/tests/src/sd/create_backup_tests.rs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! api-level `SdCreateRemoteBackup` round trip against the emulator. +//! +//! Drives the full public-surface flow: provision a partition whose policy +//! names itself as the backup backing partition +//! ([`crate::utils::sd_provision::finalized_backing_session`]), mint an SD sealing +//! key, attest it via the masked-blob `KeyReport`, build the receiver's +//! three-chain attestation evidence, then call +//! [`HsmSession::sd_create_remote_backup`] and validate the three returned +//! backups. This is a **self-backup** (sender == receiver): the partition +//! seals to its own attested identity key. + +use azihsm_api::*; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; + +use crate::utils::emu_helpers::EMU_LOCK; +use crate::utils::sd_provision::CaKey; +use crate::utils::sd_provision::build_receiver_evidence; +use crate::utils::sd_provision::finalized_backing_session; +use crate::utils::sd_provision::masked_key_and_report; + +/// Happy path: creating a security domain on a partition that names +/// itself as the backing partition returns three non-zero backups of the +/// pinned wire lengths. +#[test] +fn sd_create_remote_backup_roundtrip() { + let _guard = EMU_LOCK.lock(); + let sata_key = CaKey::generate(); + let (session, policy, pid_pub) = finalized_backing_session(&sata_key); + + let (masked, report) = masked_key_and_report(&session); + 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)) + .expect("create remote backup"); + + // Remote backup: HPKE-Auth seal of BKS3, 161 B, non-zero. + assert_eq!(result.pok_remote_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!( + result.pok_remote_backup.iter().any(|&b| b != 0), + "pok_remote_backup must not be all-zero", + ); + + // Local backup: BKS3 masked under the partition-local key, 180 B, + // non-zero. + assert_eq!(result.pok_local_backup.len(), MASKED_SD_LEN); + assert!( + result.pok_local_backup.iter().any(|&b| b != 0), + "pok_local_backup must not be all-zero", + ); + + // Masking-key backup: SDMK masked under the derived SDBMK, 164 B, + // non-zero. + assert_eq!(result.sd_mk_backup.len(), SD_MK_BACKUP_LEN); + assert!( + result.sd_mk_backup.iter().any(|&b| b != 0), + "sd_mk_backup must not be all-zero", + ); +} + +/// One-shot: creating a security domain is a once-per-partition +/// operation, so a second `sd_create_remote_backup` on the now-initialized +/// partition is rejected by the firmware. +#[test] +fn sd_create_remote_backup_is_one_shot() { + let _guard = EMU_LOCK.lock(); + let sata_key = CaKey::generate(); + let (session, policy, pid_pub) = finalized_backing_session(&sata_key); + + let (masked, report) = masked_key_and_report(&session); + let evidence = build_receiver_evidence(&pid_pub, &sata_key, &report); + + evidence + .with_hsm_evidence(|receiver| session.sd_create_remote_backup(&masked, receiver, &policy)) + .expect("first create remote backup"); + + // A second create on the same (now initialized) partition must fail. + let second = evidence + .with_hsm_evidence(|receiver| session.sd_create_remote_backup(&masked, receiver, &policy)); + assert!( + matches!(second, Err(HsmError::SdAlreadyInitialized)), + "second create on an initialized partition must be rejected with \ + SdAlreadyInitialized, got {second:?}", + ); +} diff --git a/api/tests/src/sd/mod.rs b/api/tests/src/sd/mod.rs new file mode 100644 index 000000000..076f99558 --- /dev/null +++ b/api/tests/src/sd/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! api-level tests for the security-domain backup command family +//! (`SdCreateRemoteBackup`, `SdResealRemoteBackup`). + +mod create_backup_tests; +mod reseal_tests; diff --git a/api/tests/src/sd/reseal_tests.rs b/api/tests/src/sd/reseal_tests.rs new file mode 100644 index 000000000..235defe00 --- /dev/null +++ b/api/tests/src/sd/reseal_tests.rs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! api-level `SdResealRemoteBackup` round trip against the emulator. +//! +//! Self-reseal on one partition: mint receiver / sender / destination SD +//! sealing keys, use `sd_create_remote_backup` to produce a real source +//! backup (BKS3 sealed to the receiver by the sender), then reseal it to +//! the destination. A successful reseal is itself the correctness check — +//! the HPKE open only succeeds if the receiver key and the attested sender +//! key match those that sealed the source. + +use azihsm_api::*; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; + +use crate::utils::emu_helpers::EMU_LOCK; +use crate::utils::sd_provision::CaKey; +use crate::utils::sd_provision::RAW_PUB_LEN; +use crate::utils::sd_provision::build_receiver_evidence; +use crate::utils::sd_provision::finalized_backing_session; +use crate::utils::sd_provision::masked_key_and_report; + +/// Create a real source backup: a fresh BKS3 sealed to the receiver's +/// attested public key (`receiver_report`) by the `masked_sender_key`. +fn create_source_backup( + session: &HsmSession, + sata_key: &CaKey, + pid_pub: &[u8; RAW_PUB_LEN], + masked_sender_key: &[u8], + receiver_report: &[u8], + policy: &[u8], +) -> Vec { + let receiver = build_receiver_evidence(pid_pub, sata_key, receiver_report); + receiver + .with_hsm_evidence(|rcvr| session.sd_create_remote_backup(masked_sender_key, rcvr, policy)) + .expect("source backup") + .pok_remote_backup +} + +/// Happy path: resealing a real source backup yields a fresh 161-byte, +/// non-zero backup distinct from the source ciphertext. +#[test] +fn sd_reseal_remote_backup_roundtrip() { + let _guard = EMU_LOCK.lock(); + let sata_key = CaKey::generate(); + let (session, policy, pid_pub) = finalized_backing_session(&sata_key); + + // 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 src_backup = create_source_backup( + &session, + &sata_key, + &pid_pub, + &masked_sndr, + &report_rcvr, + &policy, + ); + + // Reseal: open with the receiver key (auth = sender), reseal to the + // destination receiver. + let src_ev = build_receiver_evidence(&pid_pub, &sata_key, &report_sndr); + let dst_ev = build_receiver_evidence(&pid_pub, &sata_key, &report_dst); + let dst_backup = src_ev + .with_hsm_evidence(|src| { + dst_ev.with_hsm_evidence(|dest| { + session.sd_reseal_remote_backup(&masked_rcvr, src, dest, &policy, &src_backup) + }) + }) + .expect("reseal remote backup"); + + // A successful HPKE open -> seal yields a 161-byte, non-zero backup. + assert_eq!(dst_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!( + dst_backup.iter().any(|&b| b != 0), + "dst_remote_backup must not be all-zero", + ); + // The resealed backup is a fresh HPKE encapsulation, not the source. + assert_ne!( + dst_backup, + src_backup.to_vec(), + "reseal must produce a fresh encapsulation, not echo the source", + ); +} + +/// Re-randomization: two reseals of the same source produce distinct +/// ciphertexts (a fresh HPKE ephemeral each call). +#[test] +fn sd_reseal_remote_backup_rerandomizes() { + let _guard = EMU_LOCK.lock(); + 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 src_backup = create_source_backup( + &session, + &sata_key, + &pid_pub, + &masked_sndr, + &report_rcvr, + &policy, + ); + + let src_ev = build_receiver_evidence(&pid_pub, &sata_key, &report_sndr); + let dst_ev = build_receiver_evidence(&pid_pub, &sata_key, &report_dst); + let reseal = || { + src_ev + .with_hsm_evidence(|src| { + dst_ev.with_hsm_evidence(|dest| { + session.sd_reseal_remote_backup(&masked_rcvr, src, dest, &policy, &src_backup) + }) + }) + .expect("reseal remote backup") + }; + + let first = reseal(); + let second = reseal(); + assert_ne!( + first, second, + "each reseal must re-randomize the HPKE encapsulation", + ); +} diff --git a/api/tests/src/session_ex_tests.rs b/api/tests/src/session_ex_tests.rs index 0bef5ea47..4ba52b647 100644 --- a/api/tests/src/session_ex_tests.rs +++ b/api/tests/src/session_ex_tests.rs @@ -11,7 +11,7 @@ use azihsm_api::*; -use crate::emu_helpers::*; +use crate::utils::emu_helpers::*; /// Happy path: CO pairs with an Authenticated session and returns a /// live `HsmSession` over the public API. diff --git a/api/tests/src/emu_helpers.rs b/api/tests/src/utils/emu_helpers.rs similarity index 100% rename from api/tests/src/emu_helpers.rs rename to api/tests/src/utils/emu_helpers.rs diff --git a/api/tests/src/utils/mod.rs b/api/tests/src/utils/mod.rs index 3e8eae2b4..4ac1328d8 100644 --- a/api/tests/src/utils/mod.rs +++ b/api/tests/src/utils/mod.rs @@ -3,6 +3,10 @@ pub(crate) mod aes_xts; pub(crate) mod api; +#[cfg(feature = "emu")] +pub(crate) mod emu_helpers; pub(crate) mod partition; pub(crate) mod resiliency; +#[cfg(feature = "emu")] +pub(crate) mod sd_provision; pub(crate) mod session; diff --git a/api/tests/src/algo/sealing/provision.rs b/api/tests/src/utils/sd_provision.rs similarity index 51% rename from api/tests/src/algo/sealing/provision.rs rename to api/tests/src/utils/sd_provision.rs index f9b67b05b..533b42410 100644 --- a/api/tests/src/algo/sealing/provision.rs +++ b/api/tests/src/utils/sd_provision.rs @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Security-domain provisioning fixture for the api-level sealing round -//! trip. +//! Security-domain provisioning fixture shared by the api-level sealing +//! and backup tests. //! -//! Drives the full flow through the public `azihsm_api` surface — rotate -//! the CO PSK, `part_init_ex`, build a POTA-anchored PTA chain, -//! `part_final_ex` — to reach the `Initialized` state `SdSealingKeyGen` -//! requires. The chain is built on the host with [`azihsm_crypto`], -//! mirroring the wire-level `ddi/tbor/types/tests/harness/x509_fixture.rs`. +//! Drives the full flow through the public `azihsm_api` surface (rotate +//! the CO PSK, `part_init_ex`, POTA-anchored PTA chain, `part_final_ex`) +//! to reach the `Initialized` state, plus the evidence-chain and +//! sealing-key helpers the backup tests need. Certificates are built on +//! the host with [`azihsm_crypto`], mirroring the wire-level +//! `ddi/tbor/types/tests/harness/x509_fixture.rs`. use azihsm_api::*; use azihsm_crypto::EccCurve; @@ -21,9 +22,13 @@ use azihsm_crypto::SignOp; use azihsm_crypto::x509_builder::cert_builder; use azihsm_crypto::x509_builder::cert_builder::CN_LEN; use azihsm_crypto::x509_builder::cert_builder::IntermediateCertParams; +use azihsm_crypto::x509_builder::cert_builder::KeyUsage; +use azihsm_crypto::x509_builder::cert_builder::LeafCertParams; use azihsm_crypto::x509_builder::cert_builder::RootCertParams; use azihsm_crypto::x509_builder::cert_builder::SN_LEN; +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; use azihsm_ddi_tbor_types::POLICY_INFO_LEN; use azihsm_ddi_tbor_types::POLICY_MAX_KEY_LEN; use azihsm_ddi_tbor_types::POTA_THUMBPRINT_LEN; @@ -34,16 +39,29 @@ use azihsm_ddi_tbor_types::PolicyVer; use azihsm_ddi_tbor_types::SATA_THUMBPRINT_LEN; use zerocopy::IntoBytes; -use crate::emu_helpers::fresh_emu_partition; +use crate::utils::emu_helpers::fresh_emu_partition; const SEC1_PUB_LEN: usize = 97; -const RAW_PUB_LEN: usize = 96; +pub(crate) const RAW_PUB_LEN: usize = 96; const NOT_BEFORE: &[u8; 15] = b"20250101000000Z"; const NOT_AFTER: &[u8; 15] = b"20350101000000Z"; const ROOT_CN: &str = "AZIHSM POTA Root CA"; const ROOT_SN: &str = "POTAROOT1"; const PTA_CN: &str = "AZIHSM PTA Intermediate CA"; const PTA_SN: &str = "PTAINT001"; +const LEAF_CN: &str = "AZIHSM Evidence Leaf"; +const LEAF_SN: &str = "EVLEAF001"; + +/// Byte offset of the SATA public-key **data** inside the `PartPolicy` +/// image: `sata_pub_key` starts at 102 (`kind(2) ‖ len(2) ‖ data(96)`), +/// so the raw `X ‖ Y` coordinates begin at 106. +const OFF_SATA_PUB_KEY_DATA: usize = 106; + +/// Byte offsets of the backing-partition fields inside the `PartPolicy` +/// image (mirror of `fw/core/ddi/tbor/types/src/policy.rs`). +const OFF_BACKUP_PART_ID: usize = 302; +const OFF_BACKUP_PART_PUB_KEY: usize = 318; +const BACKUP_PART_ID_LEN: usize = 16; /// A fixed non-default CO PSK used to clear the default-PSK gate. const ROTATED_CO_PSK: [u8; PSK_LEN] = [ @@ -53,14 +71,14 @@ const ROTATED_CO_PSK: [u8; PSK_LEN] = [ /// A synthetic P-384 CA key (the policy POTA trust anchor) that signs /// certificates and exposes its public key. -struct CaKey { +pub(crate) struct CaKey { private_key: EccPrivateKey, pub_sec1: [u8; SEC1_PUB_LEN], } impl CaKey { /// Generate a fresh P-384 CA key. - fn generate() -> Self { + pub(crate) fn generate() -> Self { let private_key = EccPrivateKey::from_curve(EccCurve::P384).expect("P-384 key"); let (x, y) = private_key.coord_vec().expect("coords"); let mut pub_sec1 = [0u8; SEC1_PUB_LEN]; @@ -75,7 +93,7 @@ impl CaKey { /// Raw `X ‖ Y` (96-byte) public coordinates — the policy `POTAPubKey` /// form. - fn raw_pub(&self) -> [u8; RAW_PUB_LEN] { + pub(crate) fn raw_pub(&self) -> [u8; RAW_PUB_LEN] { self.pub_sec1[1..].try_into().expect("raw pub") } @@ -194,6 +212,81 @@ fn make_pta_chain(pota_ca: &CaKey, pta_pub_sec1: &[u8; SEC1_PUB_LEN]) -> PtaChai } } +/// Build an **end-entity** leaf certificate whose subject public key is +/// `leaf_pub_sec1` (an attestation-report signer's key), signed by +/// `issuer` (a self-signed CA). Unlike [`build_pta_intermediate`], the +/// leaf is `cA=false` with `digitalSignature` key usage. +fn build_leaf(leaf_pub_sec1: &[u8; SEC1_PUB_LEN], issuer: &CaKey) -> Vec { + let params = LeafCertParams { + public_key: leaf_pub_sec1, + serial_number: &serial(3), + not_before: NOT_BEFORE, + not_after: NOT_AFTER, + subject_cn: LEAF_CN, + subject_sn: LEAF_SN, + issuer_cn: ROOT_CN, + issuer_sn: ROOT_SN, + subject_key_id: &sha1_ski(leaf_pub_sec1), + authority_key_id: &issuer.ski(), + key_usage: KeyUsage::DIGITAL_SIGNATURE, + }; + let mut tbs = azihsm_crypto::x509_builder::leaf_cert::TBS_TEMPLATE; + patch_tbs_leaf(&mut tbs, ¶ms); + let (r, s) = issuer.sign(&tbs); + let mut out = vec![0u8; 1024]; + let len = cert_builder::build_leaf_cert(¶ms, &r, &s, &mut out).expect("leaf cert"); + out.truncate(len); + out +} + +/// A generated root -> leaf attestation-evidence chain, DER-encoded. +/// +/// `root_der` is a self-signed CA certificate; `leaf_der` is an +/// end-entity certificate signed by the root whose subject public key is +/// the caller-supplied report-signer key. +pub(crate) struct GeneratedChain { + root_der: Vec, + leaf_der: Vec, +} + +/// Build a root -> leaf chain: a self-signed root CA (`ca`) certifying an +/// end-entity leaf that carries `leaf_pub_raw` (raw `X ‖ Y`, the report +/// signer's public key). +/// +/// Pass a caller-controlled `ca` (e.g. the SATA anchor key) when the chain +/// must be anchored to a known public key; otherwise use a fresh +/// [`CaKey::generate`]. +fn make_chain(ca: &CaKey, leaf_pub_raw: &[u8; RAW_PUB_LEN]) -> GeneratedChain { + let mut leaf_sec1 = [0u8; SEC1_PUB_LEN]; + leaf_sec1[0] = 0x04; + leaf_sec1[1..].copy_from_slice(leaf_pub_raw); + GeneratedChain { + root_der: build_root(ca), + leaf_der: build_leaf(&leaf_sec1, ca), + } +} + +/// Patch a leaf-cert TBS template with the variable field values. +fn patch_tbs_leaf(tbs: &mut [u8], params: &LeafCertParams<'_>) { + use azihsm_crypto::x509_builder::leaf_cert::*; + let s_cn = pad_cn(params.subject_cn); + let i_cn = pad_cn(params.issuer_cn); + let s_sn = pad_sn(params.subject_sn); + let i_sn = pad_sn(params.issuer_sn); + tbs[PUBLIC_KEY_OFFSET..PUBLIC_KEY_OFFSET + 97].copy_from_slice(params.public_key); + tbs[SERIAL_NUMBER_OFFSET..SERIAL_NUMBER_OFFSET + 20].copy_from_slice(params.serial_number); + tbs[NOT_BEFORE_OFFSET..NOT_BEFORE_OFFSET + 15].copy_from_slice(params.not_before); + tbs[NOT_AFTER_OFFSET..NOT_AFTER_OFFSET + 15].copy_from_slice(params.not_after); + tbs[ISSUER_CN_OFFSET..ISSUER_CN_OFFSET + CN_LEN].copy_from_slice(&i_cn); + tbs[SUBJECT_CN_OFFSET..SUBJECT_CN_OFFSET + CN_LEN].copy_from_slice(&s_cn); + tbs[ISSUER_SN_OFFSET..ISSUER_SN_OFFSET + SN_LEN].copy_from_slice(&i_sn); + tbs[SUBJECT_SN_OFFSET..SUBJECT_SN_OFFSET + SN_LEN].copy_from_slice(&s_sn); + tbs[SUBJECT_KEY_ID_OFFSET..SUBJECT_KEY_ID_OFFSET + 20].copy_from_slice(params.subject_key_id); + tbs[AUTHORITY_KEY_ID_OFFSET..AUTHORITY_KEY_ID_OFFSET + 20] + .copy_from_slice(params.authority_key_id); + tbs[KEY_USAGE_OFFSET..KEY_USAGE_OFFSET + 2].copy_from_slice(¶ms.key_usage.to_bytes()); +} + /// Extract the SEC1 uncompressed public key (`0x04 ‖ X ‖ Y`) from a DER /// PKCS#10 CSR. fn pta_pub_from_csr(csr: &[u8]) -> [u8; SEC1_PUB_LEN] { @@ -377,3 +470,235 @@ pub(crate) fn finalized_co_session() -> HsmSession { session } + +/// Build a policy naming **this** partition as the backing partition +/// (`backup_part_id = PID`, `backup_part_pub_key = PID public key`) and +/// anchoring the security domain to `sata_pub` (raw `X ‖ Y`). +/// +/// The caller learns the PID / PID public key from `PartInfo` before +/// `part_init_ex`; the SATA key is the trust anchor the test also uses to +/// sign the partition-owner certificate chain. +fn backing_part_policy( + pid: &[u8], + pid_pub: &[u8], + sata_pub: &[u8; RAW_PUB_LEN], + pota_pub: &[u8; RAW_PUB_LEN], +) -> [u8; PART_POLICY_LEN] { + // Anchor the policy to a real POTA key so `part_final_ex` can validate + // a PTA certificate chain against it. + let policy = part_policy_with_pota(pota_pub); + let mut bytes = [0u8; PART_POLICY_LEN]; + bytes.copy_from_slice(policy.as_bytes()); + + // Overwrite the placeholder SATA key with the anchor's real P-384 + // coordinates (kind / len already Ecc384 / 96). + bytes[OFF_SATA_PUB_KEY_DATA..OFF_SATA_PUB_KEY_DATA + RAW_PUB_LEN].copy_from_slice(sata_pub); + + bytes[OFF_BACKUP_PART_ID..OFF_BACKUP_PART_ID + BACKUP_PART_ID_LEN].copy_from_slice(pid); + + // backup_part_pub_key = { kind: Ecc384 (LE), len: 96 (LE), data }. + let off = OFF_BACKUP_PART_PUB_KEY; + bytes[off..off + 2].copy_from_slice(&PolicyKeyKind::Ecc384.0.to_le_bytes()); + bytes[off + 2..off + 4].copy_from_slice(&(POLICY_MAX_KEY_LEN as u16).to_le_bytes()); + bytes[off + 4..off + 4 + POLICY_MAX_KEY_LEN].copy_from_slice(pid_pub); + + bytes +} + +/// Owns the DER bytes for the receiver's three evidence chains and the +/// attestation report, so a borrowed [`HsmSdEvidence`] can reference them. +pub(crate) struct SdEvidence { + mfgr: GeneratedChain, + owner: GeneratedChain, + part_owner: GeneratedChain, + report: Vec, +} + +impl SdEvidence { + /// Manufacturer chain as an ordered `[root, leaf]` cert list. + pub(crate) fn mfgr_certs(&self) -> [HsmCert<'_>; 2] { + [ + HsmCert { + cert: &self.mfgr.root_der, + }, + HsmCert { + cert: &self.mfgr.leaf_der, + }, + ] + } + + /// Owner chain as an ordered `[root, leaf]` cert list. + pub(crate) fn owner_certs(&self) -> [HsmCert<'_>; 2] { + [ + HsmCert { + cert: &self.owner.root_der, + }, + HsmCert { + cert: &self.owner.leaf_der, + }, + ] + } + + /// Partition-owner chain as an ordered `[root, leaf]` cert list. + pub(crate) fn part_owner_certs(&self) -> [HsmCert<'_>; 2] { + [ + HsmCert { + cert: &self.part_owner.root_der, + }, + HsmCert { + cert: &self.part_owner.leaf_der, + }, + ] + } + + /// The COSE_Sign1 attestation-report DER bytes. + pub(crate) fn report(&self) -> &[u8] { + &self.report + } + + /// Build a borrowed [`HsmSdEvidence`] over this party's three cert + /// chains and report and pass it to `f`. The cert arrays live only for + /// the call, so the evidence is delivered through a closure. + pub(crate) fn with_hsm_evidence(&self, f: impl FnOnce(&HsmSdEvidence<'_>) -> R) -> R { + let mfgr = self.mfgr_certs(); + let owner = self.owner_certs(); + let part_owner = self.part_owner_certs(); + f(&HsmSdEvidence { + mfgr_cert_chain: &mfgr, + owner_cert_chain: &owner, + part_owner_cert_chain: &part_owner, + report: self.report(), + }) + } +} + +/// Build the receiver's three-chain evidence for `pid_pub`: manufacturer +/// and owner chains rooted at fresh CAs, and a partition-owner chain rooted +/// at the policy `sata_key`. Every leaf certifies `pid_pub` (the report +/// signer), so all three share one leaf key. +pub(crate) fn build_receiver_evidence( + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + report: &[u8], +) -> SdEvidence { + SdEvidence { + mfgr: make_chain(&CaKey::generate(), pid_pub), + owner: make_chain(&CaKey::generate(), pid_pub), + part_owner: make_chain(sata_key, pid_pub), + report: report.to_vec(), + } +} + +/// Provision a fresh partition with a **backing-partition policy** — one +/// that names this partition (via `PartInfo`) as the backup backing +/// partition and anchors the security domain to `sata_key` — and return +/// the live CO session, the exact policy image (needed verbatim by +/// `sd_create_remote_backup`), and the partition-identity public key that +/// every evidence leaf certificate must carry. +pub(crate) fn finalized_backing_session( + sata_key: &CaKey, +) -> (HsmSession, [u8; PART_POLICY_LEN], [u8; RAW_PUB_LEN]) { + let (part, rev) = fresh_emu_partition(); + + // Bootstrap the CO session under the default PSK and rotate it; the + // bootstrap session closes on drop at the end of this block. + { + let bootstrap = part + .open_session_ex( + rev, + HsmSessionPsk::new(HsmPskId::CO), + HsmSessionExType::Authenticated, + ) + .expect("open bootstrap CO session"); + bootstrap + .change_psk(&ROTATED_CO_PSK) + .expect("rotate CO PSK"); + } + + let session = part + .open_session_ex( + rev, + HsmSessionPsk::with_psk(HsmPskId::CO, &ROTATED_CO_PSK), + HsmSessionExType::Authenticated, + ) + .expect("open rotated CO session"); + + // PID / PID public key are materialized before part_init_ex. + let pid = part.pid().expect("PartInfo PID"); + let pid_pub_vec = part.ex_pub_key().expect("PartInfo PID public key"); + assert_eq!( + pid.len(), + BACKUP_PART_ID_LEN, + "PartInfo PID must be BACKUP_PART_ID_LEN bytes", + ); + assert_eq!( + pid_pub_vec.len(), + RAW_PUB_LEN, + "PartInfo PID public key must be RAW_PUB_LEN bytes", + ); + let mut pid_pub = [0u8; RAW_PUB_LEN]; + pid_pub.copy_from_slice(&pid_pub_vec); + + // POTA anchor for the PTA certificate chain part_final_ex validates. + let pota = CaKey::generate(); + let policy = backing_part_policy(&pid, &pid_pub_vec, &sata_key.raw_pub(), &pota.raw_pub()); + + let init = session + .part_init_ex( + &mach_seed(), + &policy, + &pota_thumbprint(), + &sata_thumbprint(), + None, + ) + .expect("part_init_ex"); + + let chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + let certs = [ + HsmCert { + cert: &chain.root_der, + }, + HsmCert { + cert: &chain.pta_der, + }, + ]; + session + .part_final_ex(&policy, &certs, None) + .expect("part_final_ex"); + + (session, policy, pid_pub) +} + +/// Well-formed sealing key props: a `Sealing`-kind P-384 secret key +/// permitted for derivation only. +pub(crate) fn sealing_props() -> HsmKeyProps { + HsmKeyPropsBuilder::default() + .class(HsmKeyClass::Secret) + .key_kind(HsmKeyKind::Sealing) + .bits(384) + .can_derive(true) + .build() + .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) { + 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"); + + let report_data = [0u8; KEY_REPORT_DATA_LEN]; + let report_len = key + .generate_key_report(&report_data, None) + .expect("key report size"); + let mut report = vec![0u8; report_len]; + let written = key + .generate_key_report(&report_data, Some(&mut report)) + .expect("key report"); + report.truncate(written); + + (masked, report) +} diff --git a/ddi/interface/src/error.rs b/ddi/interface/src/error.rs index 044f4e663..dca99d070 100644 --- a/ddi/interface/src/error.rs +++ b/ddi/interface/src/error.rs @@ -7,6 +7,7 @@ use std::convert::Infallible; use azihsm_ddi_mbor_types::DdiStatus; use azihsm_ddi_mbor_types::MborError; +use azihsm_ddi_tbor_types::TborStatus; use thiserror::Error; use crate::*; @@ -58,6 +59,11 @@ pub enum DdiError { #[error("Manticore device error")] DdiStatus(DdiStatus), + /// Firmware-signalled TBOR command rejection, carrying the typed + /// [`TborStatus`]. + #[error("TBOR device error: {0:?} (0x{:08X})", .0.0)] + TborStatus(TborStatus), + /// Linux error #[cfg(target_os = "linux")] #[error("nix error")] @@ -147,11 +153,11 @@ impl From for DdiError { #[inline] fn from(e: azihsm_ddi_tbor_codec::DecodeError) -> Self { match e { - // FW-signalled error: surface the typed HsmError discriminant - // so callers can match on specific codes (InvalidSessionType, - // AeadEnvelopeAuthFailed, etc.) instead of losing the detail to a - // generic `TborDecodeError`. - azihsm_ddi_tbor_codec::DecodeError::FwError(status) => Self::DdiError(status), + // FW-signalled error: surface the typed TBOR status so callers + // can match on specific codes. + azihsm_ddi_tbor_codec::DecodeError::FwError(status) => { + Self::TborStatus(TborStatus(status)) + } _ => Self::TborDecodeError, } } diff --git a/ddi/tbor/types/tests/commands/open_session.rs b/ddi/tbor/types/tests/commands/open_session.rs index 3aa1851ef..93a531823 100644 --- a/ddi/tbor/types/tests/commands/open_session.rs +++ b/ddi/tbor/types/tests/commands/open_session.rs @@ -186,7 +186,7 @@ fn session_open_finish_unknown_session_id_emu() { .tbor(&req) .expect_err("finish against unknown session_id must fail"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection, got {err:?}", ); } @@ -205,7 +205,7 @@ fn open_session_double_finish_emu() { .tbor(&req) .expect_err("second finish against the same slot must fail"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection, got {err:?}", ); } diff --git a/ddi/tbor/types/tests/commands/session_close.rs b/ddi/tbor/types/tests/commands/session_close.rs index 620e6c5fc..efbce5b50 100644 --- a/ddi/tbor/types/tests/commands/session_close.rs +++ b/ddi/tbor/types/tests/commands/session_close.rs @@ -74,7 +74,7 @@ fn session_close_unknown_id_emu() { .session_close(0xFFFF) .expect_err("close of unknown id must fail"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection, got {err:?}", ); } @@ -92,7 +92,7 @@ fn session_close_double_close_emu() { .session_close(session_id) .expect_err("second close against the same id must fail"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection on double-close, got {err:?}", ); } diff --git a/ddi/tbor/types/tests/harness/assertions.rs b/ddi/tbor/types/tests/harness/assertions.rs index 83cc8a9b7..08619b22a 100644 --- a/ddi/tbor/types/tests/harness/assertions.rs +++ b/ddi/tbor/types/tests/harness/assertions.rs @@ -45,19 +45,17 @@ pub fn assert_tbor_decode_error(err: &DdiError) { /// response header `status` field. The host-side `decode_response` /// short-circuits on `status != 0` and the conversion in /// `azihsm_ddi_interface::error` maps that to -/// [`DdiError::DdiError(status)`]. If the contract ever changes, this -/// is the single site that needs updating. +/// [`DdiError::TborStatus`]. If the contract ever changes, this is the +/// single site that needs updating. #[track_caller] pub fn assert_fw_rejects(err: &DdiError, expected: TborStatus) { - let expected_code = expected.0; match err { - DdiError::DdiError(code) => assert_eq!( - *code, expected_code, - "FW rejected with wrong TborStatus: expected {expected:?} (0x{expected_code:08X}), \ - got 0x{code:08X}", - ), - other => panic!( - "expected DdiError::DdiError(0x{expected_code:08X}) for {expected:?}, got {other:?}", + DdiError::TborStatus(status) => assert_eq!( + *status, expected, + "FW rejected with wrong TborStatus: expected {expected:?} (0x{:08X}), \ + got {status:?} (0x{:08X})", + expected.0, status.0, ), + other => panic!("expected DdiError::TborStatus({expected:?}), got {other:?}"), } } diff --git a/ddi/tbor/types/tests/hw/open_session.rs b/ddi/tbor/types/tests/hw/open_session.rs index 91bb3eed8..dfbf94f04 100644 --- a/ddi/tbor/types/tests/hw/open_session.rs +++ b/ddi/tbor/types/tests/hw/open_session.rs @@ -314,7 +314,7 @@ fn finish_unknown_session_id_rejected() { assert!( matches!( err, - azihsm_ddi_interface::DdiError::DdiError(_) + azihsm_ddi_interface::DdiError::TborStatus(_) | azihsm_ddi_interface::DdiError::DdiStatus(_) ), "expected FW or driver rejection, got {err:?}", @@ -347,7 +347,7 @@ fn double_finish_rejected() { .exec_op_tbor::(&req, None, &mut cookie) .expect_err("second finish against the same slot must fail"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection on double-finish, got {err:?}", ); // Close the Active slot we established up front so this test @@ -400,7 +400,7 @@ fn session_close_unknown_session_id_rejected() { assert!( matches!( err, - azihsm_ddi_interface::DdiError::DdiError(_) + azihsm_ddi_interface::DdiError::TborStatus(_) | azihsm_ddi_interface::DdiError::DdiStatus(_) ), "expected FW or driver rejection on close-unknown, got {err:?}", @@ -438,7 +438,7 @@ fn pk_init_all_zero_rejected() { .exec_op_tbor::(&req, None, &mut cookie) .expect_err("all-zero pk_init must be rejected"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection for all-zero pk_init, got {err:?}", ); } @@ -462,7 +462,7 @@ fn pk_init_not_on_curve_rejected() { .exec_op_tbor::(&req, None, &mut cookie) .expect_err("off-curve pk_init must be rejected"); assert!( - matches!(err, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(err, azihsm_ddi_interface::DdiError::TborStatus(_)), "expected FW-side rejection for off-curve pk_init, got {err:?}", ); } @@ -501,7 +501,7 @@ fn open_session_fills_table_then_recovers() { // an FW-side rejection (not a driver / decode fault) // before ending the ramp-up. assert!( - matches!(e, azihsm_ddi_interface::DdiError::DdiError(_)), + matches!(e, azihsm_ddi_interface::DdiError::TborStatus(_)), "table-full rejection must be FW-side, got {e:?}", ); rejection_seen = true;