diff --git a/ddi/tbor/types/src/sd_create_peer_backup.rs b/ddi/tbor/types/src/sd_create_peer_backup.rs index 6e62fe98e..e8c95d60d 100644 --- a/ddi/tbor/types/src/sd_create_peer_backup.rs +++ b/ddi/tbor/types/src/sd_create_peer_backup.rs @@ -4,10 +4,12 @@ //! Host-side wrapper for the TBOR `SdCreatePeerBackup` command. //! //! `SdCreatePeerBackup` is an **in-session** command that creates a -//! peer-transferable backup of a security domain: it takes the local -//! partition-owner-key backup (`pok_local_backup`), re-masks it for the -//! destination peer named by `dst_evidence` under the named sealing key, -//! and returns the peer backup (`pok_peer_backup`). +//! peer-transferable backup of a security domain (manticore §3.3.10): it +//! recovers BKS3 from the caller's device-local backup (`pok_local_backup`) +//! and HPKE-Auth-seals it to the destination peer named by `dst_evidence` — +//! authenticated by the sender's own masked SD-sealing key — returning the +//! peer backup (`pok_peer_backup`). Peer cloning is gated by the security +//! domain's `allow_peer_cloning` policy flag. //! //! Both wire schemas are shared with the firmware handler via //! `azihsm_fw_ddi_tbor_types::sd_create_peer_backup`; this module adds the @@ -23,6 +25,9 @@ use alloc::vec::Vec; use crate::evidence::ReportDescriptor; use crate::policy::PartPolicy; +use crate::sd_create_remote_backup::MASKED_SD_LEN; +use crate::sd_create_remote_backup::POK_REMOTE_BACKUP_LEN; +use crate::sd_sealing_key_gen::MASKED_SEALING_KEY_LEN; use crate::tbor; use crate::CertDescriptor; @@ -31,21 +36,26 @@ pub const TBOR_OP_SD_CREATE_PEER_BACKUP: u8 = 0x0E; /// Host-facing TBOR `SdCreatePeerBackup` request. #[tbor(opcode = TBOR_OP_SD_CREATE_PEER_BACKUP, session_ctrl = in_session)] -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TborSdCreatePeerBackupReq { /// Session id this request is bound to. Cross-checked against the /// SQE-carried session id by the dispatcher. #[tbor(session_id)] pub session_id: u16, - /// Vault id (`HsmKeyId`) of the sealing key the `pok_local_backup` is - /// bound to. Carried as a `KeyId` (inline 16-bit, TOC entry type 1); - /// represented here as the raw `u16` handle. - #[tbor(key_id)] - pub sealing_key_id: u16, + /// The sender's masked SD-sealing key (from `SdSealingKeyGen`), exactly + /// [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to recover + /// the sender's private HPKE key (`SndrPriv`) that authenticates the + /// seal. A fixed-length `[u8; N]` field; the firmware schema is the + /// length authority. + pub masked_sealing_key: [u8; MASKED_SEALING_KEY_LEN], + + /// Unified [`PartPolicy`] describing the security domain being backed + /// up. Encoded as its 484-byte little-endian image. + pub policy: PartPolicy, /// Destination manufacturer certificate-chain descriptors. Flattened - /// from the firmware `dst_evidence` field group (its four TOC + /// from the firmware `dst_evidence` field group (first of its four TOC /// entries); the DER bytes travel out of band. #[tbor(max_len = 8)] pub dst_mfgr_cert_chain: Vec, @@ -61,26 +71,21 @@ pub struct TborSdCreatePeerBackupReq { /// Destination attestation-report (COSE_Sign1) descriptor. pub dst_report: ReportDescriptor, - /// Unified [`PartPolicy`] describing the security domain being backed - /// up. Encoded as its 484-byte little-endian image. - pub policy: PartPolicy, - - /// Local partition-owner-key backup to re-mask (a masked BKS3 wrapped - /// under the device-local key). Exactly 180 B on the wire; the - /// firmware schema is the length authority. - #[tbor(max_len = 180)] - pub pok_local_backup: Vec, + /// Device-local partition-owner-key backup (a masked BKS3 wrapped under + /// `PartLocalMK`) from which BKS3 is recovered. Exactly + /// [`MASKED_SD_LEN`] (180 B); the firmware schema is the length + /// authority. + pub pok_local_backup: [u8; MASKED_SD_LEN], } /// Host-facing TBOR `SdCreatePeerBackup` response. #[tbor(response)] -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TborSdCreatePeerBackupResp { - /// Partition-owner-key backup re-masked for the destination peer - /// (exactly 180 B on the wire; the firmware schema is the length - /// authority). - #[tbor(max_len = 180)] - pub pok_peer_backup: Vec, + /// Peer backup: an HPKE-Auth seal of BKS3 (exactly + /// [`POK_REMOTE_BACKUP_LEN`] = 161 B on the wire; the firmware schema is + /// the length authority). A fixed-length `[u8; N]` field. + pub pok_peer_backup: [u8; POK_REMOTE_BACKUP_LEN], } #[cfg(test)] @@ -89,26 +94,27 @@ mod tests { use super::*; - const POK_BACKUP_LEN: usize = 180; - #[test] fn request_encodes_fields() { let req = TborSdCreatePeerBackupReq { session_id: 9, - sealing_key_id: 0x1234, + masked_sealing_key: [0u8; MASKED_SEALING_KEY_LEN], policy: PartPolicy::zeroed(), - pok_local_backup: alloc::vec![0xABu8; POK_BACKUP_LEN], - ..Default::default() + dst_mfgr_cert_chain: Vec::new(), + dst_owner_cert_chain: Vec::new(), + dst_part_owner_cert_chain: Vec::new(), + dst_report: ReportDescriptor::default(), + pok_local_backup: [0xABu8; MASKED_SD_LEN], }; let mut buf = [0u8; 1024]; let frame = req.encode_request(&mut buf).expect("encode"); - // The 484-byte policy plus the 180-byte backup must be carried in - // the data section. + // The 484-byte policy plus the sealing key and the backup must be + // carried in the data section. assert!( - frame.len() > 484 + POK_BACKUP_LEN, - "encoded frame must carry the policy and backup" + frame.len() > 484 + MASKED_SEALING_KEY_LEN + MASKED_SD_LEN, + "encoded frame must carry the policy, key, and backup" ); } } diff --git a/ddi/tbor/types/src/sd_restore_peer_backup.rs b/ddi/tbor/types/src/sd_restore_peer_backup.rs index c408f94ce..f89eb792f 100644 --- a/ddi/tbor/types/src/sd_restore_peer_backup.rs +++ b/ddi/tbor/types/src/sd_restore_peer_backup.rs @@ -4,19 +4,17 @@ //! Host-side wrapper for the TBOR `SdRestorePeerBackup` command. //! //! `SdRestorePeerBackup` is an **in-session** command that restores a -//! security domain from a peer backup: it unmasks the caller-supplied -//! peer partition-owner-key backup (`pok_peer_backup`, a masked BKS3) -//! under the named sealing key, re-wraps it under the device-local key, -//! and returns the local backup (`pok_local_backup`) together with the -//! security-domain masking-key backup (`sd_mk_backup`). +//! security domain from a **peer** backup (manticore §3.3.11): it +//! HPKE-Auth-opens the caller-supplied `pok_peer_backup` (an HPKE seal of +//! BKS3) with the masked receiver key — authenticated by the sender peer's +//! attested key — recovers `SDMK` from `prev_sd_mk_backup`, and returns the +//! device-local backups (`pok_local_backup`, `sd_mk_backup`). It is +//! `SdRestoreRemoteBackup` plus a peer-cloning policy gate. //! //! Both wire schemas are shared with the firmware handler via -//! `azihsm_fw_ddi_tbor_types::sd_restore_peer_backup`; this module adds -//! the host-facing value types so [`exec_op_tbor`] returns owned response -//! values. The firmware splices the source attestation evidence in as an -//! `Evidence` field group; the host derive has no field-group support, so -//! this wrapper spells those four TOC entries out explicitly as the -//! `src_*` cert-chain / report descriptor fields. +//! `azihsm_fw_ddi_tbor_types::sd_restore_peer_backup`; this module adds the +//! host-facing value types so [`exec_op_tbor`] returns owned response +//! values. //! //! [`exec_op_tbor`]: ../../azihsm_ddi_interface/trait.DdiDev.html#method.exec_op_tbor @@ -24,6 +22,9 @@ use alloc::vec::Vec; use crate::evidence::ReportDescriptor; use crate::policy::PartPolicy; +use crate::sd_create_remote_backup::POK_REMOTE_BACKUP_LEN; +use crate::sd_create_remote_backup::SD_MK_BACKUP_LEN; +use crate::sd_sealing_key_gen::MASKED_SEALING_KEY_LEN; use crate::tbor; use crate::CertDescriptor; @@ -32,50 +33,48 @@ pub const TBOR_OP_SD_RESTORE_PEER_BACKUP: u8 = 0x0F; /// Host-facing TBOR `SdRestorePeerBackup` request. #[tbor(opcode = TBOR_OP_SD_RESTORE_PEER_BACKUP, session_ctrl = in_session)] -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TborSdRestorePeerBackupReq { /// Session id this request is bound to. Cross-checked against the /// SQE-carried session id by the dispatcher. #[tbor(session_id)] pub session_id: u16, - /// Vault id (`HsmKeyId`) of the sealing key the `pok_peer_backup` is - /// bound to. Carried as a `KeyId` (inline 16-bit, TOC entry type 1); - /// represented here as the raw `u16` handle. - #[tbor(key_id)] - pub sealing_key_id: u16, + /// The receiver's masked SD-sealing key (from `SdSealingKeyGen`), + /// exactly [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to + /// recover the receiver's private HPKE key (`RcvrPriv`). + pub masked_sealing_key: [u8; MASKED_SEALING_KEY_LEN], - /// Source manufacturer certificate-chain descriptors. Flattened from - /// the firmware `src_evidence` field group (its four TOC entries); the - /// DER bytes travel out of band. + /// Unified [`PartPolicy`] describing the security domain being + /// restored. Encoded as its 484-byte little-endian image. + pub policy: PartPolicy, + + /// Source peer manufacturer certificate-chain descriptors. Flattened + /// from the firmware `src_evidence` field group (first of its four TOC + /// entries); the DER bytes travel out of band. #[tbor(max_len = 8)] pub src_mfgr_cert_chain: Vec, - /// Source owner certificate-chain descriptors. + /// Source peer owner certificate-chain descriptors. #[tbor(max_len = 8)] pub src_owner_cert_chain: Vec, - /// Source partition-owner certificate-chain descriptors. + /// Source peer partition-owner certificate-chain descriptors. #[tbor(max_len = 8)] pub src_part_owner_cert_chain: Vec, - /// Source attestation-report (COSE_Sign1) descriptor. + /// Source peer attestation-report (COSE_Sign1) descriptor. pub src_report: ReportDescriptor, - /// Unified [`PartPolicy`] describing the security domain being - /// restored. Encoded as its 484-byte little-endian image. - pub policy: PartPolicy, - - /// Peer partition-owner-key backup to restore (a masked BKS3). - /// Exactly 180 B on the wire; the firmware schema is the length + /// Peer backup to restore: an HPKE-Auth seal of BKS3, exactly + /// [`POK_REMOTE_BACKUP_LEN`] (161 B). The firmware schema is the length /// authority. - #[tbor(max_len = 180)] - pub pok_peer_backup: Vec, + pub pok_peer_backup: [u8; POK_REMOTE_BACKUP_LEN], - /// Security-domain masking-key backup envelope. Exactly 164 B on the - /// wire; the firmware schema is the length authority. - #[tbor(max_len = 164)] - pub sd_mk_backup: Vec, + /// Previous security-domain masking-key backup (SDMK masked under the + /// derived SDBMK), exactly [`SD_MK_BACKUP_LEN`] (164 B), from which + /// `SDMK` is recovered. + pub prev_sd_mk_backup: [u8; SD_MK_BACKUP_LEN], } /// Host-facing TBOR `SdRestorePeerBackup` response. @@ -100,28 +99,28 @@ mod tests { use super::*; - const POK_BACKUP_LEN: usize = 180; - const SD_MK_BACKUP_LEN: usize = 164; - #[test] fn request_encodes_all_fields() { let req = TborSdRestorePeerBackupReq { session_id: 9, - sealing_key_id: 0x1234, + masked_sealing_key: [0u8; MASKED_SEALING_KEY_LEN], policy: PartPolicy::zeroed(), - pok_peer_backup: alloc::vec![0xABu8; POK_BACKUP_LEN], - sd_mk_backup: alloc::vec![0xCDu8; SD_MK_BACKUP_LEN], - ..Default::default() + src_mfgr_cert_chain: Vec::new(), + src_owner_cert_chain: Vec::new(), + src_part_owner_cert_chain: Vec::new(), + src_report: ReportDescriptor::default(), + pok_peer_backup: [0xABu8; POK_REMOTE_BACKUP_LEN], + prev_sd_mk_backup: [0xCDu8; SD_MK_BACKUP_LEN], }; - let mut buf = [0u8; 1024]; + let mut buf = [0u8; 2048]; let frame = req.encode_request(&mut buf).expect("encode"); - // The 484-byte policy plus the two backup blobs must be carried in - // the data section. + // The 484-byte policy plus the sealing key and the two backups must + // be carried in the data section. assert!( - frame.len() > 484 + POK_BACKUP_LEN + SD_MK_BACKUP_LEN, - "encoded frame must carry the policy and backups" + frame.len() > 484 + MASKED_SEALING_KEY_LEN + POK_REMOTE_BACKUP_LEN + SD_MK_BACKUP_LEN, + "encoded frame must carry the policy, key, and backups" ); } } diff --git a/ddi/tbor/types/src/status.rs b/ddi/tbor/types/src/status.rs index 202d34732..7d314a018 100644 --- a/ddi/tbor/types/src/status.rs +++ b/ddi/tbor/types/src/status.rs @@ -317,6 +317,12 @@ pub enum TborStatus { /// backup whose bound SVN is newer than the current firmware SVN /// (mirror of `HsmError::SdBackupSvnRollback`). SdBackupSvnRollback = 0x08700109, + + /// A `SdCreatePeerBackup` / `SdRestorePeerBackup` handler was asked to + /// clone a security domain to (or from) a peer, but the partition's + /// policy does not permit peer cloning (mirror of + /// `HsmError::SdPeerCloningNotAllowed`). + SdPeerCloningNotAllowed = 0x0870010A, } impl core::fmt::Debug for TborStatus { diff --git a/ddi/tbor/types/tests/commands/mod.rs b/ddi/tbor/types/tests/commands/mod.rs index d14fa6ad2..b14278c44 100644 --- a/ddi/tbor/types/tests/commands/mod.rs +++ b/ddi/tbor/types/tests/commands/mod.rs @@ -15,9 +15,11 @@ pub mod part_final; pub mod part_info; pub mod part_init; pub mod psk_change; +pub mod sd_create_peer_backup; pub mod sd_create_remote_backup; pub mod sd_reseal_remote_backup; pub mod sd_restore_local_backup; +pub mod sd_restore_peer_backup; pub mod sd_restore_remote_backup; pub mod sd_sealing_key_gen; pub mod session_close; diff --git a/ddi/tbor/types/tests/commands/sd_create_peer_backup.rs b/ddi/tbor/types/tests/commands/sd_create_peer_backup.rs new file mode 100644 index 000000000..002fb0ec6 --- /dev/null +++ b/ddi/tbor/types/tests/commands/sd_create_peer_backup.rs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Integration tests for the TBOR `SdCreatePeerBackup` command. +//! +//! `SdCreatePeerBackup` recovers BKS3 from the caller's device-local +//! backup (`pok_local_backup`) and HPKE-Auth-seals it to a destination +//! peer named by `dst_evidence` — authenticated by the sender's own masked +//! SD-sealing key — returning the peer backup (`pok_peer_backup`, a 161-byte +//! HPKE seal). It is **stateless** and gated by the security domain's +//! `allow_peer_cloning` policy flag. +//! +//! These tests run a **self-peer** backup (sender == receiver): one +//! partition mints an SD sealing key, attests it via `KeyReport`, creates +//! its security domain, and then re-seals the recovered BKS3 to its own +//! attested public key. +//! +//! Coverage: +//! * Round-trip — non-zero 161-byte `pok_peer_backup`. +//! * Policy without `allow_peer_cloning` → `SdPeerCloningNotAllowed`. +//! * Not finalized → `InvalidArg`. + +#![cfg(feature = "emu")] + +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::TborPartInfoReq; +use azihsm_ddi_tbor_types::TborSdCreatePeerBackupReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use zerocopy::TryFromBytes; + +use crate::commands::part_init::bootstrap_rotated_co; +use crate::commands::part_init::mach_seed; +use crate::commands::part_init::pota_thumbprint; +use crate::commands::part_init::ROTATED_CO_PSK; +use crate::commands::sd_create_remote_backup::backing_part_policy; +use crate::commands::sd_create_remote_backup::backup_request; +use crate::commands::sd_create_remote_backup::build_receiver_evidence; +use crate::commands::sd_create_remote_backup::masked_key_and_report; +use crate::commands::sd_create_remote_backup::ReceiverEvidence; +use crate::harness::x509_fixture::make_pta_chain; +use crate::harness::x509_fixture::pta_pub_from_csr; +use crate::harness::x509_fixture::CaKey; +use crate::harness::x509_fixture::RAW_PUB_LEN; +use crate::harness::SessionHandshake; +use crate::harness::TestCtx; + +/// Byte offset of the `flags` field in the 484-byte `PartPolicy` image. +const OFF_FLAGS: usize = 418; + +/// `PolicyFlags::ALLOW_PEER_CLONING` (bit 2) — mirror of the firmware +/// policy flag that gates the peer-backup commands. +const ALLOW_PEER_CLONING: u8 = 1 << 2; + +/// A finalized backing partition ready for the peer-backup commands: the +/// live CO session, its exact policy image, its PID public key (every +/// evidence leaf certifies it), and `PartFinal`'s `local_mk_backup` +/// (replayed on a rebooted peer to restore `PartLocalMK`). +pub(crate) struct PeerPartition { + pub(crate) session: SessionHandshake, + pub(crate) policy: [u8; PART_POLICY_LEN], + pub(crate) pid_pub: [u8; RAW_PUB_LEN], + pub(crate) local_mk_backup: Vec, +} + +/// Drive `PartInit → PartFinal` on `ctx` with a backing-partition policy +/// anchored to `sata`/`pota`, optionally opting the security domain into +/// peer cloning. The same policy image must be replayed verbatim by the +/// peer-backup commands (for the `policy_hash` re-check). +pub(crate) fn finalize_peer_partition( + ctx: &TestCtx, + seed: &[u8], + sata: &CaKey, + pota: &CaKey, + allow_cloning: bool, +) -> PeerPartition { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + + let info = ctx.tbor(&TborPartInfoReq::new()).expect("PartInfo"); + let mut pid_pub = [0u8; RAW_PUB_LEN]; + pid_pub.copy_from_slice(&info.pid_pub_key); + + let mut policy = backing_part_policy( + &info.pid, + &info.pid_pub_key, + &sata.raw_pub(), + &pota.raw_pub(), + ); + if allow_cloning { + policy[OFF_FLAGS] |= ALLOW_PEER_CLONING; + } + + let init = ctx + .part_init(&session, seed, &policy, &pota_thumbprint()) + .expect("PartInit"); + let chain = make_pta_chain(pota, &pta_pub_from_csr(&init.pta_csr)); + let local_mk_backup = ctx + .part_final(&session, &policy, &[], &chain.der_items()) + .expect("PartFinal") + .local_mk_backup; + + PeerPartition { + session, + policy, + pid_pub, + local_mk_backup, + } +} + +/// Assemble a `SdCreatePeerBackup` request from a masked sealing key, +/// destination evidence, policy, and the local backup to re-seal. +pub(crate) fn create_peer_req( + session_id: u16, + masked_sealing_key: &[u8], + evidence: &ReceiverEvidence, + policy: &[u8; PART_POLICY_LEN], + pok_local_backup: &[u8; MASKED_SD_LEN], +) -> TborSdCreatePeerBackupReq { + TborSdCreatePeerBackupReq { + session_id, + masked_sealing_key: masked_sealing_key + .try_into() + .expect("masked sealing key is exactly MASKED_SEALING_KEY_LEN bytes"), + policy: PartPolicy::try_read_from_bytes(policy).expect("policy image is canonical"), + dst_mfgr_cert_chain: evidence.mfgr.clone(), + dst_owner_cert_chain: evidence.owner.clone(), + dst_part_owner_cert_chain: evidence.part_owner.clone(), + dst_report: evidence.report, + pok_local_backup: *pok_local_backup, + } +} + +#[test] +fn sd_create_peer_backup_roundtrip_emu() { + let ctx = TestCtx::new(); + let sata = CaKey::generate(); + let pota = CaKey::generate(); + + let part = finalize_peer_partition(&ctx, &mach_seed(), &sata, &pota, true); + let session_id = part.session.session_id; + + // Mint + attest a sealing key, then create the security domain to + // obtain the device-local backup this command re-seals. + let (masked, report) = masked_key_and_report(&ctx, session_id); + let evidence = build_receiver_evidence(&part.pid_pub, &sata, &report); + let created = ctx + .tbor_oob( + &backup_request(session_id, masked.clone(), &evidence, &part.policy), + &evidence.oob(), + ) + .expect("SdCreateRemoteBackup"); + + let req = create_peer_req( + session_id, + &masked, + &evidence, + &part.policy, + &created.pok_local_backup, + ); + let resp = ctx + .tbor_oob(&req, &evidence.oob()) + .expect("SdCreatePeerBackup roundtrip"); + + assert_eq!(resp.pok_peer_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!( + resp.pok_peer_backup.iter().any(|&b| b != 0), + "pok_peer_backup must not be all-zero", + ); +} + +#[test] +fn sd_create_peer_backup_rejects_without_peer_cloning_emu() { + let ctx = TestCtx::new(); + let sata = CaKey::generate(); + let pota = CaKey::generate(); + + // Finalize with a policy that does NOT opt into peer cloning. + let part = finalize_peer_partition(&ctx, &mach_seed(), &sata, &pota, false); + let session_id = part.session.session_id; + + // A real sealing key + evidence so the request reaches the policy gate; + // the peer-cloning check fires before any local backup is unmasked, so + // a zero `pok_local_backup` is sufficient. + let (masked, report) = masked_key_and_report(&ctx, session_id); + let evidence = build_receiver_evidence(&part.pid_pub, &sata, &report); + let req = create_peer_req( + session_id, + &masked, + &evidence, + &part.policy, + &[0u8; MASKED_SD_LEN], + ); + ctx.expect_fw_reject_oob(&req, &evidence.oob(), TborStatus::SdPeerCloningNotAllowed); +} + +#[test] +fn sd_create_peer_backup_rejects_before_finalize_emu() { + // A partition that has not been finalized is rejected at the lifecycle + // gate before any evidence or crypto work. + let ctx = TestCtx::new(); + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + let req = TborSdCreatePeerBackupReq { + session_id: session.session_id, + masked_sealing_key: [0u8; 180], + policy: PartPolicy::zeroed(), + dst_mfgr_cert_chain: Vec::new(), + dst_owner_cert_chain: Vec::new(), + dst_part_owner_cert_chain: Vec::new(), + dst_report: Default::default(), + pok_local_backup: [0u8; MASKED_SD_LEN], + }; + ctx.expect_fw_reject(&req, TborStatus::InvalidArg); +} diff --git a/ddi/tbor/types/tests/commands/sd_restore_peer_backup.rs b/ddi/tbor/types/tests/commands/sd_restore_peer_backup.rs new file mode 100644 index 000000000..2961cdee8 --- /dev/null +++ b/ddi/tbor/types/tests/commands/sd_restore_peer_backup.rs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Integration tests for the TBOR `SdRestorePeerBackup` command. +//! +//! `SdRestorePeerBackup` restores a security domain from a **peer** backup: +//! it HPKE-Auth-opens the caller-supplied `pok_peer_backup` (an HPKE seal +//! of BKS3) with the receiver's masked SD-sealing key — authenticated by +//! the sender peer's attested key — recovers `SDMK` from `prev_sd_mk_backup`, +//! and returns the device-local backups. It is `SdRestoreRemoteBackup` +//! plus an `allow_peer_cloning` policy gate. +//! +//! The **round-trip** test uses a self-peer backup (sender == receiver): a +//! first device finalizes, `CreateSD`s, and `CreatePeerBackup`s (producing +//! the `pok_peer_backup` / `prev_sd_mk_backup` this command consumes, plus +//! the `local_mk_backup`), then a second device (factory-reset, same machine +//! seed) restores `PartLocalMK` via `PartFinal` — so it can unmask the +//! captured sealing key — and restores the security domain from the peer +//! backup. +//! +//! Coverage: +//! * Round-trip — create-peer → reboot → PartFinal(restore PartLocalMK) → +//! restore-peer returns non-zero refreshed local backups. +//! * One-shot — restore onto an already-initialized SD → `SdAlreadyInitialized`. +//! * Policy without `allow_peer_cloning` → `SdPeerCloningNotAllowed`. +//! * Restore before finalize → `InvalidArg`. + +#![cfg(feature = "emu")] + +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::TborSdRestorePeerBackupReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; +use zerocopy::TryFromBytes; + +use crate::commands::part_init::bootstrap_rotated_co; +use crate::commands::part_init::mach_seed; +use crate::commands::part_init::pota_thumbprint; +use crate::commands::part_init::ROTATED_CO_PSK; +use crate::commands::sd_create_peer_backup::create_peer_req; +use crate::commands::sd_create_peer_backup::finalize_peer_partition; +use crate::commands::sd_create_remote_backup::backup_request; +use crate::commands::sd_create_remote_backup::build_receiver_evidence; +use crate::commands::sd_create_remote_backup::masked_key_and_report; +use crate::commands::sd_create_remote_backup::ReceiverEvidence; +use crate::harness::x509_fixture::make_pta_chain; +use crate::harness::x509_fixture::pta_pub_from_csr; +use crate::harness::x509_fixture::CaKey; +use crate::harness::TestCtx; + +/// A peer backup produced by the first device's `CreatePeerBackup`, +/// replayed on the second (rebooted) device to restore the security domain. +struct PeerBackup { + /// The receiver's masked SD-sealing key (used to open the backup). + masked_sealing_key: Vec, + /// Sender attestation evidence (OOB items + descriptors). In a + /// self-peer backup this is the same evidence the seal used. + evidence: ReceiverEvidence, + /// The 484-byte `PartPolicy` image (with `allow_peer_cloning` set). + policy: [u8; PART_POLICY_LEN], + /// `pok_peer_backup` from `CreatePeerBackup` — the HPKE seal to restore. + pok_peer_backup: [u8; POK_REMOTE_BACKUP_LEN], + /// `sd_mk_backup` from `CreateSD` — the previous SDMK backup. + prev_sd_mk_backup: [u8; SD_MK_BACKUP_LEN], + /// `PartFinal`'s `local_mk_backup`, replayed to restore `PartLocalMK`. + local_mk_backup: Vec, +} + +/// Drive device 1: finalize a cloning-enabled backing partition, `CreateSD`, +/// and `CreatePeerBackup`, capturing everything device 2 needs to restore +/// the domain from the peer backup. +fn create_peer_backup(seed: &[u8], sata: &CaKey, pota: &CaKey) -> PeerBackup { + let ctx = TestCtx::new(); + let part = finalize_peer_partition(&ctx, seed, sata, pota, true); + let session_id = part.session.session_id; + + let (masked, report) = masked_key_and_report(&ctx, session_id); + let evidence = build_receiver_evidence(&part.pid_pub, sata, &report); + let created = ctx + .tbor_oob( + &backup_request(session_id, masked.clone(), &evidence, &part.policy), + &evidence.oob(), + ) + .expect("SdCreateRemoteBackup"); + let peer = ctx + .tbor_oob( + &create_peer_req( + session_id, + &masked, + &evidence, + &part.policy, + &created.pok_local_backup, + ), + &evidence.oob(), + ) + .expect("SdCreatePeerBackup"); + + PeerBackup { + masked_sealing_key: masked, + evidence, + policy: part.policy, + pok_peer_backup: peer.pok_peer_backup, + prev_sd_mk_backup: created.sd_mk_backup, + local_mk_backup: part.local_mk_backup, + } +} + +/// Assemble a `SdRestorePeerBackup` request from a captured backup. +fn restore_peer_req(session_id: u16, backup: &PeerBackup) -> TborSdRestorePeerBackupReq { + TborSdRestorePeerBackupReq { + session_id, + masked_sealing_key: backup + .masked_sealing_key + .as_slice() + .try_into() + .expect("masked sealing key is exactly MASKED_SEALING_KEY_LEN bytes"), + policy: PartPolicy::try_read_from_bytes(&backup.policy).expect("policy image is canonical"), + src_mfgr_cert_chain: backup.evidence.mfgr.clone(), + src_owner_cert_chain: backup.evidence.owner.clone(), + src_part_owner_cert_chain: backup.evidence.part_owner.clone(), + src_report: backup.evidence.report, + pok_peer_backup: backup.pok_peer_backup, + prev_sd_mk_backup: backup.prev_sd_mk_backup, + } +} + +#[test] +fn sd_restore_peer_backup_roundtrip_emu() { + let seed = mach_seed(); + let sata = CaKey::generate(); + let pota = CaKey::generate(); + + // Device 1: finalize + CreateSD + CreatePeerBackup, capturing the peer + // backup. + let backup = create_peer_backup(&seed, &sata, &pota); + + // Device 2 (reboot): restore PartLocalMK (so the captured sealing key + // unmasks), then restore the SD from the peer backup. + let ctx = TestCtx::new(); + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + let init = ctx + .part_init(&session, &seed, &backup.policy, &pota_thumbprint()) + .expect("PartInit (device 2)"); + let chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + ctx.part_final( + &session, + &backup.policy, + &backup.local_mk_backup, + &chain.der_items(), + ) + .expect("PartFinal must restore PartLocalMK from the prior backup"); + + let req = restore_peer_req(session.session_id, &backup); + let resp = ctx + .tbor_oob(&req, &backup.evidence.oob()) + .expect("SdRestorePeerBackup roundtrip"); + + // Local backup (BKS3 masked under PartLocalMK), 180 B, non-zero. + assert_eq!(resp.pok_local_backup.len(), MASKED_SD_LEN); + assert!( + resp.pok_local_backup.iter().any(|&b| b != 0), + "pok_local_backup must not be all-zero", + ); + // Refreshed masking-key backup (SDMK re-masked under SDBMK), 164 B. + assert_eq!(resp.sd_mk_backup.len(), SD_MK_BACKUP_LEN); + assert!( + resp.sd_mk_backup.iter().any(|&b| b != 0), + "sd_mk_backup must not be all-zero", + ); +} + +#[test] +fn sd_restore_peer_backup_is_one_shot_emu() { + let ctx = TestCtx::new(); + let sata = CaKey::generate(); + let pota = CaKey::generate(); + + // A single device that has just created its SD (via CreateSD) is already + // SD-initialized, so a peer restore on the same incarnation is rejected + // by the one-shot gate. + let part = finalize_peer_partition(&ctx, &mach_seed(), &sata, &pota, true); + let session_id = part.session.session_id; + + let (masked, report) = masked_key_and_report(&ctx, session_id); + let evidence = build_receiver_evidence(&part.pid_pub, &sata, &report); + let created = ctx + .tbor_oob( + &backup_request(session_id, masked.clone(), &evidence, &part.policy), + &evidence.oob(), + ) + .expect("SdCreateRemoteBackup"); + let peer = ctx + .tbor_oob( + &create_peer_req( + session_id, + &masked, + &evidence, + &part.policy, + &created.pok_local_backup, + ), + &evidence.oob(), + ) + .expect("SdCreatePeerBackup"); + + let backup = PeerBackup { + masked_sealing_key: masked, + evidence, + policy: part.policy, + pok_peer_backup: peer.pok_peer_backup, + prev_sd_mk_backup: created.sd_mk_backup, + local_mk_backup: Vec::new(), + }; + let req = restore_peer_req(session_id, &backup); + ctx.expect_fw_reject_oob( + &req, + &backup.evidence.oob(), + TborStatus::SdAlreadyInitialized, + ); +} + +#[test] +fn sd_restore_peer_backup_rejects_without_peer_cloning_emu() { + let ctx = TestCtx::new(); + let sata = CaKey::generate(); + let pota = CaKey::generate(); + + // Finalize with a policy that does NOT opt into peer cloning. The + // peer-cloning gate fires in phase 1 (after the policy-hash re-check) + // before any HPKE work, so dummy backup blobs are sufficient — but a + // real sealing key and OOB evidence are needed to reach the gate. + let part = finalize_peer_partition(&ctx, &mach_seed(), &sata, &pota, false); + let session_id = part.session.session_id; + + let (masked, report) = masked_key_and_report(&ctx, session_id); + let evidence = build_receiver_evidence(&part.pid_pub, &sata, &report); + let backup = PeerBackup { + masked_sealing_key: masked, + evidence, + policy: part.policy, + pok_peer_backup: [0u8; POK_REMOTE_BACKUP_LEN], + prev_sd_mk_backup: [0u8; SD_MK_BACKUP_LEN], + local_mk_backup: Vec::new(), + }; + let req = restore_peer_req(session_id, &backup); + ctx.expect_fw_reject_oob( + &req, + &backup.evidence.oob(), + TborStatus::SdPeerCloningNotAllowed, + ); +} + +#[test] +fn sd_restore_peer_backup_rejects_before_finalize_emu() { + // A partition that has not been finalized is rejected at the lifecycle + // gate before any evidence or crypto work. + let ctx = TestCtx::new(); + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + let req = TborSdRestorePeerBackupReq { + session_id: session.session_id, + masked_sealing_key: [0u8; 180], + policy: PartPolicy::zeroed(), + src_mfgr_cert_chain: Vec::new(), + src_owner_cert_chain: Vec::new(), + src_part_owner_cert_chain: Vec::new(), + src_report: Default::default(), + pok_peer_backup: [0u8; POK_REMOTE_BACKUP_LEN], + prev_sd_mk_backup: [0u8; SD_MK_BACKUP_LEN], + }; + ctx.expect_fw_reject(&req, TborStatus::InvalidArg); +} diff --git a/docs/tbor-ddi/README.md b/docs/tbor-ddi/README.md index de7b57070..9c1b1e3b7 100644 --- a/docs/tbor-ddi/README.md +++ b/docs/tbor-ddi/README.md @@ -62,8 +62,8 @@ single `none` TOC placeholder and no typed body fields. | `0x0B` | `SdResealRemoteBackup` | InSession | [`commands/sd_reseal_remote_backup.md`](./commands/sd_reseal_remote_backup.md) | | `0x0C` | `SdRestoreRemoteBackup` | InSession | [`commands/sd_restore_remote_backup.md`](./commands/sd_restore_remote_backup.md) | | `0x0D` | `SdRestoreLocalBackup` | InSession | [`commands/sd_restore_local_backup.md`](./commands/sd_restore_local_backup.md) | -| `0x0E` | `SdCreatePeerBackup` (schema-only) | InSession | [`commands/sd_create_peer_backup.md`](./commands/sd_create_peer_backup.md) | -| `0x0F` | `SdRestorePeerBackup` (schema-only) | InSession | [`commands/sd_restore_peer_backup.md`](./commands/sd_restore_peer_backup.md) | +| `0x0E` | `SdCreatePeerBackup` | InSession | [`commands/sd_create_peer_backup.md`](./commands/sd_create_peer_backup.md) | +| `0x0F` | `SdRestorePeerBackup` | InSession | [`commands/sd_restore_peer_backup.md`](./commands/sd_restore_peer_backup.md) | | `0x10` | `KeyReport` | InSession | [`commands/key_report.md`](./commands/key_report.md) | ## Default-PSK gate diff --git a/docs/tbor-ddi/commands/sd_create_peer_backup.md b/docs/tbor-ddi/commands/sd_create_peer_backup.md index 27dda0269..961c4903a 100644 --- a/docs/tbor-ddi/commands/sd_create_peer_backup.md +++ b/docs/tbor-ddi/commands/sd_create_peer_backup.md @@ -5,15 +5,50 @@ Licensed under the MIT License. # SdCreatePeerBackup (Opcode 0x0E) -**Handler:** _Not yet landed — wire schema only._ -**Session:** InSession +**Handler:** `fw/core/lib/src/ddi/tbor/sd_create_peer_backup.rs` +**Session:** InSession (Crypto Officer) ## Description -Creates a peer-transferable backup of a security domain: takes the local -partition-owner-key backup (`pok_local_backup`), re-masks it for the -destination peer named by `dst_evidence` under the named sealing key, and -returns the peer backup (`pok_peer_backup`). +Creates a **peer-transferable** backup of a security domain (manticore +§3.3.10): it recovers BKS3 from the caller's device-local backup +(`pok_local_backup`) and HPKE-Auth-seals it to a destination peer — named +by `dst_evidence` and authenticated by the sender's own masked SD-sealing +key — returning the peer backup (`pok_peer_backup`). + +It is **[`SdCreateRemoteBackup`](sd_create_remote_backup.md)'s +HPKE-Auth-seal front-end over a recovered (not freshly minted) BKS3**, +sharing the BKS3-recovery primitive with +[`SdRestoreLocalBackup`](sd_restore_local_backup.md) +(`fw/core/lib/src/ddi/tbor/sd_backup.rs`). + +Algorithm: + +1. Gate to a Crypto-Officer, `Active` session on an `Initialized` + partition (`PartLocalMK` and the policy hash are bound by `PartFinal`). + Unlike the restores this is **not** one-shot and does not touch `SDMK`, + so it neither requires nor sets the SD-initialized flag — a rebooted + partition can clone to a peer after `PartFinal` without first restoring + the SD locally. +2. Bind the caller-supplied `policy` to the partition's fixed `policy_hash`, + then require its `allow_peer_cloning` flag (`SdPeerCloningNotAllowed`). +3. Verify the **destination** peer evidence against that policy: the + manufacturer / owner / partition-owner certificate chains are validated + and anchored to the policy `SATA` key, the report's v2 `policy_hash` + must equal `SHA-384(policy)`, and its attested COSE_Key is recovered as + **`RcvrPub`**. +4. Unmask `masked_sealing_key` under its scope's masking key → the sender's + private HPKE key **`SndrPriv`** (must be an `SdSealing` key), and derive + `SndrPub` on-device. +5. Recover **BKS3** from `pok_local_backup` (unmask under `PartLocalMK`; + must be an `SdPartitionOwnerSeed` envelope whose bound SVN is not newer + than the current firmware SVN). +6. HPKE-Auth-seal BKS3 to `RcvrPub` with `SndrPriv` as the + sender-authentication key, returning `pok_peer_backup` (161 B). + `SndrPriv` and BKS3 are zeroized before returning. + +The command is **stateless**: no vault writes, no partition-state +mutation, no undo log. ## Request @@ -25,13 +60,13 @@ variable-length data section. | Offset | Field | Type | Description | |---|---|---|---| | 4 | `session_id` | `session_id` (inline) | Session this request is bound to; cross-checked against the SQE-carried session id. | -| 8 | `sealing_key_id` | `key_id` (inline) | Vault id (`HsmKeyId`) of the sealing key the `pok_local_backup` is bound to (`KeyId`, TOC entry type 1). | -| 12 | `mfgr_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination manufacturer certificate-chain descriptors (from the `dst_evidence` field group). | -| 16 | `owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination owner certificate-chain descriptors. | -| 20 | `part_owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination partition-owner certificate-chain descriptors. | -| 24 | `evidence` | `buffer` (single `&ReportDescriptor`, 4 B) | Destination attestation-report (COSE_Sign1) descriptor. | -| 28 | `policy` | `buffer` (fixed 484 B) | Caller-asserted unified `PartPolicy` describing the security domain being backed up. Length pinned to `PART_POLICY_LEN` (484 B); a wrong length is rejected at decode. | -| 32 | `pok_local_backup` | `buffer` (fixed 180 B) | Local partition-owner-key backup to re-mask (a masked BKS3 wrapped under the device-local key) = `MASKED_SD_LEN` (180 B). | +| 8 | `masked_sealing_key` | `buffer` (fixed 180 B) | The **sender's** masked SD-sealing key (from [`SdSealingKeyGen`](sd_sealing_key_gen.md)); unmasked on-device to recover `SndrPriv`. Length pinned to `MASKED_SEALING_KEY_LEN` (180 B). Never a vault handle. | +| 12 | `policy` | `buffer` (fixed 484 B) | Caller-asserted unified `PartPolicy` describing the security domain being backed up. Length pinned to `PART_POLICY_LEN` (484 B); its SHA-384 digest must equal the partition's bound `policy_hash` and the receiver report's v2 `policy_hash`. | +| 16 | `mfgr_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination manufacturer certificate-chain descriptors (from the `dst_evidence` field group). | +| 20 | `owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination owner certificate-chain descriptors. | +| 24 | `part_owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Destination partition-owner certificate-chain descriptors. | +| 28 | `evidence` | `buffer` (single `&ReportDescriptor`, 4 B) | Destination attestation-report (COSE_Sign1) descriptor. | +| 32 | `pok_local_backup` | `buffer` (fixed 180 B) | Device-local partition-owner-key backup (a masked BKS3 wrapped under `PartLocalMK`) from which BKS3 is recovered = `MASKED_SD_LEN` (180 B). | The four `mfgr_cert_chain` … `evidence` entries are spliced in by the shared [`Evidence`](../../../fw/core/ddi/tbor/types/src/evidence.rs) @@ -41,33 +76,43 @@ COSE_Sign1 report travel **out of band**, referenced by these ### Data section -Carries the packed destination cert-chain and report descriptors, the -484-byte `policy` image, and the 180-byte `pok_local_backup` blob. +Carries the 180-byte `masked_sealing_key`, the 484-byte `policy` image, +the packed destination cert-chain and report descriptors, and the 180-byte +`pok_local_backup` blob. ## Response -Wire layout: 8-byte header, followed by the TOC entry, then the data +Wire layout: 8-byte header, followed by the TOC entries, then the data section. ### TOC entries | Offset | Field | Type | Description | |---|---|---|---| -| 8 | `pok_peer_backup` | `buffer` (fixed 180 B) | Partition-owner-key backup re-masked for the destination peer, sized as a masked BKS3 = `MASKED_SD_LEN` (180 B). | +| 8 | `pok_peer_backup` | `buffer` (fixed 161 B) | Peer backup: an HPKE-Auth seal of BKS3 to the destination peer = `POK_REMOTE_BACKUP_LEN` (161 B). | ### Data section -Carries the 180-byte `pok_peer_backup` blob. +Carries the 161-byte `pok_peer_backup` seal. ## Errors | Error | Cause | |---|---| -| `TborInvalidFixedLength` | `policy` is not exactly 484 B, or `pok_local_backup` is not exactly 180 B (rejected at decode before the handler runs) | -| `SessionNotFound` | `session_id` does not refer to an allocated slot | -| `DdiDecodeFailed` | Malformed request body | +| `TborInvalidFixedLength` | `masked_sealing_key` ≠ 180 B, `policy` ≠ 484 B, or `pok_local_backup` ≠ 180 B (rejected at decode before the handler runs) | +| `InvalidArg` | Partition is not `Initialized` (not finalized); the policy `SATA` key is not P-384; the destination report's `policy_hash` ≠ `SHA-384(policy)`; or the missing OOB evidence page | +| `SdPeerCloningNotAllowed` | The partition's policy does not set `allow_peer_cloning` | +| `SdBackupSvnRollback` | `pok_local_backup`'s bound SVN is newer than the current firmware SVN (anti-rollback) | +| `UnsupportedKeyType` | `masked_sealing_key` is not an `SdSealing` key, or `pok_local_backup` is not an `SdPartitionOwnerSeed` envelope | +| `AesGcmDecryptTagDoesNotMatch` | `pok_local_backup` is tampered or was masked under a different `PartLocalMK` (unmask tag mismatch) | +| Evidence errors | The destination certificate chains fail validation or do not anchor to the policy `SATA` key, or the report signature is invalid | +| `InvalidPermissions` | Not a Crypto-Officer session | +| `SessionNotFound` | `session_id` does not refer to an `Active` slot | ## See also - Wire encoding: [TBOR specification](../../../fw/core/ddi/tbor/docs/spec.md) - Wire schema: `fw/core/ddi/tbor/types/src/sd_create_peer_backup.rs` +- Shared SD-backup mechanics: `fw/core/lib/src/ddi/tbor/sd_backup.rs` +- Consumer of the peer backup: [`SdRestorePeerBackup`](sd_restore_peer_backup.md) +- Producer of the local backup: [`SdCreateRemoteBackup`](sd_create_remote_backup.md) diff --git a/docs/tbor-ddi/commands/sd_restore_peer_backup.md b/docs/tbor-ddi/commands/sd_restore_peer_backup.md index df8dce25e..0da706eeb 100644 --- a/docs/tbor-ddi/commands/sd_restore_peer_backup.md +++ b/docs/tbor-ddi/commands/sd_restore_peer_backup.md @@ -5,16 +5,53 @@ Licensed under the MIT License. # SdRestorePeerBackup (Opcode 0x0F) -**Handler:** _Not yet landed — wire schema only._ -**Session:** InSession +**Handler:** `fw/core/lib/src/ddi/tbor/sd_restore_peer_backup.rs` +**Session:** InSession (Crypto Officer) ## Description -Restores a security domain from a peer backup: unmasks the -caller-supplied peer partition-owner-key backup (`pok_peer_backup`, a -masked BKS3) under the named sealing key, re-wraps it under the -device-local key, and returns the local backup (`pok_local_backup`) -together with the security-domain masking-key backup (`sd_mk_backup`). +Restores a security domain from a **peer** backup (manticore §3.3.11) — +the peer-cloning recovery path. It is +**[`SdRestoreRemoteBackup`](sd_restore_remote_backup.md) plus a +peer-cloning policy gate**: it HPKE-Auth-opens the caller-supplied +`pok_peer_backup` (an HPKE seal of BKS3) with the receiver's masked +SD-sealing key — authenticated by the sender peer's attested key — +recovers `SDMK` from `prev_sd_mk_backup`, and returns the device-local +backups so the security domain can afterwards be restored locally without +the peer. + +The provisioning half is shared with the local and remote restores +(`fw/core/lib/src/ddi/tbor/sd_backup.rs::reprovision_sd_from_bks3`). + +Algorithm: + +1. Gate to a Crypto-Officer, `Active` session on an `Initialized` + partition; fail-fast if the SD is already initialized + (`SdAlreadyInitialized`). +2. Bind the caller-supplied `policy` to the partition's fixed `policy_hash`, + then require its `allow_peer_cloning` flag (`SdPeerCloningNotAllowed`). +3. Verify the **sender** peer evidence against that policy: the cert chains + are validated and anchored to the policy `SATA` key, the report's v2 + `policy_hash` must equal `SHA-384(policy)`, and its attested COSE_Key is + recovered as **`SndrPub`**. +4. Unmask `masked_sealing_key` under its scope's masking key → the + receiver's private HPKE key **`RcvrPriv`** (must be an `SdSealing` + key), and derive `RcvrPub` on-device. +5. HPKE-Auth-open `pok_peer_backup` (`sk_r = RcvrPriv`, sender-auth + `SndrPub`) → **BKS3**. +6. Recover `SDMK` from `prev_sd_mk_backup` (SDMK masked under the SDBMK + derived from BKS3 + the partition `policy_hash`), re-mask both backups + at the current `{svn, owner}`, vault `SDMK` (SecurityDomain scope), + record `SD_MK_KEY_ID`, and mark the partition SD-initialized — + undo-guarded. `RcvrPriv`, BKS3, SDMK, and SDBMK are zeroized before + returning. + +The command is **stateful** (vaults `SDMK`, marks the partition +SD-initialized) and **one-shot** per partition incarnation: a second +create/restore returns `SdAlreadyInitialized`. Because `masked_sealing_key` +is bound to the device masking key, the realistic recovery sequence after +a reboot is `PartInit` → `PartFinal(prev_local_mk_backup)` (which restores +`PartLocalMK`) → `SdRestorePeerBackup`. ## Request @@ -26,14 +63,14 @@ variable-length data section. | Offset | Field | Type | Description | |---|---|---|---| | 4 | `session_id` | `session_id` (inline) | Session this request is bound to; cross-checked against the SQE-carried session id. | -| 8 | `sealing_key_id` | `key_id` (inline) | Vault id (`HsmKeyId`) of the sealing key the `pok_peer_backup` is bound to (`KeyId`, TOC entry type 1). | -| 12 | `mfgr_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source manufacturer certificate-chain descriptors (from the `src_evidence` field group). | -| 16 | `owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source owner certificate-chain descriptors. | -| 20 | `part_owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source partition-owner certificate-chain descriptors. | -| 24 | `evidence` | `buffer` (single `&ReportDescriptor`, 4 B) | Source attestation-report (COSE_Sign1) descriptor. | -| 28 | `policy` | `buffer` (fixed 484 B) | Caller-asserted unified `PartPolicy` describing the security domain being restored. Length pinned to `PART_POLICY_LEN` (484 B); a wrong length is rejected at decode. | -| 32 | `pok_peer_backup` | `buffer` (fixed 180 B) | Peer partition-owner-key backup to restore (a masked BKS3) = `MASKED_SD_LEN` (180 B). | -| 36 | `sd_mk_backup` | `buffer` (fixed 164 B) | Security-domain masking-key backup envelope = `LOCAL_MK_BACKUP_LEN` (164 B). | +| 8 | `masked_sealing_key` | `buffer` (fixed 180 B) | The **receiver's** masked SD-sealing key (from [`SdSealingKeyGen`](sd_sealing_key_gen.md)); unmasked on-device to recover `RcvrPriv`. Length pinned to `MASKED_SEALING_KEY_LEN` (180 B). Never a vault handle. | +| 12 | `policy` | `buffer` (fixed 484 B) | Caller-asserted unified `PartPolicy` describing the security domain being restored. Length pinned to `PART_POLICY_LEN` (484 B); its SHA-384 digest must equal the partition's bound `policy_hash` and each report's v2 `policy_hash`. | +| 16 | `mfgr_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source peer manufacturer certificate-chain descriptors (from the `src_evidence` field group). | +| 20 | `owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source peer owner certificate-chain descriptors. | +| 24 | `part_owner_cert_chain` | `buffer` (typed `&[CertDescriptor]`) | Source peer partition-owner certificate-chain descriptors. | +| 28 | `evidence` | `buffer` (single `&ReportDescriptor`, 4 B) | Source peer attestation-report (COSE_Sign1) descriptor. | +| 32 | `pok_peer_backup` | `buffer` (fixed 161 B) | Peer backup to restore: an HPKE-Auth seal of BKS3 = `POK_REMOTE_BACKUP_LEN` (161 B). | +| 36 | `prev_sd_mk_backup` | `buffer` (fixed 164 B) | Previous security-domain masking-key backup (SDMK masked under the derived SDBMK) = `SD_MK_BACKUP_LEN` (164 B); `SDMK` is recovered from it. | The four `mfgr_cert_chain` … `evidence` entries are spliced in by the shared [`Evidence`](../../../fw/core/ddi/tbor/types/src/evidence.rs) @@ -43,9 +80,9 @@ COSE_Sign1 report travel **out of band**, referenced by these ### Data section -Carries the packed source cert-chain and report descriptors, the -484-byte `policy` image, the 180-byte `pok_peer_backup` blob, and the -164-byte `sd_mk_backup` envelope. +Carries the 180-byte `masked_sealing_key`, the 484-byte `policy` image, +the packed source cert-chain and report descriptors, the 161-byte +`pok_peer_backup` seal, and the 164-byte `prev_sd_mk_backup` envelope. ## Response @@ -56,8 +93,8 @@ section. | Offset | Field | Type | Description | |---|---|---|---| -| 8 | `pok_local_backup` | `buffer` (fixed 180 B) | Partition-owner-key backup re-wrapped under the device-local key, sized as a masked BKS3 = `MASKED_SD_LEN` (180 B). | -| 12 | `sd_mk_backup` | `buffer` (fixed 164 B) | Security-domain masking-key backup envelope = `LOCAL_MK_BACKUP_LEN` (164 B). | +| 8 | `pok_local_backup` | `buffer` (fixed 180 B) | Local partition-owner-key backup (BKS3 re-masked under `PartLocalMK`), sized as a masked BKS3 = `MASKED_SD_LEN` (180 B). | +| 12 | `sd_mk_backup` | `buffer` (fixed 164 B) | Refreshed security-domain masking-key backup envelope (SDMK re-masked under SDBMK) = `SD_MK_BACKUP_LEN` (164 B). | ### Data section @@ -68,11 +105,22 @@ Carries the 180-byte `pok_local_backup` blob and the 164-byte | Error | Cause | |---|---| -| `TborInvalidFixedLength` | `policy` is not exactly 484 B, `pok_peer_backup` is not exactly 180 B, or `sd_mk_backup` is not exactly 164 B (rejected at decode before the handler runs) | -| `SessionNotFound` | `session_id` does not refer to an allocated slot | -| `DdiDecodeFailed` | Malformed request body | +| `TborInvalidFixedLength` | `masked_sealing_key` ≠ 180 B, `policy` ≠ 484 B, `pok_peer_backup` ≠ 161 B, or `prev_sd_mk_backup` ≠ 164 B (rejected at decode before the handler runs) | +| `InvalidArg` | Partition is not `Initialized` (not finalized); the policy `SATA` key is not P-384; the sender report's `policy_hash` ≠ `SHA-384(policy)`; or the opened backup is not a 48-byte BKS3 | +| `SdAlreadyInitialized` | A security domain is already initialized on this partition incarnation (one-shot gate) | +| `SdPeerCloningNotAllowed` | The partition's policy does not set `allow_peer_cloning` | +| `SdBackupSvnRollback` | A backup's bound SVN is newer than the current firmware SVN (anti-rollback) | +| `UnsupportedKeyType` | `masked_sealing_key` is not an `SdSealing` key, or `prev_sd_mk_backup` is not an `SdMasking` envelope | +| `AesGcmDecryptTagDoesNotMatch` | A backup blob is tampered or was masked/sealed under a different key (unmask / HPKE-open tag mismatch) | +| Evidence errors | The sender certificate chains fail validation or do not anchor to the policy `SATA` key, or the report signature is invalid | +| `InvalidPermissions` | Not a Crypto-Officer session | +| `SessionNotFound` | `session_id` does not refer to an `Active` slot | ## See also - Wire encoding: [TBOR specification](../../../fw/core/ddi/tbor/docs/spec.md) - Wire schema: `fw/core/ddi/tbor/types/src/sd_restore_peer_backup.rs` +- Shared SD-backup mechanics: `fw/core/lib/src/ddi/tbor/sd_backup.rs` +- Remote recovery path: [`SdRestoreRemoteBackup`](sd_restore_remote_backup.md) +- Local recovery path: [`SdRestoreLocalBackup`](sd_restore_local_backup.md) +- Producer of the peer backup: [`SdCreatePeerBackup`](sd_create_peer_backup.md) diff --git a/fw/core/ddi/tbor/types/src/sd_create_peer_backup.rs b/fw/core/ddi/tbor/types/src/sd_create_peer_backup.rs index 61e5027aa..beac6fb78 100644 --- a/fw/core/ddi/tbor/types/src/sd_create_peer_backup.rs +++ b/fw/core/ddi/tbor/types/src/sd_create_peer_backup.rs @@ -4,51 +4,73 @@ //! TBOR `SdCreatePeerBackup` wire schema. //! //! `SdCreatePeerBackup` is an in-session command that creates a -//! peer-transferable backup of a security domain: it takes the local -//! partition-owner-key backup (`pok_local_backup`), re-masks it for the -//! destination peer named by `dst_evidence` under the named sealing key, -//! and returns the peer backup (`pok_peer_backup`). +//! **peer-transferable** backup of a security domain (manticore §3.3.10): +//! it recovers BKS3 from the caller's device-local backup +//! (`pok_local_backup`) and HPKE-Auth-seals it to a destination peer — +//! named by `dst_evidence` and authenticated by the sender's own masked +//! SD-sealing key — returning the peer backup (`pok_peer_backup`). The +//! command is **stateless**: nothing is persisted. +//! +//! Peer cloning is gated by the security domain's policy +//! (`allow_peer_cloning`). //! //! Inputs: //! //! * `session_id` — TOC-carried session id; cross-checked against the //! SQE-carried session id by the dispatcher (parity with the other //! in-session commands). -//! * `sealing_key_id` — vault id -//! ([`KeyId`](azihsm_fw_ddi_tbor_api::KeyId), TOC entry type 1) of the -//! sealing key the `pok_local_backup` is bound to. -//! * `dst_evidence` — destination peer side-band attestation evidence -//! ([`Evidence`](crate::evidence::Evidence) field group). +//! * `masked_sealing_key` — the **sender's** masked SD-sealing key (from +//! [`SdSealingKeyGen`](crate::sd_sealing_key_gen)), exactly +//! [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to recover the +//! sender's private HPKE key (`SndrPriv`) that authenticates the seal; +//! never a vault handle. //! * `policy` — the unified [`PartPolicy`] describing the security domain -//! being backed up. Length pinned to [`PART_POLICY_LEN`] (484 B). -//! * `pok_local_backup` — the local partition-owner-key backup to -//! re-mask (a masked BKS3 wrapped under the device-local key), exactly -//! [`MASKED_SD_LEN`] (180 B). +//! being backed up. Length pinned to [`PART_POLICY_LEN`] (484 B); its +//! SHA-384 digest must equal the partition's bound `policy_hash` and the +//! receiver report's v2 `policy_hash`. +//! * `dst_evidence` — **destination** peer side-band attestation evidence +//! ([`Evidence`](crate::evidence::Evidence) field group); its attested +//! key is the receiver public key (`RcvrPub`) the backup is sealed to. +//! * `pok_local_backup` — the device-local partition-owner-key backup (a +//! masked BKS3 wrapped under `PartLocalMK`), exactly [`MASKED_SD_LEN`] +//! (180 B), from which BKS3 is recovered. //! //! Output: //! -//! * `pok_peer_backup` — the partition-owner-key backup re-masked for the -//! destination peer, exactly [`MASKED_SD_LEN`] (180 B). +//! * `pok_peer_backup` — the peer backup: an HPKE-Auth seal of BKS3, +//! exactly [`POK_REMOTE_BACKUP_LEN`] (161 B). use azihsm_fw_ddi_tbor_api::tbor; use crate::evidence::*; pub use crate::policy::PART_POLICY_LEN; pub use crate::sd_create_remote_backup::MASKED_SD_LEN; +pub use crate::sd_create_remote_backup::POK_REMOTE_BACKUP_LEN; +pub use crate::sd_sealing_key_gen::MASKED_SEALING_KEY_LEN; /// TBOR opcode for `SdCreatePeerBackup`. pub const TBOR_OP_SD_CREATE_PEER_BACKUP: u8 = 0x0E; +// `masked_sealing_key` is a masked SD-sealing key; the derive needs an +// integer literal on the field, so the length is spelled out as `180` and +// pinned against the canonical `MASKED_SEALING_KEY_LEN` here. +const _: () = assert!(MASKED_SEALING_KEY_LEN == 180); + // `policy` carries the unified `PartPolicy`; the derive needs an integer // literal on the field, so the length is spelled out as `484` and pinned // against the canonical value here. const _: () = assert!(PART_POLICY_LEN == 484); -// `pok_local_backup` / `pok_peer_backup` are masked BKS3 envelopes; the -// derive needs an integer literal on the field, so the length is spelled -// out as `180` and pinned against the canonical value here. +// `pok_local_backup` is a masked BKS3 envelope; the derive needs an integer +// literal on the field, so the length is spelled out as `180` and pinned +// against the canonical value here. const _: () = assert!(MASKED_SD_LEN == 180); +// `pok_peer_backup` is an HPKE-Auth seal; the derive needs an integer +// literal on the field, so the length is spelled out as `161` and pinned +// against `POK_REMOTE_BACKUP_LEN` here. +const _: () = assert!(POK_REMOTE_BACKUP_LEN == 161); + /// `SdCreatePeerBackup` request schema. #[tbor(opcode = 0x0E)] pub struct TborSdCreatePeerBackupReq<'a> { @@ -58,18 +80,12 @@ pub struct TborSdCreatePeerBackupReq<'a> { #[tbor(session_id)] pub session_id: SessionId, - /// Vault id ([`HsmKeyId`](azihsm_fw_hsm_pal_traits::HsmKeyId)) of the - /// sealing key the `pok_local_backup` is bound to. Carried as a - /// [`KeyId`](azihsm_fw_ddi_tbor_api::KeyId) (TOC entry type 1). - #[tbor(key_id)] - pub sealing_key_id: KeyId, - - /// Destination peer side-band attestation evidence (manufacturer / - /// owner / partition-owner certificate chains plus the attestation - /// report). Spliced in as the [`Evidence`](crate::evidence::Evidence) - /// field group's four TOC entries. - #[tbor(include)] - pub dst_evidence: Evidence<'a>, + /// The sender's masked SD-sealing key (from `SdSealingKeyGen`), exactly + /// [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to recover + /// the sender's private HPKE key (`SndrPriv`) that authenticates the + /// seal. + #[tbor(buffer, len = 180)] + pub masked_sealing_key: &'a [u8], /// Caller-asserted unified [`PartPolicy`] describing the security /// domain being backed up. Length pinned to [`PART_POLICY_LEN`] @@ -79,9 +95,17 @@ pub struct TborSdCreatePeerBackupReq<'a> { #[tbor(buffer, len = 484)] pub policy: &'a [u8], - /// Local partition-owner-key backup to re-mask (a masked BKS3 wrapped - /// under the device-local key). Always exactly [`MASKED_SD_LEN`] - /// (180 B). + /// Destination peer side-band attestation evidence (manufacturer / + /// owner / partition-owner certificate chains plus the attestation + /// report). Spliced in as the [`Evidence`](crate::evidence::Evidence) + /// field group's four TOC entries; its attested key is the receiver + /// public key the backup is sealed to. + #[tbor(include)] + pub dst_evidence: Evidence<'a>, + + /// Device-local partition-owner-key backup (a masked BKS3 wrapped under + /// `PartLocalMK`) from which BKS3 is recovered. Always exactly + /// [`MASKED_SD_LEN`] (180 B). #[tbor(buffer, len = 180)] pub pok_local_backup: &'a [u8], } @@ -89,9 +113,9 @@ pub struct TborSdCreatePeerBackupReq<'a> { /// `SdCreatePeerBackup` response schema. #[tbor(response)] pub struct TborSdCreatePeerBackupResp<'a> { - /// Partition-owner-key backup re-masked for the destination peer. - /// Always exactly [`MASKED_SD_LEN`] (180 B). - #[tbor(buffer, len = 180)] + /// Peer backup: an HPKE-Auth seal of BKS3. Always exactly + /// [`POK_REMOTE_BACKUP_LEN`] (161 B). + #[tbor(buffer, len = 161)] pub pok_peer_backup: &'a [u8], } @@ -99,13 +123,13 @@ pub struct TborSdCreatePeerBackupResp<'a> { mod tests { #![allow(clippy::unwrap_used)] - use azihsm_fw_ddi_tbor_api::KeyId; use azihsm_fw_ddi_tbor_api::SessionId; use super::*; #[test] fn request_round_trips_fields() { + let masked = [0u8; MASKED_SEALING_KEY_LEN]; let policy = [0u8; PART_POLICY_LEN]; let pok_local = [0xABu8; MASKED_SD_LEN]; let cert = CertDescriptor { @@ -122,7 +146,9 @@ mod tests { .unwrap() .session_id(SessionId(7)) .unwrap() - .sealing_key_id(KeyId(0x5678)) + .masked_sealing_key(&masked) + .unwrap() + .policy(&policy) .unwrap() .dst_evidence(|e| { e.mfgr_cert_chain(&chain)? @@ -131,24 +157,23 @@ mod tests { .evidence(&report) }) .unwrap() - .policy(&policy) - .unwrap() .pok_local_backup(&pok_local) .unwrap() .finish(); + assert_eq!(frame.masked_sealing_key().len(), MASKED_SEALING_KEY_LEN); assert_eq!(frame.policy().len(), PART_POLICY_LEN); assert_eq!(frame.pok_local_backup().len(), MASKED_SD_LEN); } #[test] fn response_round_trips_pok_peer_backup() { - let pok_peer = [0xABu8; MASKED_SD_LEN]; + let pok_peer = [0xABu8; POK_REMOTE_BACKUP_LEN]; let mut buf = [0u8; 512]; let frame = TborSdCreatePeerBackupResp::encode(&mut buf, 0, true) .unwrap() .pok_peer_backup(&pok_peer) .unwrap() .finish(); - assert_eq!(frame.pok_peer_backup().len(), MASKED_SD_LEN); + assert_eq!(frame.pok_peer_backup().len(), POK_REMOTE_BACKUP_LEN); } } diff --git a/fw/core/ddi/tbor/types/src/sd_restore_peer_backup.rs b/fw/core/ddi/tbor/types/src/sd_restore_peer_backup.rs index 89ddc3135..f4b01368f 100644 --- a/fw/core/ddi/tbor/types/src/sd_restore_peer_backup.rs +++ b/fw/core/ddi/tbor/types/src/sd_restore_peer_backup.rs @@ -3,61 +3,85 @@ //! TBOR `SdRestorePeerBackup` wire schema. //! -//! `SdRestorePeerBackup` is an in-session command that restores a -//! security domain from a peer backup: it unmasks the caller-supplied -//! peer partition-owner-key backup (`pok_peer_backup`, a masked BKS3) -//! under the named sealing key, re-wraps it under the device-local key, -//! and returns the local backup (`pok_local_backup`) together with the -//! security-domain masking-key backup (`sd_mk_backup`). +//! `SdRestorePeerBackup` is an in-session command that restores a security +//! domain from a **peer** backup (manticore §3.3.11): it HPKE-Auth-opens +//! the caller-supplied `pok_peer_backup` (an HPKE seal of BKS3) with the +//! receiver's masked SD-sealing key — authenticated by the sender peer's +//! attested key — recovers `SDMK` from `prev_sd_mk_backup`, and returns the +//! device-local backups (`pok_local_backup`, `sd_mk_backup`) so the +//! security domain can later be restored locally without the peer. +//! +//! It is [`SdRestoreRemoteBackup`](crate::sd_restore_remote_backup) plus a +//! peer-cloning policy gate (`allow_peer_cloning`); the backup recovered +//! here originates from a peer partition rather than a remote sealing +//! authority. //! //! Inputs: //! //! * `session_id` — TOC-carried session id; cross-checked against the //! SQE-carried session id by the dispatcher (parity with the other //! in-session commands). -//! * `sealing_key_id` — vault id -//! ([`KeyId`](azihsm_fw_ddi_tbor_api::KeyId), TOC entry type 1) of the -//! sealing key the `pok_peer_backup` is bound to. -//! * `src_evidence` — source peer side-band attestation evidence -//! ([`Evidence`](crate::evidence::Evidence) field group). +//! * `masked_sealing_key` — the **receiver's** masked SD-sealing key (from +//! [`SdSealingKeyGen`](crate::sd_sealing_key_gen)), exactly +//! [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to recover the +//! receiver's private HPKE key (`RcvrPriv`) that opens the backup; never +//! a vault handle. //! * `policy` — the unified [`PartPolicy`] describing the security domain -//! being restored. Length pinned to [`PART_POLICY_LEN`] (484 B). -//! * `pok_peer_backup` — the peer partition-owner-key backup to restore -//! (a masked BKS3), exactly [`MASKED_SD_LEN`] (180 B). -//! * `sd_mk_backup` — the security-domain masking-key backup envelope, -//! exactly [`LOCAL_MK_BACKUP_LEN`] (164 B). +//! being restored. Length pinned to [`PART_POLICY_LEN`] (484 B); its +//! SHA-384 digest must equal the partition's bound `policy_hash` and each +//! report's v2 `policy_hash`. +//! * `src_evidence` — **source** peer side-band attestation evidence +//! ([`Evidence`](crate::evidence::Evidence) field group); its attested +//! key is the sender public key that sealed `pok_peer_backup`. +//! * `pok_peer_backup` — the peer backup to restore: an HPKE-Auth seal of +//! BKS3, exactly [`POK_REMOTE_BACKUP_LEN`] (161 B). +//! * `prev_sd_mk_backup` — the previous security-domain masking-key backup +//! (SDMK masked under the derived SDBMK), exactly [`SD_MK_BACKUP_LEN`] +//! (164 B), from which `SDMK` is recovered. //! //! Output: //! -//! * `pok_local_backup` — the partition-owner-key backup re-wrapped under -//! the device-local key, exactly [`MASKED_SD_LEN`] (180 B). -//! * `sd_mk_backup` — the security-domain masking-key backup envelope, -//! exactly [`LOCAL_MK_BACKUP_LEN`] (164 B). +//! * `pok_local_backup` — the local partition-owner-key backup (BKS3 masked +//! under `PartLocalMK`), exactly [`MASKED_SD_LEN`] (180 B). +//! * `sd_mk_backup` — the refreshed security-domain masking-key backup +//! envelope, exactly [`SD_MK_BACKUP_LEN`] (164 B). use azihsm_fw_ddi_tbor_api::tbor; use crate::evidence::*; -pub use crate::part_final::LOCAL_MK_BACKUP_LEN; pub use crate::policy::PART_POLICY_LEN; pub use crate::sd_create_remote_backup::MASKED_SD_LEN; +pub use crate::sd_create_remote_backup::POK_REMOTE_BACKUP_LEN; +pub use crate::sd_create_remote_backup::SD_MK_BACKUP_LEN; +pub use crate::sd_sealing_key_gen::MASKED_SEALING_KEY_LEN; /// TBOR opcode for `SdRestorePeerBackup`. pub const TBOR_OP_SD_RESTORE_PEER_BACKUP: u8 = 0x0F; +// `masked_sealing_key` is a masked SD-sealing key; the derive needs an +// integer literal on the field, so the length is spelled out as `180` and +// pinned against the canonical `MASKED_SEALING_KEY_LEN` here. +const _: () = assert!(MASKED_SEALING_KEY_LEN == 180); + // `policy` carries the unified `PartPolicy`; the derive needs an integer // literal on the field, so the length is spelled out as `484` and pinned // against the canonical value here. const _: () = assert!(PART_POLICY_LEN == 484); -// `pok_peer_backup` / `pok_local_backup` are masked BKS3 envelopes; the -// derive needs an integer literal on the field, so the length is spelled -// out as `180` and pinned against the canonical value here. +// `pok_peer_backup` is an HPKE-Auth seal; the derive needs an integer +// literal on the field, so the length is spelled out as `161` and pinned +// against `POK_REMOTE_BACKUP_LEN` here. +const _: () = assert!(POK_REMOTE_BACKUP_LEN == 161); + +// `pok_local_backup` is a masked BKS3 envelope; the derive needs an integer +// literal on the field, so the length is spelled out as `180` and pinned +// against the canonical value here. const _: () = assert!(MASKED_SD_LEN == 180); -// `sd_mk_backup` is a `local_mk`-style backup envelope; the derive needs -// an integer literal on the field, so the length is spelled out as `164` -// and pinned against the canonical value here. -const _: () = assert!(LOCAL_MK_BACKUP_LEN == 164); +// `prev_sd_mk_backup` / `sd_mk_backup` are SD masking-key backup envelopes; +// the derive needs an integer literal on the field, so the length is +// spelled out as `164` and pinned against the canonical value here. +const _: () = assert!(SD_MK_BACKUP_LEN == 164); /// `SdRestorePeerBackup` request schema. #[tbor(opcode = 0x0F)] @@ -68,18 +92,11 @@ pub struct TborSdRestorePeerBackupReq<'a> { #[tbor(session_id)] pub session_id: SessionId, - /// Vault id ([`HsmKeyId`](azihsm_fw_hsm_pal_traits::HsmKeyId)) of the - /// sealing key the `pok_peer_backup` is bound to. Carried as a - /// [`KeyId`](azihsm_fw_ddi_tbor_api::KeyId) (TOC entry type 1). - #[tbor(key_id)] - pub sealing_key_id: KeyId, - - /// Source peer side-band attestation evidence (manufacturer / owner / - /// partition-owner certificate chains plus the attestation report). - /// Spliced in as the [`Evidence`](crate::evidence::Evidence) field - /// group's four TOC entries. - #[tbor(include)] - pub src_evidence: Evidence<'a>, + /// The receiver's masked SD-sealing key (from `SdSealingKeyGen`), + /// exactly [`MASKED_SEALING_KEY_LEN`] (180 B). Unmasked on-device to + /// recover the receiver's private HPKE key (`RcvrPriv`). + #[tbor(buffer, len = 180)] + pub masked_sealing_key: &'a [u8], /// Caller-asserted unified [`PartPolicy`] describing the security /// domain being restored. Length pinned to [`PART_POLICY_LEN`] @@ -89,15 +106,23 @@ pub struct TborSdRestorePeerBackupReq<'a> { #[tbor(buffer, len = 484)] pub policy: &'a [u8], - /// Peer partition-owner-key backup to restore (a masked BKS3). - /// Always exactly [`MASKED_SD_LEN`] (180 B). - #[tbor(buffer, len = 180)] + /// Source peer side-band attestation evidence (manufacturer / owner / + /// partition-owner certificate chains plus the attestation report). + /// Spliced in as the [`Evidence`](crate::evidence::Evidence) field + /// group's four TOC entries; its attested key is the sender public key + /// that sealed `pok_peer_backup`. + #[tbor(include)] + pub src_evidence: Evidence<'a>, + + /// Peer backup to restore: an HPKE-Auth seal of BKS3. Always exactly + /// [`POK_REMOTE_BACKUP_LEN`] (161 B). + #[tbor(buffer, len = 161)] pub pok_peer_backup: &'a [u8], - /// Security-domain masking-key backup envelope. Always exactly - /// [`LOCAL_MK_BACKUP_LEN`] (164 B). + /// Previous security-domain masking-key backup (SDMK masked under the + /// derived SDBMK). Always exactly [`SD_MK_BACKUP_LEN`] (164 B). #[tbor(buffer, len = 164)] - pub sd_mk_backup: &'a [u8], + pub prev_sd_mk_backup: &'a [u8], } /// `SdRestorePeerBackup` response schema. @@ -109,7 +134,7 @@ pub struct TborSdRestorePeerBackupResp<'a> { pub pok_local_backup: &'a [u8], /// Security-domain masking-key backup envelope. Always exactly - /// [`LOCAL_MK_BACKUP_LEN`] (164 B). + /// [`SD_MK_BACKUP_LEN`] (164 B). #[tbor(buffer, len = 164)] pub sd_mk_backup: &'a [u8], } @@ -118,16 +143,16 @@ pub struct TborSdRestorePeerBackupResp<'a> { mod tests { #![allow(clippy::unwrap_used)] - use azihsm_fw_ddi_tbor_api::KeyId; use azihsm_fw_ddi_tbor_api::SessionId; use super::*; #[test] fn request_round_trips_fields() { + let masked = [0u8; MASKED_SEALING_KEY_LEN]; let policy = [0u8; PART_POLICY_LEN]; - let pok_peer = [0xABu8; MASKED_SD_LEN]; - let sd_mk = [0xCDu8; LOCAL_MK_BACKUP_LEN]; + let pok_peer = [0xABu8; POK_REMOTE_BACKUP_LEN]; + let prev_sd_mk = [0xCDu8; SD_MK_BACKUP_LEN]; let cert = CertDescriptor { index: 0, length: crate::tbor_int::U16::new(8), @@ -137,12 +162,14 @@ mod tests { length: crate::tbor_int::U16::new(16), }; let chain = [cert]; - let mut buf = [0u8; 1024]; + let mut buf = [0u8; 2048]; let frame = TborSdRestorePeerBackupReq::encode(&mut buf) .unwrap() .session_id(SessionId(7)) .unwrap() - .sealing_key_id(KeyId(0x5678)) + .masked_sealing_key(&masked) + .unwrap() + .policy(&policy) .unwrap() .src_evidence(|e| { e.mfgr_cert_chain(&chain)? @@ -151,22 +178,21 @@ mod tests { .evidence(&report) }) .unwrap() - .policy(&policy) - .unwrap() .pok_peer_backup(&pok_peer) .unwrap() - .sd_mk_backup(&sd_mk) + .prev_sd_mk_backup(&prev_sd_mk) .unwrap() .finish(); + assert_eq!(frame.masked_sealing_key().len(), MASKED_SEALING_KEY_LEN); assert_eq!(frame.policy().len(), PART_POLICY_LEN); - assert_eq!(frame.pok_peer_backup().len(), MASKED_SD_LEN); - assert_eq!(frame.sd_mk_backup().len(), LOCAL_MK_BACKUP_LEN); + assert_eq!(frame.pok_peer_backup().len(), POK_REMOTE_BACKUP_LEN); + assert_eq!(frame.prev_sd_mk_backup().len(), SD_MK_BACKUP_LEN); } #[test] fn response_round_trips_backups() { let pok_local = [0xABu8; MASKED_SD_LEN]; - let sd_mk = [0xCDu8; LOCAL_MK_BACKUP_LEN]; + let sd_mk = [0xCDu8; SD_MK_BACKUP_LEN]; let mut buf = [0u8; 512]; let frame = TborSdRestorePeerBackupResp::encode(&mut buf, 0, true) .unwrap() @@ -176,6 +202,6 @@ mod tests { .unwrap() .finish(); assert_eq!(frame.pok_local_backup().len(), MASKED_SD_LEN); - assert_eq!(frame.sd_mk_backup().len(), LOCAL_MK_BACKUP_LEN); + assert_eq!(frame.sd_mk_backup().len(), SD_MK_BACKUP_LEN); } } diff --git a/fw/core/lib/src/ddi/tbor/mod.rs b/fw/core/lib/src/ddi/tbor/mod.rs index d0c49985e..b6626cb38 100644 --- a/fw/core/lib/src/ddi/tbor/mod.rs +++ b/fw/core/lib/src/ddi/tbor/mod.rs @@ -29,9 +29,11 @@ pub mod part_init; pub mod policy; pub(crate) mod psk_change; pub(crate) mod sd_backup; +pub(crate) mod sd_create_peer_backup; pub(crate) mod sd_create_remote_backup; pub(crate) mod sd_reseal_remote_backup; pub(crate) mod sd_restore_local_backup; +pub(crate) mod sd_restore_peer_backup; pub(crate) mod sd_restore_remote_backup; pub(crate) mod sd_sealing_key_gen; pub(crate) mod session_close; @@ -143,6 +145,21 @@ pub(crate) mod opcode { /// [`super::sd_restore_local_backup`]. pub(crate) const SD_RESTORE_LOCAL_BACKUP: u8 = 0x0D; + /// `SdCreatePeerBackup` — create a peer-transferable backup of a + /// security domain: recover BKS3 from the caller's `pok_local_backup` + /// (under `PartLocalMK`) and HPKE-Auth-seal it to a destination peer + /// named by `dst_evidence`, authenticated by the sender's masked SD + /// sealing key. Gated by the SD policy's `allow_peer_cloning`. See + /// [`super::sd_create_peer_backup`]. + pub(crate) const SD_CREATE_PEER_BACKUP: u8 = 0x0E; + + /// `SdRestorePeerBackup` — restore a security domain from a peer backup: + /// HPKE-Auth-open `pok_peer_backup` with the masked receiver key + /// (authenticated by the sender peer's attested key), recover SDMK from + /// `prev_sd_mk_backup`, and re-provision the SD. Gated by the SD + /// policy's `allow_peer_cloning`. See [`super::sd_restore_peer_backup`]. + pub(crate) const SD_RESTORE_PEER_BACKUP: u8 = 0x0F; + /// `KeyReport` — attest a masked key: unmask it, derive its public /// component on-device, and return a PID-signed COSE_Sign1 /// key-attestation report over it. See [`super::key_report`]. @@ -287,6 +304,10 @@ pub(crate) async fn dispatch<'p, P: HsmPal>( opcode::SD_RESTORE_LOCAL_BACKUP => { sd_restore_local_backup::handle(pal, io, req_buf, undo).await } + opcode::SD_CREATE_PEER_BACKUP => sd_create_peer_backup::handle(pal, io, req_buf, oob).await, + opcode::SD_RESTORE_PEER_BACKUP => { + sd_restore_peer_backup::handle(pal, io, req_buf, oob, undo).await + } opcode::KEY_REPORT => key_report::handle(pal, io, req_buf).await, _ => Err(HsmError::UnsupportedCmd), } @@ -312,6 +333,8 @@ fn is_known_opcode(opcode: u8) -> bool { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::SD_RESTORE_REMOTE_BACKUP | opcode::SD_RESTORE_LOCAL_BACKUP + | opcode::SD_CREATE_PEER_BACKUP + | opcode::SD_RESTORE_PEER_BACKUP | opcode::KEY_REPORT ) } @@ -344,6 +367,8 @@ fn is_in_session(opcode: u8) -> bool { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::SD_RESTORE_REMOTE_BACKUP | opcode::SD_RESTORE_LOCAL_BACKUP + | opcode::SD_CREATE_PEER_BACKUP + | opcode::SD_RESTORE_PEER_BACKUP | opcode::KEY_REPORT => true, // Default-deny: any future opcode is treated as in-session // until classified, so the default-PSK gate applies to it. @@ -384,6 +409,8 @@ fn needs_session_id_cross_check(opcode: u8) -> bool { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::SD_RESTORE_REMOTE_BACKUP | opcode::SD_RESTORE_LOCAL_BACKUP + | opcode::SD_CREATE_PEER_BACKUP + | opcode::SD_RESTORE_PEER_BACKUP | opcode::KEY_REPORT => true, _ => true, } diff --git a/fw/core/lib/src/ddi/tbor/sd_backup.rs b/fw/core/lib/src/ddi/tbor/sd_backup.rs index aecb59030..c65ea1da6 100644 --- a/fw/core/lib/src/ddi/tbor/sd_backup.rs +++ b/fw/core/lib/src/ddi/tbor/sd_backup.rs @@ -90,6 +90,44 @@ pub(super) fn platform_svn_owner(pal: &P) -> HsmResult<(u64, u16)> { Ok((svn, owner)) } +/// Recover **BKS3** from a device-local partition-owner-key backup. +/// +/// Unmasks `pok_scratch` (a caller-staged `pok_local_backup` = masked BKS3) +/// in place under the partition-local masking key (`PartLocalMK`, from +/// `PartFinal`) and returns the recovered BKS3 as a view into `pok_scratch`. +/// The blob must be an [`SdPartitionOwnerSeed`](HsmVaultKeyKind::SdPartitionOwnerSeed) +/// envelope, and its bound SVN must not be newer than `svn` (anti-rollback, +/// enforced only after the AEAD tag authenticates the envelope so a tampered +/// cleartext SVN fails the tag rather than spoofing the check). +/// +/// Shared by the local restore (recovers BKS3 to re-provision the SD) and +/// the peer-backup create (recovers BKS3 to re-seal it to a peer). The +/// caller wipes `pok_scratch`. +pub(super) async fn recover_bks3_from_pok_local<'a, P: HsmPal>( + pal: &P, + io: &impl HsmIo, + svn: u64, + pok_scratch: &'a mut DmaBuf, +) -> HsmResult<&'a DmaBuf> { + let local_mk_id = part_state::part_local_mk_key_id(pal, io)?; + let local_mk = pal.vault_key(io, local_mk_id)?; + let view = unmask(pal, io, local_mk, pok_scratch).await?; + if !matches!(view.key_kind, HsmVaultKeyKind::SdPartitionOwnerSeed) { + return Err(HsmError::UnsupportedKeyType); + } + if view.svn > svn { + return Err(HsmError::SdBackupSvnRollback); + } + // Firmware invariant: the AEAD tag has authenticated the envelope, so a + // genuine backup always carries a `BKS3_LEN` seed; a mismatch signals + // corruption / a sizing bug, not a client error. Mirrors + // `restore_part_local_mk` in `part_final`. + if view.target_key.len() != BKS3_LEN { + return Err(HsmError::InternalError); + } + Ok(view.target_key) +} + /// Derive `SDBMK` for `bks3` at `{svn, owner}` into a fresh scoped buffer. /// /// `SDBMK = KBKDF(BKS3, mfgr_seed[svn] ‖ owner_seed[owner] ‖ policy_hash)`. diff --git a/fw/core/lib/src/ddi/tbor/sd_create_peer_backup.rs b/fw/core/lib/src/ddi/tbor/sd_create_peer_backup.rs new file mode 100644 index 000000000..e189f3530 --- /dev/null +++ b/fw/core/lib/src/ddi/tbor/sd_create_peer_backup.rs @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `SdCreatePeerBackup` handler. +//! +//! Creates a **peer-transferable** backup of a security domain (manticore +//! §3.3.10): it recovers BKS3 from the caller's device-local backup +//! (`pok_local_backup`) and HPKE-Auth-seals it to a destination peer — +//! named by `dst_evidence` and authenticated by the sender's own masked +//! SD-sealing key — returning the peer backup (`pok_peer_backup`). +//! +//! It is **[`SdCreateRemoteBackup`](super::sd_create_remote_backup)'s +//! HPKE-Auth-seal front-end over a recovered (not freshly minted) BKS3**, +//! sharing [`recover_bks3_from_pok_local`](super::sd_backup::recover_bks3_from_pok_local) +//! with the local restore. Peer cloning is gated by the security domain's +//! `allow_peer_cloning` policy flag. +//! +//! Flow: +//! +//! 1. Decode; gate to a Crypto-Officer, `Active` session on an +//! `Initialized` partition (`PartLocalMK` and the policy hash are bound +//! by `PartFinal`). Unlike the restores this is **not** one-shot and +//! does not touch `SDMK`, so it neither requires nor sets the +//! SD-initialized flag — a rebooted partition can clone to a peer after +//! `PartFinal` without first restoring the SD locally. +//! 2. Bind the caller-supplied [`PartPolicy`] to the partition's fixed +//! `policy_hash`, require its `allow_peer_cloning` flag +//! ([`SdPeerCloningNotAllowed`](HsmError::SdPeerCloningNotAllowed)), then +//! verify the **destination** peer evidence against it: its cert chains +//! are validated and anchored to the policy SATA key, its report's v2 +//! `policy_hash` must equal `SHA-384(policy)`, and its attested COSE_Key +//! is recovered as `RcvrPub`. +//! 3. Unmask `masked_sealing_key` → `SndrPriv` (must be an +//! [`SdSealing`](HsmVaultKeyKind::SdSealing) key) and derive `SndrPub`. +//! 4. Recover BKS3 from `pok_local_backup` under `PartLocalMK` (shared +//! [`recover_bks3_from_pok_local`](super::sd_backup::recover_bks3_from_pok_local)). +//! 5. HPKE-Auth-seal BKS3 to `RcvrPub` with `SndrPriv` as the +//! sender-authentication key, returning `pok_peer_backup` (161 B). +//! `SndrPriv` and BKS3 are zeroized before returning. +//! +//! **Stateless:** nothing is persisted, no vault writes, no undo log. This +//! command is **Crypto-Officer-only**. +//! +//! [`PartPolicy`]: super::policy + +use azihsm_fw_core_crypto_hpke::seal; +use azihsm_fw_core_crypto_hpke::AuthParams; +use azihsm_fw_core_crypto_hpke::HpkeSealConfig; +use azihsm_fw_core_crypto_hpke::HpkeSuite; +use azihsm_fw_core_crypto_key_masking::aead::peek_metadata; +use azihsm_fw_core_crypto_key_masking::aead::unmask; +use azihsm_fw_core_crypto_key_report::POLICY_HASH_LEN; +use azihsm_fw_core_evidence::verify_evidence; +use azihsm_fw_core_evidence::EvidenceRefs; +use azihsm_fw_core_evidence::TrustAnchors; +use azihsm_fw_ddi_tbor_types::policy::PolicyKeyKind; +use azihsm_fw_ddi_tbor_types::policy::POLICY_MAX_KEY_LEN; +use azihsm_fw_ddi_tbor_types::TborSdCreatePeerBackupReq; +use azihsm_fw_ddi_tbor_types::TborSdCreatePeerBackupResp; +use azihsm_fw_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_fw_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_fw_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_fw_hsm_oob::OobPtr; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmEccCurve; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmHashAlgo; +use azihsm_fw_hsm_pal_traits::HsmIo; +use azihsm_fw_hsm_pal_traits::HsmKeyId; +use azihsm_fw_hsm_pal_traits::HsmPal; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; +use azihsm_fw_hsm_pal_traits::HsmSessId; +use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; +use azihsm_fw_hsm_pal_traits::PartState; + +use super::masking_key_id_for_scope; +use super::part_final::verify_policy_hash; +use super::sd_backup; +use super::validate_crypto_officer_active_session; +use crate::part_state; + +/// NIST curve for the SD sealing keys and the peer-backup HPKE seal. +const SD_CURVE: HsmEccCurve = HsmEccCurve::P384; + +/// HPKE ciphersuite for the peer-backup seal. +const HPKE_SUITE: HpkeSuite = HpkeSuite::DHKemP384Sha384AesGcm256; + +/// Length of the BKS3 sealed into the peer backup. +const BKS3_LEN: usize = 48; + +/// SEC1 uncompressed point tag (`0x04 ‖ X ‖ Y`). +const SEC1_UNCOMPRESSED: u8 = 0x04; + +/// Handle a TBOR `SdCreatePeerBackup` request. +/// +/// **Stateless**: recovers BKS3 from the caller's local backup and re-seals +/// it to the destination peer; no vault writes, no partition-state +/// mutation, no undo log. +pub(crate) async fn handle<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + req_buf: &mut DmaBuf, + oob: Option, +) -> HsmResult<&'p DmaBuf> { + let mk_key_id = gate_request(pal, io, req_buf)?; + + // The destination attestation evidence is mandatory side-band data + // carried in the out-of-band SGL page. + let oob = oob.ok_or(HsmError::InvalidArg)?; + + // Allocate the fixed-size peer-backup response in the IO scope so it + // survives the crypto scratch allocator's reset. + let pok_peer_out = pal.dma_alloc(io, POK_REMOTE_BACKUP_LEN)?; + + pal.alloc_scoped_async(io, async |alloc| -> HsmResult<()> { + let coord = SD_CURVE.priv_key_len(); + let (svn, _owner) = sd_backup::platform_svn_owner(pal)?; + + // `pk_r` (the attested `RcvrPub`) is recovered by the evidence check + // in phase 1 and consumed by the seal in phase 2. + let pk_r = alloc.dma_alloc(1 + 2 * coord)?; + + // ── Phase 1: policy binding + peer-cloning gate + receiver evidence ── + { + let req = TborSdCreatePeerBackupReq::decode(&*req_buf)?; + let policy = req.policy(); + + // The re-supplied policy must match the one bound at `PartInit`. + verify_policy_hash(pal, io, alloc, policy).await?; + let part_policy = super::policy::from_bytes(policy)?; + + // Peer cloning is gated by the (now authenticated) SD policy. + if !part_policy.flags.allow_peer_cloning() { + return Err(HsmError::SdPeerCloningNotAllowed); + } + + let sata = &part_policy.sata_pub_key; + if sata.kind() != PolicyKeyKind::Ecc384 || sata.len() != POLICY_MAX_KEY_LEN { + return Err(HsmError::InvalidArg); + } + + // The destination peer's report must attest to the same policy. + let expected = alloc.dma_alloc(POLICY_HASH_LEN)?; + pal.hash(io, HsmHashAlgo::Sha384, policy, expected, true) + .await?; + + let dst_hash = alloc.dma_alloc(POLICY_HASH_LEN)?; + { + let ev = req.dst_evidence(); + verify_evidence( + pal, + io, + &oob, + &EvidenceRefs { + mfgr_chain: ev.mfgr_cert_chain(), + owner_chain: ev.owner_cert_chain(), + part_owner_chain: ev.part_owner_cert_chain(), + report: ev.evidence(), + }, + &TrustAnchors { + sata: &sata.data[..POLICY_MAX_KEY_LEN], + }, + pk_r, + Some(dst_hash), + ) + .await?; + } + if dst_hash[..POLICY_HASH_LEN] != expected[..POLICY_HASH_LEN] { + return Err(HsmError::InvalidArg); + } + } + + // ── Phase 2: recover SndrPriv + BKS3, then HPKE-Auth-seal the BKS3 + // to RcvrPub. The masked sealing key and the local backup are + // copied into crypto scratch so the request-buffer borrow is + // confined; every recovered secret is scrubbed on EVERY exit path + // (scope rewind does not clear DMA). + let masking_key = pal.vault_key(io, mk_key_id)?; + + let blob = alloc.dma_alloc(MASKED_SEALING_KEY_LEN)?; + let pok_scratch = alloc.dma_alloc(MASKED_SD_LEN)?; + { + let req = TborSdCreatePeerBackupReq::decode(&*req_buf)?; + blob.copy_from_slice(req.masked_sealing_key()); + pok_scratch.copy_from_slice(req.pok_local_backup()); + } + + // Unmask into `blob`, copy the recovered private key out into its + // own scratch, then scrub `blob` immediately. + let unmask_res = async { + let view = unmask(pal, io, masking_key, blob).await?; + let sndr_priv = alloc.dma_alloc(view.target_key.len())?; + sndr_priv.copy_from_slice(view.target_key); + Ok::<_, HsmError>((view.key_kind, sndr_priv)) + } + .await; + blob.zeroize(); + let (key_kind, sndr_priv) = unmask_res?; + + let crypto_res = async { + if !matches!(key_kind, HsmVaultKeyKind::SdSealing) { + return Err(HsmError::UnsupportedKeyType); + } + + // Sender public key (`SndrPub`) in SEC1 BE, derived on-device + // from the recovered private key. + let pk_s = alloc.dma_alloc(1 + 2 * coord)?; + pal.ecc_pub_from_priv(io, SD_CURVE, sndr_priv, &mut pk_s[1..1 + 2 * coord]) + .await?; + pk_s[0] = SEC1_UNCOMPRESSED; + pk_s[1..1 + coord].reverse(); + pk_s[1 + coord..1 + 2 * coord].reverse(); + + // Recover BKS3 from the caller's local backup (unmask under + // `PartLocalMK`), copy it out, then scrub the staging buffer. + // `recover_bks3_from_pok_local` validates the recovered length is + // `BKS3_LEN`, so the copy below cannot mismatch. + let bks3 = alloc.dma_alloc(BKS3_LEN)?; + let recover_res = async { + let recovered = + sd_backup::recover_bks3_from_pok_local(pal, io, svn, pok_scratch).await?; + bks3.copy_from_slice(recovered); + Ok::<_, HsmError>(()) + } + .await; + pok_scratch.zeroize(); + recover_res?; + + // ── HPKE-Auth-seal the recovered BKS3 to RcvrPub ─────────── + let cfg = HpkeSealConfig::auth( + HPKE_SUITE, + pk_r, + &[], + &[], + AuthParams { + sk_s: sndr_priv, + pk_s, + }, + ); + let seal_res = async { + // Size query, then split the peer-backup response buffer into + // the `enc` and `ct` regions the seal writes. + let sizes = seal(pal, io, &cfg, bks3, None, None, alloc).await?; + if sizes.enc_len + sizes.ct_len != POK_REMOTE_BACKUP_LEN { + return Err(HsmError::InternalError); + } + let (enc, ct) = pok_peer_out.split_at_mut(sizes.enc_len); + seal(pal, io, &cfg, bks3, Some(enc), Some(ct), alloc).await?; + Ok::<_, HsmError>(()) + } + .await; + bks3.zeroize(); + seal_res + } + .await; + + // Scrub the recovered sender private key on every path. + sndr_priv.zeroize(); + crypto_res?; + + Ok(()) + }) + .await?; + + encode_response(pal, io, pok_peer_out) +} + +/// Gate the initial session/state checks and resolve the masking-key ID for +/// the caller-supplied `masked_sealing_key`. +/// +/// Validates that the session is an active Crypto-Officer session and that +/// the partition is `Initialized` (so `PartLocalMK` and the policy hash are +/// available), then routes the masked sealing key to its masking key. +/// Unlike the create/restore provisioning commands, this is **not** +/// one-shot: it neither requires nor sets the SD-initialized flag. +fn gate_request(pal: &P, io: &impl HsmIo, req_buf: &DmaBuf) -> HsmResult { + let req = TborSdCreatePeerBackupReq::decode(req_buf)?; + let sess_id = HsmSessId::from(u16::from(req.session_id())); + validate_crypto_officer_active_session(pal, io, sess_id)?; + + // `PartLocalMK` and the policy hash are provisioned by `PartFinal`, so + // the partition must be finalized (`Initialized`). + if part_state::part_state(pal, io)? != PartState::Initialized { + return Err(HsmError::InvalidArg); + } + + // Route the masked sealing key to its masking key via the cleartext, + // tag-bound metadata (before unmasking). + let scope = peek_metadata(req.masked_sealing_key())? + .usage_flags() + .scope(); + masking_key_id_for_scope(pal, io, scope) +} + +/// Encode the `SdCreatePeerBackup` response around the peer backup. +fn encode_response<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + pok_peer: &DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let resp = pal.dma_alloc_var(io, |buf| { + let frame = TborSdCreatePeerBackupResp::encode(buf, 0, false)? + .pok_peer_backup(pok_peer)? + .finish(); + Ok(frame.as_bytes().len()) + })?; + Ok(resp) +} diff --git a/fw/core/lib/src/ddi/tbor/sd_restore_local_backup.rs b/fw/core/lib/src/ddi/tbor/sd_restore_local_backup.rs index 1a65e0297..19c5dad53 100644 --- a/fw/core/lib/src/ddi/tbor/sd_restore_local_backup.rs +++ b/fw/core/lib/src/ddi/tbor/sd_restore_local_backup.rs @@ -21,12 +21,12 @@ //! already initialized ([`SdAlreadyInitialized`](HsmError::SdAlreadyInitialized)). //! 2. Unmask `pok_local_backup` under the partition-local masking key //! (`PartLocalMK`, from `PartFinal`) → **BKS3**. The blob must be an -//! [`SdPartitionOwnerSeed`](HsmVaultKeyKind::SdPartitionOwnerSeed) +//! [`SdPartitionOwnerSeed`](azihsm_fw_hsm_pal_traits::HsmVaultKeyKind::SdPartitionOwnerSeed) //! envelope, and its bound SVN must not be newer than the current //! firmware SVN ([`SdBackupSvnRollback`](HsmError::SdBackupSvnRollback)). //! 3. Derive `SDBMK` from BKS3 + the partition `policy_hash`, then unmask //! `sd_mk_backup` under `SDBMK` → **SDMK** (must be an -//! [`SdMasking`](HsmVaultKeyKind::SdMasking) envelope; same anti-rollback). +//! [`SdMasking`](azihsm_fw_hsm_pal_traits::HsmVaultKeyKind::SdMasking) envelope; same anti-rollback). //! 4. Re-mask both at the current `{svn, owner}`: `CurrSDLocalBackup = //! mask(BKS3, PartLocalMK)` and `CurrSDKMKBackup = mask(SDMK, SDBMK)`. //! 5. **Commit** ([`commit_sd_to_vault`](super::sd_backup::commit_sd_to_vault)): @@ -39,7 +39,6 @@ //! //! This command is **Crypto-Officer-only**. -use azihsm_fw_core_crypto_key_masking::aead::unmask; use azihsm_fw_ddi_tbor_types::TborSdRestoreLocalBackupReq; use azihsm_fw_ddi_tbor_types::TborSdRestoreLocalBackupResp; use azihsm_fw_ddi_tbor_types::MASKED_SD_LEN; @@ -51,7 +50,6 @@ use azihsm_fw_hsm_pal_traits::HsmPal; use azihsm_fw_hsm_pal_traits::HsmResult; use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; use azihsm_fw_hsm_pal_traits::HsmSessId; -use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; use azihsm_fw_hsm_pal_traits::PartState; use azihsm_fw_hsm_undo::UndoLog; @@ -116,29 +114,7 @@ pub(crate) async fn handle<'p, P: HsmPal>( // EVERY exit path below. let res = async { // ── Recover BKS3 from pok_local_backup under PartLocalMK ── - let bks3 = { - let local_mk_id = part_state::part_local_mk_key_id(pal, io)?; - let local_mk = pal.vault_key(io, local_mk_id)?; - let view = unmask(pal, io, local_mk, pok_scratch).await?; - if !matches!(view.key_kind, HsmVaultKeyKind::SdPartitionOwnerSeed) { - return Err(HsmError::UnsupportedKeyType); - } - // Anti-rollback: a backup minted under a newer SVN cannot be - // restored on this (older) firmware. Enforced after the - // AEAD tag authenticates the envelope, so a tampered - // cleartext SVN fails the tag rather than spoofing this. - if view.svn > svn { - return Err(HsmError::SdBackupSvnRollback); - } - // Firmware invariant: the AEAD tag has authenticated the - // envelope, so a genuine backup always carries a `BKS3_LEN` - // seed; a mismatch signals corruption / a sizing bug, not a - // client error. Mirrors `restore_part_local_mk` in `part_final`. - if view.target_key.len() != sd_backup::BKS3_LEN { - return Err(HsmError::InternalError); - } - view.target_key - }; + let bks3 = sd_backup::recover_bks3_from_pok_local(pal, io, svn, pok_scratch).await?; // Recover SDMK from `mk_scratch`, re-mask both backups, and // commit the SD to the vault (shared with the remote restore). diff --git a/fw/core/lib/src/ddi/tbor/sd_restore_peer_backup.rs b/fw/core/lib/src/ddi/tbor/sd_restore_peer_backup.rs new file mode 100644 index 000000000..84a23a08f --- /dev/null +++ b/fw/core/lib/src/ddi/tbor/sd_restore_peer_backup.rs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `SdRestorePeerBackup` handler. +//! +//! Restores a security domain from a **peer** backup (manticore §3.3.11): +//! it HPKE-Auth-opens the caller-supplied `pok_peer_backup` (an HPKE seal +//! of BKS3) with the receiver's masked SD-sealing key — authenticated by +//! the sender peer's attested key — recovers `SDMK` from +//! `prev_sd_mk_backup`, and returns the device-local backups so the +//! security domain can later be restored locally without the peer. +//! +//! It is [`SdRestoreRemoteBackup`](super::sd_restore_remote_backup) plus a +//! **peer-cloning policy gate**: the recovered backup originates from a +//! peer partition endorsed by the same POTA rather than a remote sealing +//! authority, so it is only accepted when the security domain's policy sets +//! `allow_peer_cloning`. The provisioning back-end is shared with the +//! local and remote restores via +//! [`sd_backup::reprovision_sd_from_bks3`](super::sd_backup::reprovision_sd_from_bks3). +//! +//! Flow: +//! +//! 1. Decode; gate to a Crypto-Officer, `Active` session on an +//! `Initialized` partition, and fail-fast if the SD is already +//! initialized ([`SdAlreadyInitialized`](HsmError::SdAlreadyInitialized)). +//! 2. Bind the caller-supplied [`PartPolicy`] to the partition's fixed +//! `policy_hash`, require its `allow_peer_cloning` flag +//! ([`SdPeerCloningNotAllowed`](HsmError::SdPeerCloningNotAllowed)), then +//! verify the **sender** peer evidence against it: its cert chains are +//! validated and anchored to the policy SATA key, its report's v2 +//! `policy_hash` must equal `SHA-384(policy)`, and its attested COSE_Key +//! is recovered as `SndrPub`. +//! 3. Unmask `masked_sealing_key` → `RcvrPriv` (must be an +//! [`SdSealing`](HsmVaultKeyKind::SdSealing) key) and derive `RcvrPub`. +//! 4. HPKE-Auth-open `pok_peer_backup` (`sk_r = RcvrPriv`, sender-auth +//! `SndrPub`) → **BKS3**. +//! 5. Recover `SDMK` from `prev_sd_mk_backup`, re-mask both backups, vault +//! `SDMK`, and mark the partition SD-initialized — undo-guarded (shared +//! [`reprovision_sd_from_bks3`](super::sd_backup::reprovision_sd_from_bks3)). +//! `RcvrPriv`, BKS3, SDMK, and SDBMK are zeroized before returning. +//! +//! **Stateful & one-shot** (parity with the other SD-provisioning +//! commands). This command is **Crypto-Officer-only**. +//! +//! [`PartPolicy`]: super::policy + +use azihsm_fw_core_crypto_hpke::open; +use azihsm_fw_core_crypto_hpke::HpkeOpenConfig; +use azihsm_fw_core_crypto_hpke::HpkeSuite; +use azihsm_fw_core_crypto_key_masking::aead::peek_metadata; +use azihsm_fw_core_crypto_key_masking::aead::unmask; +use azihsm_fw_core_crypto_key_report::POLICY_HASH_LEN; +use azihsm_fw_core_evidence::verify_evidence; +use azihsm_fw_core_evidence::EvidenceRefs; +use azihsm_fw_core_evidence::TrustAnchors; +use azihsm_fw_core_evidence::ATTESTED_KEY_LEN; +use azihsm_fw_ddi_tbor_types::policy::PolicyKeyKind; +use azihsm_fw_ddi_tbor_types::policy::POLICY_MAX_KEY_LEN; +use azihsm_fw_ddi_tbor_types::TborSdRestorePeerBackupReq; +use azihsm_fw_ddi_tbor_types::TborSdRestorePeerBackupResp; +use azihsm_fw_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_fw_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_fw_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_fw_ddi_tbor_types::SD_MK_BACKUP_LEN; +use azihsm_fw_hsm_oob::OobPtr; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmEccCurve; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmHashAlgo; +use azihsm_fw_hsm_pal_traits::HsmIo; +use azihsm_fw_hsm_pal_traits::HsmPal; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; +use azihsm_fw_hsm_pal_traits::HsmSessId; +use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; +use azihsm_fw_hsm_pal_traits::PartState; +use azihsm_fw_hsm_undo::UndoLog; + +use super::masking_key_id_for_scope; +use super::part_final::verify_policy_hash; +use super::sd_backup; +use super::validate_crypto_officer_active_session; +use crate::part_state; + +/// NIST curve for the SD sealing keys and the peer-backup HPKE seal. +const SD_CURVE: HsmEccCurve = HsmEccCurve::P384; + +/// HPKE ciphersuite for the peer-backup seal. +const HPKE_SUITE: HpkeSuite = HpkeSuite::DHKemP384Sha384AesGcm256; + +/// Length of the BKS3 carried inside the peer backup. +const BKS3_LEN: usize = 48; + +/// SEC1 uncompressed point tag (`0x04 ‖ X ‖ Y`). +const SEC1_UNCOMPRESSED: u8 = 0x04; + +/// Length of the HPKE encapsulated key `enc` (P-384 SEC1 uncompressed). +const SD_ENC_LEN: usize = 1 + 2 * 48; + +// A peer backup is `enc(97) ‖ ct(BKS3 48 + GCM tag 16 = 64)` = 161 B. +const _: () = assert!(SD_ENC_LEN + (BKS3_LEN + 16) == POK_REMOTE_BACKUP_LEN); + +/// Handle a TBOR `SdRestorePeerBackup` request. +/// +/// **Stateful**: re-provisions the security-domain masking key (`SDMK`) in +/// the vault and marks the partition security-domain-initialized, guarded +/// by the per-command `undo` log. The one-shot `SD_INITIALIZED` claim is +/// the race-winner gate against a concurrently-dispatched create/restore. +pub(crate) async fn handle<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + req_buf: &mut DmaBuf, + oob: Option, + undo: &mut UndoLog<'p>, +) -> HsmResult<&'p DmaBuf> { + // Session/state gating + masking-key routing use only the shared + // `decode` view. `mk_key_id` is `Copy`, so it outlives the view. + let mk_key_id = { + let req = TborSdRestorePeerBackupReq::decode(&*req_buf)?; + let sess_id = HsmSessId::from(u16::from(req.session_id())); + validate_crypto_officer_active_session(pal, io, sess_id)?; + + // The SD masking keys / policy hash are provisioned by `PartFinal`, + // so the partition must be finalized (`Initialized`). + if part_state::part_state(pal, io)? != PartState::Initialized { + return Err(HsmError::InvalidArg); + } + + // Fail-fast: a restore onto an already-initialized security domain + // is rejected. The atomic `SD_INITIALIZED` claim in the commit + // phase is the authoritative race-winner gate. + if part_state::part_is_sd_initialized(pal, io)? { + return Err(HsmError::SdAlreadyInitialized); + } + + // Route the masked receiver key to its masking key via the + // cleartext, tag-bound metadata (before unmasking). + let scope = peek_metadata(req.masked_sealing_key())? + .usage_flags() + .scope(); + masking_key_id_for_scope(pal, io, scope)? + }; + + // The sender attestation evidence is mandatory side-band data carried + // in the out-of-band SGL page. + let oob = oob.ok_or(HsmError::InvalidArg)?; + + // Allocate the two fixed-size response backups in the IO scope so they + // survive the crypto scratch allocator's reset. + let pok_local_out = pal.dma_alloc(io, MASKED_SD_LEN)?; + let sd_mk_out = pal.dma_alloc(io, SD_MK_BACKUP_LEN)?; + + pal.alloc_scoped_async(io, async |alloc| -> HsmResult<()> { + let coord = SD_CURVE.priv_key_len(); + let (svn, owner) = sd_backup::platform_svn_owner(pal)?; + + // `pk_sndr` (the attested `SndrPub`) is recovered by the evidence + // check in phase 1 and consumed by the HPKE open in phase 2. + let pk_sndr = alloc.dma_alloc(ATTESTED_KEY_LEN)?; + + // ── Phase 1: policy binding + peer-cloning gate + sender evidence ── + { + let req = TborSdRestorePeerBackupReq::decode(&*req_buf)?; + let policy = req.policy(); + + // The re-supplied policy must match the one bound at `PartInit`. + verify_policy_hash(pal, io, alloc, policy).await?; + let part_policy = super::policy::from_bytes(policy)?; + + // Peer cloning is gated by the (now authenticated) SD policy. + if !part_policy.flags.allow_peer_cloning() { + return Err(HsmError::SdPeerCloningNotAllowed); + } + + let sata = &part_policy.sata_pub_key; + if sata.kind() != PolicyKeyKind::Ecc384 || sata.len() != POLICY_MAX_KEY_LEN { + return Err(HsmError::InvalidArg); + } + + // The sender peer's report must attest to the same policy. + let expected = alloc.dma_alloc(POLICY_HASH_LEN)?; + pal.hash(io, HsmHashAlgo::Sha384, policy, expected, true) + .await?; + + let src_hash = alloc.dma_alloc(POLICY_HASH_LEN)?; + { + let ev = req.src_evidence(); + verify_evidence( + pal, + io, + &oob, + &EvidenceRefs { + mfgr_chain: ev.mfgr_cert_chain(), + owner_chain: ev.owner_cert_chain(), + part_owner_chain: ev.part_owner_cert_chain(), + report: ev.evidence(), + }, + &TrustAnchors { + sata: &sata.data[..POLICY_MAX_KEY_LEN], + }, + pk_sndr, + Some(src_hash), + ) + .await?; + } + if src_hash[..POLICY_HASH_LEN] != expected[..POLICY_HASH_LEN] { + return Err(HsmError::InvalidArg); + } + } + + // ── Phase 2: recover RcvrPriv, HPKE-open the peer backup, and + // re-provision the SD. The masked key, peer backup, and previous + // masking-key backup are copied into crypto scratch so the + // request-buffer borrow is confined; every recovered secret is + // scrubbed on EVERY exit path (scope rewind does not clear DMA). + let masking_key = pal.vault_key(io, mk_key_id)?; + + let blob = alloc.dma_alloc(MASKED_SEALING_KEY_LEN)?; + let peer_backup = alloc.dma_alloc(POK_REMOTE_BACKUP_LEN)?; + let prev_sd_mk = alloc.dma_alloc(SD_MK_BACKUP_LEN)?; + { + let req = TborSdRestorePeerBackupReq::decode(&*req_buf)?; + blob.copy_from_slice(req.masked_sealing_key()); + peer_backup.copy_from_slice(req.pok_peer_backup()); + prev_sd_mk.copy_from_slice(req.prev_sd_mk_backup()); + } + + // Unmask into `blob`, copy the recovered private key out into its + // own scratch, then scrub `blob` immediately. + let unmask_res = async { + let view = unmask(pal, io, masking_key, blob).await?; + let rcvr_priv = alloc.dma_alloc(view.target_key.len())?; + rcvr_priv.copy_from_slice(view.target_key); + Ok::<_, HsmError>((view.key_kind, rcvr_priv)) + } + .await; + blob.zeroize(); + let (key_kind, rcvr_priv) = unmask_res?; + + let crypto_res = async { + if !matches!(key_kind, HsmVaultKeyKind::SdSealing) { + return Err(HsmError::UnsupportedKeyType); + } + + // Receiver public key (`RcvrPub`) in SEC1 BE, derived on-device + // from the recovered private key. + let pk_r = alloc.dma_alloc(1 + 2 * coord)?; + pal.ecc_pub_from_priv(io, SD_CURVE, rcvr_priv, &mut pk_r[1..1 + 2 * coord]) + .await?; + pk_r[0] = SEC1_UNCOMPRESSED; + pk_r[1..1 + coord].reverse(); + pk_r[1 + coord..1 + 2 * coord].reverse(); + + // HPKE `open` needs a plaintext buffer at least the ciphertext + // length (`ct` = BKS3 + GCM tag = 64 B); the recovered BKS3 + // occupies its first `BKS3_LEN` bytes. Both hold secret + // material and are scrubbed on all paths. + let pt_buf = alloc.dma_alloc(POK_REMOTE_BACKUP_LEN - SD_ENC_LEN)?; + let bks3 = alloc.dma_alloc(BKS3_LEN)?; + let inner = async { + // ── Open the peer backup with RcvrPriv, authenticated by + // the sender peer key (`SndrPub`). + let (enc, ct) = peer_backup.split_at(SD_ENC_LEN); + let open_cfg = HpkeOpenConfig::auth(HPKE_SUITE, rcvr_priv, pk_r, &[], &[], pk_sndr); + let pt_len = + open(pal, io, &open_cfg, enc, ct, Some(&mut pt_buf[..]), alloc).await?; + if pt_len != BKS3_LEN { + return Err(HsmError::InvalidArg); + } + bks3.copy_from_slice(&pt_buf[..BKS3_LEN]); + + // ── Recover SDMK, re-mask both backups, and commit the SD + // to the vault (shared with the local / remote restore). + sd_backup::reprovision_sd_from_bks3( + pal, + io, + alloc, + undo, + svn, + owner, + bks3, + prev_sd_mk, + pok_local_out, + sd_mk_out, + ) + .await + } + .await; + + bks3.zeroize(); + pt_buf.zeroize(); + prev_sd_mk.zeroize(); + inner + } + .await; + + // Scrub the recovered receiver private key on every path. + rcvr_priv.zeroize(); + crypto_res?; + + Ok(()) + }) + .await?; + + encode_response(pal, io, pok_local_out, sd_mk_out) +} + +/// Encode the `SdRestorePeerBackup` response around the local backups. +fn encode_response<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + pok_local: &DmaBuf, + sd_mk: &DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let resp = pal.dma_alloc_var(io, |buf| { + let frame = TborSdRestorePeerBackupResp::encode(buf, 0, false)? + .pok_local_backup(pok_local)? + .sd_mk_backup(sd_mk)? + .finish(); + Ok(frame.as_bytes().len()) + })?; + Ok(resp) +} diff --git a/fw/core/lib/src/op.rs b/fw/core/lib/src/op.rs index 652710c0f..958cbc576 100644 --- a/fw/core/lib/src/op.rs +++ b/fw/core/lib/src/op.rs @@ -266,6 +266,8 @@ impl SessionCtrl { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::SD_RESTORE_REMOTE_BACKUP | opcode::SD_RESTORE_LOCAL_BACKUP + | opcode::SD_CREATE_PEER_BACKUP + | opcode::SD_RESTORE_PEER_BACKUP | opcode::KEY_REPORT => Self::InSession, opcode::SESSION_CLOSE => Self::Close, _ => Self::NoSession, diff --git a/fw/pal/traits/src/error.rs b/fw/pal/traits/src/error.rs index 3dd6ef0ff..2913fc736 100644 --- a/fw/pal/traits/src/error.rs +++ b/fw/pal/traits/src/error.rs @@ -422,6 +422,14 @@ pub enum HsmError { /// spoofing this error. SdBackupSvnRollback = 0x08700109, + /// A `SdCreatePeerBackup` / `SdRestorePeerBackup` handler was asked to + /// clone a security domain to (or from) a peer partition, but the + /// partition's unified [`PartPolicy`](crate::PartPolicy) does not permit + /// peer cloning (its `allow_peer_cloning` policy flag is clear). Peer + /// backup/restore is only allowed when the security-domain owner has + /// opted in via policy. + SdPeerCloningNotAllowed = 0x0870010A, + // Firmware-internal diagnostic codes logged by the CPU fault and panic // exception handlers (`azihsm_fw_uno_fault`). These are not DDI protocol // statuses: they use the PAL diagnostic facility (`0x08F`) to stay clear of