diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f582dea7b..fd0e0549b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -86,6 +86,34 @@ name = "fuzz_tbor_part_final" path = "fuzz_targets/ddi/tbor/fuzz_tbor_part_final.rs" test = false +[[bin]] +bench = false +doc = false +name = "fuzz_tbor_sd_create_peer_backup" +path = "fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_peer_backup.rs" +test = false + +[[bin]] +bench = false +doc = false +name = "fuzz_tbor_sd_create_remote_backup" +path = "fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_remote_backup.rs" +test = false + +[[bin]] +bench = false +doc = false +name = "fuzz_tbor_sd_restore_local_backup" +path = "fuzz_targets/ddi/tbor/fuzz_tbor_sd_restore_local_backup.rs" +test = false + +[[bin]] +bench = false +doc = false +name = "fuzz_tbor_sd_reseal_remote_backup" +path = "fuzz_targets/ddi/tbor/fuzz_tbor_sd_reseal_remote_backup.rs" +test = false + [[bin]] bench = false doc = false diff --git a/fuzz/fuzz_targets/common.rs b/fuzz/fuzz_targets/common.rs index c38625c35..40b947361 100644 --- a/fuzz/fuzz_targets/common.rs +++ b/fuzz/fuzz_targets/common.rs @@ -46,6 +46,7 @@ use azihsm_ddi_tbor_types::TborKeyReportReq; use azihsm_ddi_tbor_types::TborPartInfoReq; use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; use libfuzzer_sys::arbitrary; use libfuzzer_sys::arbitrary::Arbitrary; use zerocopy::TryFromBytes; @@ -465,8 +466,7 @@ pub fn known_good_part_policy(pota_pub_key: [u8; POLICY_MAX_KEY_LEN]) -> [u8; PA bytes } -/// Drive `PartInit` → `PartFinal` so the partition is initialized and -/// partition-local masking keys are available to a fuzz target. +/// Drive `PartInit` → `PartFinal` so partition-local masking keys are available. pub fn finalize_partition(ctx: &TestCtx, session: &SessionHandshake) { let pota = CaKey::generate(); let policy = known_good_part_policy(pota.raw_pub()); @@ -488,8 +488,7 @@ fn add_evidence_item(oob_items: &mut Vec>, der: &[u8]) -> CertDescriptor } } -/// Initialize a partition and create a test security domain so its -/// masking key is available to a fuzz target. +/// Initialize a partition and security domain so security-domain masking is available. pub fn create_test_security_domain(ctx: &TestCtx, session: &SessionHandshake) { use zerocopy::IntoBytes; @@ -553,10 +552,29 @@ pub fn create_test_security_domain(ctx: &TestCtx, session: &SessionHandshake) { }) .expect("sealing-key report generation should succeed"); + let mut receiver_pub = [0u8; RAW_PUB_LEN]; + let coord_len = RAW_PUB_LEN / 2; + for (dst, src) in receiver_pub[..coord_len] + .iter_mut() + .zip(sealing_key.pub_key[..coord_len].iter().rev()) + { + *dst = *src; + } + for (dst, src) in receiver_pub[coord_len..] + .iter_mut() + .zip(sealing_key.pub_key[coord_len..].iter().rev()) + { + *dst = *src; + } + let receiver = make_chain(&sata, &receiver_pub); let mfgr = make_chain(&CaKey::generate(), &pid_pub); let owner = make_chain(&CaKey::generate(), &pid_pub); let part_owner = make_chain(&sata, &pid_pub); let mut oob_items = Vec::new(); + let receiver_chain = vec![ + add_evidence_item(&mut oob_items, &receiver.root_der), + add_evidence_item(&mut oob_items, &receiver.leaf_der), + ]; let mfgr_chain = vec![ add_evidence_item(&mut oob_items, &mfgr.root_der), add_evidence_item(&mut oob_items, &mfgr.leaf_der), @@ -574,6 +592,7 @@ pub fn create_test_security_domain(ctx: &TestCtx, session: &SessionHandshake) { let req = TborSdCreateRemoteBackupReq { session_id: session.session_id, masked_sealing_key: sealing_key.masked_key, + receiver_cert_chain: receiver_chain, receiver_mfgr_cert_chain: mfgr_chain, receiver_owner_cert_chain: owner_chain, receiver_part_owner_cert_chain: part_owner_chain, @@ -588,3 +607,29 @@ pub fn create_test_security_domain(ctx: &TestCtx, session: &SessionHandshake) { ctx.tbor_oob(&req, &oob) .expect("test security-domain creation should succeed"); } + +/// Exercise key scopes that cannot be used by the backup fixtures' custom partition. +pub fn exercise_non_partition_key_scope( + ctx: &TestCtx, + session: &SessionHandshake, + scope: KeyScope, +) { + let req = TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: scope.to_tbor(), + }; + match scope { + KeyScope::SecurityDomain => { + create_test_security_domain(ctx, session); + ctx.tbor(&req) + .expect("security-domain-scoped sealing-key generation should succeed"); + } + KeyScope::Unspecified | KeyScope::Session | KeyScope::Internal => { + finalize_partition(ctx, session); + ctx.expect_fw_reject(&req, TborStatus::UnsupportedKeyScope); + } + KeyScope::Ephemeral | KeyScope::Local => { + unreachable!("partition scopes are exercised by the backup fixture"); + } + } +} diff --git a/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_peer_backup.rs b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_peer_backup.rs new file mode 100644 index 000000000..8d0f7df23 --- /dev/null +++ b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_peer_backup.rs @@ -0,0 +1,468 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![no_main] + +#[path = "../../common.rs"] +mod common; + +use azihsm_ddi_tbor_test_harness::ROTATED_CO_PSK; +use azihsm_ddi_tbor_test_harness::ROTATED_CU_PSK; +use azihsm_ddi_tbor_test_harness::TestCtx; +use azihsm_ddi_tbor_test_harness::bootstrap_rotated_co; +use azihsm_ddi_tbor_test_harness::bootstrap_rotated_cu; +use azihsm_ddi_tbor_test_harness::x509_fixture::CaKey; +use azihsm_ddi_tbor_test_harness::x509_fixture::RAW_PUB_LEN; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_pta_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::pta_pub_from_csr; +use azihsm_ddi_tbor_types::CertDescriptor; +use azihsm_ddi_tbor_types::KEY_REPORT_DATA_LEN; +use azihsm_ddi_tbor_types::KEY_REPORT_MAX_LEN; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::PolicyFlags; +use azihsm_ddi_tbor_types::PolicyKeyKind; +use azihsm_ddi_tbor_types::PolicyPubKey; +use azihsm_ddi_tbor_types::ReportDescriptor; +use azihsm_ddi_tbor_types::TborKeyReportReq; +use azihsm_ddi_tbor_types::TborPartInfoReq; +use azihsm_ddi_tbor_types::TborSdCreatePeerBackupReq; +use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::tbor_int::U16; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use zerocopy::IntoBytes; +use zerocopy::TryFromBytes; + +#[derive(Debug, Clone, Copy, Arbitrary)] +enum Fault { + ValidSelfPeer, + ValidDistinctKeys, + Repeat, + NotFinalized, + MissingOob, + PolicyMismatch, + CloningDisabled, + EmptyMfgrChain, + EmptyOwnerChain, + EmptyPartOwnerChain, + WrongSataAnchor, + MismatchedEvidenceLeaf, + InvalidReportSignature, + InvalidSenderTag, + InvalidBackupTag, + WrongKindBackup, + ZeroReportLength, + OversizedReportLength, + ReportIndexOutOfRange, + ActiveCuSession, + InactiveCoSession, +} + +#[derive(Arbitrary, Debug)] +struct FuzzInput { + fault: Fault, + key_scope: common::KeyScope, + policy_info: [u8; 64], + report_data: [u8; KEY_REPORT_DATA_LEN], + mutation_mask: u8, + repeat_count: u8, +} + +struct Evidence { + receiver: Vec, + mfgr: Vec, + owner: Vec, + part_owner: Vec, + report: ReportDescriptor, + oob: Vec>, +} + +impl Evidence { + fn oob_slices(&self) -> Vec<&[u8]> { + self.oob.iter().map(Vec::as_slice).collect() + } +} + +fn push_item(oob: &mut Vec>, bytes: &[u8]) -> CertDescriptor { + let descriptor = CertDescriptor { + index: oob.len() as u8, + length: U16::new(bytes.len() as u16), + }; + oob.push(bytes.to_vec()); + descriptor +} + +fn push_chain( + oob: &mut Vec>, + chain: &azihsm_ddi_tbor_test_harness::x509_fixture::GeneratedChain, +) -> Vec { + vec![ + push_item(oob, &chain.root_der), + push_item(oob, &chain.leaf_der), + ] +} + +fn make_evidence_with_sata( + sata: &CaKey, + pid_pub: &[u8; RAW_PUB_LEN], + recipient_pub: &[u8; RAW_PUB_LEN], + report_bytes: &[u8], +) -> Evidence { + let mut oob = Vec::new(); + let receiver = push_chain(&mut oob, &make_chain(sata, recipient_pub)); + let mfgr = push_chain(&mut oob, &make_chain(&CaKey::generate(), pid_pub)); + let owner = push_chain(&mut oob, &make_chain(&CaKey::generate(), pid_pub)); + let part_owner = push_chain(&mut oob, &make_chain(sata, pid_pub)); + let report = push_item(&mut oob, report_bytes); + + Evidence { + receiver, + mfgr, + owner, + part_owner, + report: ReportDescriptor { + index: report.index, + length: report.length, + }, + oob, + } +} + +fn public_key_be(wire_le: &[u8; 96]) -> [u8; 96] { + let mut out = [0u8; 96]; + for i in 0..48 { + out[i] = wire_le[47 - i]; + out[48 + i] = wire_le[95 - i]; + } + out +} + +fn build_policy( + pota: &CaKey, + sata: &CaKey, + pid: &[u8], + pid_pub: &[u8; RAW_PUB_LEN], + info: [u8; 64], + allow_cloning: bool, +) -> PartPolicy { + let mut bytes = common::known_good_part_policy(pota.raw_pub()); + let mut policy = + PartPolicy::try_read_from_bytes(&bytes).expect("known-good policy must decode"); + policy.sata_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, RAW_PUB_LEN as u16, sata.raw_pub()); + policy.backup_part_id.copy_from_slice(pid); + policy.backup_part_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, RAW_PUB_LEN as u16, *pid_pub); + policy.flags = if allow_cloning { + PolicyFlags::from_bits(PolicyFlags::ALLOW_PEER_CLONING) + } else { + PolicyFlags::new() + }; + policy.info = info; + bytes.copy_from_slice(policy.as_bytes()); + PartPolicy::try_read_from_bytes(&bytes).expect("constructed policy must decode") +} + +fn checked_success(ctx: &TestCtx, req: &TborSdCreatePeerBackupReq, oob: &[&[u8]]) { + let response = ctx + .tbor_oob(req, oob) + .expect("SdCreatePeerBackup should succeed for a valid fixture"); + assert_eq!( + response.pok_peer_backup.len(), + POK_REMOTE_BACKUP_LEN, + "peer backup response must have the schema length", + ); + assert!( + response.pok_peer_backup.iter().any(|&byte| byte != 0), + "peer backup response must not be all zero", + ); + assert_eq!( + response.pok_peer_backup[0], 0x04, + "HPKE encapsulated key must begin with an uncompressed P-384 point", + ); +} + +fn setup_unfinalized(ctx: &TestCtx) { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let req = TborSdCreatePeerBackupReq { + session_id: session.session_id, + masked_sealing_key: [0; MASKED_SEALING_KEY_LEN], + policy: PartPolicy::zeroed(), + 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: [0; MASKED_SD_LEN], + }; + ctx.expect_fw_reject(&req, TborStatus::InvalidArg); + ctx.session_close(session.session_id) + .expect("close unfinalized CO session"); +} + +fuzz_target!(|input: FuzzInput| { + common::common_fuzz_test(&|ctx: &TestCtx, _path: &str| { + if !matches!( + input.key_scope, + common::KeyScope::Ephemeral | common::KeyScope::Local + ) { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + common::exercise_non_partition_key_scope(ctx, &session, input.key_scope); + ctx.session_close(session.session_id) + .expect("close key-scope CO session"); + return; + } + + if matches!(input.fault, Fault::NotFinalized) { + setup_unfinalized(ctx); + return; + } + + let mut session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let info = ctx + .tbor(&TborPartInfoReq::new()) + .expect("PartInfo should succeed"); + let pid_pub: [u8; RAW_PUB_LEN] = info + .pid_pub_key + .as_slice() + .try_into() + .expect("PartInfo PID public key must be P-384"); + + let pota = CaKey::generate(); + let sata = CaKey::generate(); + let allow_cloning = !matches!(input.fault, Fault::CloningDisabled); + let policy = build_policy( + &pota, + &sata, + &info.pid, + &pid_pub, + input.policy_info, + allow_cloning, + ); + let policy_bytes: [u8; PART_POLICY_LEN] = + policy.as_bytes().try_into().expect("policy size is pinned"); + + let init = ctx + .part_init( + &session, + &common::mach_seed(), + &policy_bytes, + &common::pota_thumbprint(), + ) + .expect("PartInit should succeed for the valid policy"); + let pta = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + let pta_items = pta.der_items(); + let finalized = ctx + .part_final(&session, &policy_bytes, &[], &pta_items) + .expect("PartFinal should succeed for the valid PTA chain"); + + let recipient = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: input.key_scope.to_tbor(), + }) + .expect("recipient SdSealingKeyGen should succeed"); + let report = ctx + .tbor(&TborKeyReportReq { + session_id: session.session_id, + masked_key: recipient.masked_key.to_vec(), + report_data: input.report_data, + }) + .expect("recipient KeyReport should succeed"); + let recipient_pub = public_key_be(&recipient.pub_key); + + let mut evidence = make_evidence_with_sata(&sata, &pid_pub, &recipient_pub, &report.report); + + let sender = if matches!(input.fault, Fault::ValidSelfPeer | Fault::Repeat) { + recipient.masked_key + } else { + ctx.tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: input.key_scope.to_tbor(), + }) + .expect("sender SdSealingKeyGen should succeed") + .masked_key + }; + + let remote_req = TborSdCreateRemoteBackupReq { + session_id: session.session_id, + masked_sealing_key: recipient.masked_key, + receiver_cert_chain: evidence.receiver.clone(), + receiver_mfgr_cert_chain: evidence.mfgr.clone(), + receiver_owner_cert_chain: evidence.owner.clone(), + receiver_part_owner_cert_chain: evidence.part_owner.clone(), + receiver_report: evidence.report, + policy: policy.clone(), + }; + let oob = evidence.oob_slices(); + let local_backup = ctx + .tbor_oob(&remote_req, &oob) + .expect("SdCreateRemoteBackup fixture must create a valid local backup") + .pok_local_backup; + + if input.key_scope == common::KeyScope::Local { + // Reboot and restore only PartLocalMK. The generated local backup + // and sealing-key envelopes remain valid, but the security domain + // itself is not restored, so the fuzzed command exercises its + // pre-SD-init lifecycle path as well as its stateless repeatability. + ctx.session_close(session.session_id) + .expect("close setup CO session before reboot"); + ctx.erase() + .expect("factory-reset between backup generation and peer create"); + session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let restore_init = ctx + .part_init( + &session, + &common::mach_seed(), + &policy_bytes, + &common::pota_thumbprint(), + ) + .expect("PartInit should succeed after fixture reboot"); + let restore_pta = make_pta_chain(&pota, &pta_pub_from_csr(&restore_init.pta_csr)); + let restore_pta_items = restore_pta.der_items(); + ctx.part_final( + &session, + &policy_bytes, + &finalized.local_mk_backup, + &restore_pta_items, + ) + .expect("PartFinal should restore only the local masking key"); + } + + // Apply only mutations with a deterministic semantic outcome. A zero + // mask deliberately leaves the fixture valid and must remain success. + let mutation_is_noop = input.mutation_mask == 0; + match input.fault { + Fault::WrongSataAnchor => { + let wrong_sata = CaKey::generate(); + let chain = make_chain(&wrong_sata, &pid_pub); + evidence.part_owner = push_chain(&mut evidence.oob, &chain); + } + Fault::MismatchedEvidenceLeaf => { + let other_leaf = CaKey::generate().raw_pub(); + let chain = make_chain(&CaKey::generate(), &other_leaf); + evidence.owner = push_chain(&mut evidence.oob, &chain); + } + Fault::InvalidReportSignature if !mutation_is_noop => { + let desc = evidence.report; + evidence.oob[usize::from(desc.index)][usize::from(desc.length.get()) - 1] ^= + input.mutation_mask; + } + Fault::EmptyMfgrChain => evidence.mfgr.clear(), + Fault::EmptyOwnerChain => evidence.owner.clear(), + Fault::EmptyPartOwnerChain => evidence.part_owner.clear(), + Fault::ZeroReportLength => evidence.report.length = U16::new(0), + Fault::OversizedReportLength => { + evidence.report.length = U16::new((KEY_REPORT_MAX_LEN + 1) as u16) + } + Fault::ReportIndexOutOfRange => evidence.report.index = u8::MAX, + _ => {} + } + + let mut session_open = true; + if matches!( + input.fault, + Fault::ActiveCuSession | Fault::InactiveCoSession + ) { + ctx.session_close(session.session_id) + .expect("close CO session before session-gate scenario"); + session_open = false; + if matches!(input.fault, Fault::ActiveCuSession) { + session = bootstrap_rotated_cu(ctx, &ROTATED_CU_PSK); + session_open = true; + } + } + + let mut req = TborSdCreatePeerBackupReq { + session_id: session.session_id, + masked_sealing_key: sender, + policy: policy.clone(), + 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: local_backup, + }; + + match input.fault { + Fault::PolicyMismatch => req.policy.info[0] ^= 1, + Fault::InvalidSenderTag if !mutation_is_noop => { + req.masked_sealing_key[MASKED_SEALING_KEY_LEN - 1] ^= input.mutation_mask; + } + Fault::InvalidBackupTag if !mutation_is_noop => { + req.pok_local_backup[MASKED_SD_LEN - 1] ^= input.mutation_mask; + } + Fault::WrongKindBackup => { + // SdSealingKeyGen returns a genuine partition-scoped masked-key + // envelope with the same fixed size, but the recovery + // primitive requires SdPartitionOwnerSeed. + req.pok_local_backup = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: input.key_scope.to_tbor(), + }) + .expect("wrong-kind envelope fixture should be valid") + .masked_key; + } + _ => {} + } + + let expected_reject = match input.fault { + Fault::MissingOob => Some(TborStatus::InvalidArg), + Fault::PolicyMismatch => Some(TborStatus::InvalidArg), + Fault::CloningDisabled => Some(TborStatus::SdPeerCloningNotAllowed), + Fault::EmptyMfgrChain | Fault::EmptyOwnerChain | Fault::EmptyPartOwnerChain => { + Some(TborStatus::InvalidArg) + } + Fault::WrongSataAnchor | Fault::MismatchedEvidenceLeaf => Some(TborStatus::InvalidArg), + Fault::InvalidReportSignature if !mutation_is_noop => Some(TborStatus::InvalidArg), + Fault::InvalidSenderTag if !mutation_is_noop => { + Some(TborStatus::AesGcmDecryptTagDoesNotMatch) + } + Fault::InvalidBackupTag if !mutation_is_noop => { + Some(TborStatus::AesGcmDecryptTagDoesNotMatch) + } + Fault::WrongKindBackup => Some(TborStatus::UnsupportedKeyType), + Fault::ZeroReportLength + | Fault::OversizedReportLength + | Fault::ReportIndexOutOfRange => Some(TborStatus::InvalidArg), + Fault::ActiveCuSession => Some(TborStatus::InvalidPermissions), + Fault::InactiveCoSession => Some(TborStatus::SessionNotFound), + _ => None, + }; + + let should_succeed = expected_reject.is_none(); + let missing_oob = matches!(input.fault, Fault::MissingOob); + if should_succeed { + let oob = evidence.oob_slices(); + let calls = if matches!(input.fault, Fault::Repeat) { + 2 + usize::from(input.repeat_count % 3) + } else { + 1 + }; + for _ in 0..calls { + checked_success(ctx, &req, &oob); + } + } else { + let status = expected_reject.expect("rejection classification set above"); + if missing_oob { + ctx.expect_fw_reject(&req, status); + } else { + let oob = evidence.oob_slices(); + ctx.expect_fw_reject_oob(&req, &oob, status); + } + } + + if session_open { + ctx.session_close(session.session_id) + .expect("close fuzz session"); + } + }); +}); diff --git a/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_remote_backup.rs b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_remote_backup.rs new file mode 100644 index 000000000..ab3b507e3 --- /dev/null +++ b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_create_remote_backup.rs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![no_main] + +#[path = "../../common.rs"] +mod common; + +use azihsm_ddi_tbor_test_harness::ROTATED_CO_PSK; +use azihsm_ddi_tbor_test_harness::TestCtx; +use azihsm_ddi_tbor_test_harness::bootstrap_rotated_co; +use azihsm_ddi_tbor_test_harness::x509_fixture::CaKey; +use azihsm_ddi_tbor_test_harness::x509_fixture::GeneratedChain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_pta_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::pta_pub_from_csr; +use azihsm_ddi_tbor_types::CertDescriptor; +use azihsm_ddi_tbor_types::KEY_REPORT_DATA_LEN; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_ddi_tbor_types::POLICY_MAX_KEY_LEN; +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::PolicyFlags; +use azihsm_ddi_tbor_types::PolicyKeyKind; +use azihsm_ddi_tbor_types::PolicyPubKey; +use azihsm_ddi_tbor_types::ReportDescriptor; +use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; +use azihsm_ddi_tbor_types::TborKeyReportReq; +use azihsm_ddi_tbor_types::TborPartInfoReq; +use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::tbor_int::U16; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use zerocopy::IntoBytes; +use zerocopy::TryFromBytes; + +/// Each variant starts from a freshly provisioned, otherwise-valid +/// partition and request. Mutations therefore reach distinct handler gates +/// instead of spending most inputs on malformed TBOR framing. +#[derive(Arbitrary, Clone, Copy, Debug)] +enum Mutation { + Valid, + ValidTrustedSa, + MissingOob, + EmptyReceiverChain, + WrongSataAnchor, + WrongBackingPartition, + PolicyHashMismatch, + TamperReceiverCertificate { offset: u16, mask: u8 }, + CorruptMaskedSealingKey { offset: u16, mask: u8 }, + TamperTrustedReport { mask: u8 }, + TrustedReceiverKeyMismatch, + TrustedEvidenceLeafMismatch, + OneShot, +} + +#[derive(Arbitrary, Debug)] +struct FuzzInput { + mutation: Mutation, + key_scope: common::KeyScope, +} + +impl Default for FuzzInput { + fn default() -> Self { + Self { + mutation: Mutation::Valid, + key_scope: common::KeyScope::Local, + } + } +} + +struct Fixture { + session_id: u16, + policy: [u8; PART_POLICY_LEN], + request: TborSdCreateRemoteBackupReq, + oob_items: Vec>, + receiver_leaf_index: usize, + report_index: Option, +} + +fn build_policy( + pota_pub: [u8; POLICY_MAX_KEY_LEN], + sata_pub: [u8; POLICY_MAX_KEY_LEN], + sapota_pub: Option<[u8; POLICY_MAX_KEY_LEN]>, + pid: &[u8], + pid_pub: &[u8], + bind_backing_partition: bool, +) -> [u8; PART_POLICY_LEN] { + let mut policy_bytes = common::known_good_part_policy(pota_pub); + let mut policy = + ::try_read_from_bytes(&policy_bytes).expect("known policy"); + + policy.sata_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, POLICY_MAX_KEY_LEN as u16, sata_pub); + if let Some(sapota_pub) = sapota_pub { + policy.sapota_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, POLICY_MAX_KEY_LEN as u16, sapota_pub); + policy.flags = PolicyFlags::from_bits(PolicyFlags::REQUIRE_TRUSTED_SA_KEY); + } + + if bind_backing_partition { + policy.backup_part_id.copy_from_slice(pid); + let pid_pub: [u8; POLICY_MAX_KEY_LEN] = + pid_pub.try_into().expect("PartInfo public key is P-384"); + policy.backup_part_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, POLICY_MAX_KEY_LEN as u16, pid_pub); + } + + policy_bytes.copy_from_slice(policy.as_bytes()); + policy_bytes +} + +fn push_oob_item(items: &mut Vec>, der: &[u8]) -> CertDescriptor { + let index = u8::try_from(items.len()).expect("bounded OOB item count"); + let length = u16::try_from(der.len()).expect("bounded fixture DER length"); + items.push(der.to_vec()); + CertDescriptor { + index, + length: U16::new(length), + } +} + +fn push_chain(items: &mut Vec>, chain: GeneratedChain) -> Vec { + vec![ + push_oob_item(items, &chain.root_der), + push_oob_item(items, &chain.leaf_der), + ] +} + +fn raw_pub_from_wire(pub_key: &[u8]) -> [u8; POLICY_MAX_KEY_LEN] { + const COORD_LEN: usize = POLICY_MAX_KEY_LEN / 2; + assert_eq!(pub_key.len(), POLICY_MAX_KEY_LEN); + + let mut raw = [0u8; POLICY_MAX_KEY_LEN]; + for (dst, src) in raw[..COORD_LEN] + .iter_mut() + .zip(pub_key[..COORD_LEN].iter().rev()) + { + *dst = *src; + } + for (dst, src) in raw[COORD_LEN..] + .iter_mut() + .zip(pub_key[COORD_LEN..].iter().rev()) + { + *dst = *src; + } + raw +} + +fn build_fixture( + ctx: &TestCtx, + trusted_sa: bool, + bind_backing_partition: bool, + mutation: &Mutation, + key_scope: common::KeyScope, +) -> Fixture { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let part_info = ctx.tbor(&TborPartInfoReq::new()).expect("PartInfo"); + + let pota = CaKey::generate(); + let sata = CaKey::generate(); + let sapota = trusted_sa.then(CaKey::generate); + let policy = build_policy( + pota.raw_pub(), + sata.raw_pub(), + sapota.as_ref().map(CaKey::raw_pub), + &part_info.pid, + &part_info.pid_pub_key, + bind_backing_partition, + ); + + let init = ctx + .part_init( + &session, + &common::mach_seed(), + &policy, + &common::pota_thumbprint(), + ) + .expect("PartInit"); + let pta_chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + ctx.part_final(&session, &policy, &[], &pta_chain.der_items()) + .expect("PartFinal"); + + let sealing_key = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: key_scope.to_tbor(), + }) + .expect("SdSealingKeyGen"); + let masked_sealing_key = sealing_key.masked_key.to_vec(); + let receiver_pub = raw_pub_from_wire(&sealing_key.pub_key); + let report = trusted_sa.then(|| { + ctx.tbor(&TborKeyReportReq { + session_id: session.session_id, + masked_key: masked_sealing_key.clone(), + report_data: [0u8; KEY_REPORT_DATA_LEN], + }) + .expect("KeyReport") + .report + }); + + let receiver_pub = if matches!(mutation, Mutation::TrustedReceiverKeyMismatch) { + CaKey::generate().raw_pub() + } else { + receiver_pub + }; + let receiver_chain = if matches!(mutation, Mutation::WrongSataAnchor) { + let wrong_sata = CaKey::generate(); + make_chain(&wrong_sata, &receiver_pub) + } else { + make_chain(&sata, &receiver_pub) + }; + + let mut oob_items = Vec::new(); + let receiver_desc = push_chain(&mut oob_items, receiver_chain); + let receiver_leaf_index = usize::from(receiver_desc[1].index); + let mut mfgr_desc = Vec::new(); + let mut owner_desc = Vec::new(); + let mut part_owner_desc = Vec::new(); + let mut report_desc = ReportDescriptor::default(); + let mut report_index = None; + + if trusted_sa { + let pid_pub: [u8; POLICY_MAX_KEY_LEN] = part_info + .pid_pub_key + .as_slice() + .try_into() + .expect("PartInfo public key is P-384"); + let mfgr = make_chain(&CaKey::generate(), &pid_pub); + let owner = if matches!(mutation, Mutation::TrustedEvidenceLeafMismatch) { + let other_pid_pub = CaKey::generate().raw_pub(); + make_chain(&CaKey::generate(), &other_pid_pub) + } else { + make_chain(&CaKey::generate(), &pid_pub) + }; + let part_owner = make_chain( + sapota.as_ref().expect("trusted policy has SAPOTA"), + &pid_pub, + ); + mfgr_desc = push_chain(&mut oob_items, mfgr); + owner_desc = push_chain(&mut oob_items, owner); + part_owner_desc = push_chain(&mut oob_items, part_owner); + + let report_item = push_oob_item( + &mut oob_items, + report.as_deref().expect("trusted fixture has report"), + ); + report_index = Some(usize::from(report_item.index)); + report_desc = ReportDescriptor { + index: report_item.index, + length: report_item.length, + }; + } + + let policy_value = + ::try_read_from_bytes(&policy).expect("policy image"); + let request = TborSdCreateRemoteBackupReq { + session_id: session.session_id, + masked_sealing_key: masked_sealing_key + .as_slice() + .try_into() + .expect("SdSealingKeyGen returns a fixed-length key"), + receiver_cert_chain: receiver_desc, + receiver_mfgr_cert_chain: mfgr_desc, + receiver_owner_cert_chain: owner_desc, + receiver_part_owner_cert_chain: part_owner_desc, + receiver_report: report_desc, + policy: policy_value, + }; + + Fixture { + session_id: session.session_id, + policy, + request, + oob_items, + receiver_leaf_index, + report_index, + } +} + +fn assert_success(ctx: &TestCtx, fixture: &Fixture) { + let oob: Vec<&[u8]> = fixture.oob_items.iter().map(Vec::as_slice).collect(); + let response = ctx + .tbor_oob(&fixture.request, &oob) + .expect("valid SdCreateRemoteBackup request should succeed"); + + assert_eq!(response.pok_remote_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!(response.pok_remote_backup.iter().any(|byte| *byte != 0)); + assert_eq!(response.pok_local_backup.len(), MASKED_SD_LEN); + assert!(response.pok_local_backup.iter().any(|byte| *byte != 0)); + assert_eq!(response.sd_mk_backup.len(), SD_MK_BACKUP_LEN); + assert!(response.sd_mk_backup.iter().any(|byte| *byte != 0)); +} + +fn assert_rejected(ctx: &TestCtx, fixture: &Fixture, expected: TborStatus) { + let oob: Vec<&[u8]> = fixture.oob_items.iter().map(Vec::as_slice).collect(); + ctx.expect_fw_reject_oob(&fixture.request, &oob, expected); +} + +fn flip_byte(bytes: &mut [u8], offset: u16, mask: u8) -> bool { + if mask == 0 { + return false; + } + if bytes.is_empty() { + return false; + } + let index = usize::from(offset) % bytes.len(); + bytes[index] ^= mask; + true +} + +fn run_case(ctx: &TestCtx, mutation: Mutation, key_scope: common::KeyScope) { + if !matches!( + key_scope, + common::KeyScope::Ephemeral | common::KeyScope::Local + ) { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + common::exercise_non_partition_key_scope(ctx, &session, key_scope); + ctx.session_close(session.session_id) + .expect("close key-scope CO session"); + return; + } + + let trusted_sa = matches!( + mutation, + Mutation::ValidTrustedSa + | Mutation::TamperTrustedReport { .. } + | Mutation::TrustedReceiverKeyMismatch + | Mutation::TrustedEvidenceLeafMismatch + ); + let bind_backing_partition = !matches!(mutation, Mutation::WrongBackingPartition); + let mut fixture = build_fixture( + ctx, + trusted_sa, + bind_backing_partition, + &mutation, + key_scope, + ); + + match mutation { + Mutation::Valid | Mutation::ValidTrustedSa => { + assert_success(ctx, &fixture); + } + Mutation::MissingOob => { + ctx.expect_fw_reject(&fixture.request, TborStatus::InvalidArg); + } + Mutation::EmptyReceiverChain => { + fixture.request.receiver_cert_chain.clear(); + assert_rejected(ctx, &fixture, TborStatus::InvalidArg); + } + Mutation::WrongSataAnchor | Mutation::WrongBackingPartition => { + assert_rejected(ctx, &fixture, TborStatus::InvalidArg); + } + Mutation::PolicyHashMismatch => { + fixture.request.policy.info[0] ^= 1; + let bound_policy = ::try_read_from_bytes(&fixture.policy) + .expect("bound policy image"); + assert_ne!( + fixture.request.policy.info, bound_policy.info, + "policy mutation must differ from the bound policy", + ); + assert_rejected(ctx, &fixture, TborStatus::InvalidArg); + } + Mutation::TamperReceiverCertificate { offset, mask } => { + let changed = flip_byte( + &mut fixture.oob_items[fixture.receiver_leaf_index], + offset, + mask, + ); + if changed { + let oob: Vec<&[u8]> = fixture.oob_items.iter().map(Vec::as_slice).collect(); + assert!( + ctx.tbor_oob(&fixture.request, &oob).is_err(), + "tampered receiver certificate must be rejected", + ); + } else { + assert_success(ctx, &fixture); + } + } + Mutation::CorruptMaskedSealingKey { offset, mask } => { + let byte = &mut fixture.request.masked_sealing_key + [usize::from(offset) % MASKED_SEALING_KEY_LEN]; + *byte ^= mask; + if mask == 0 { + assert_success(ctx, &fixture); + } else { + let oob: Vec<&[u8]> = fixture.oob_items.iter().map(Vec::as_slice).collect(); + assert!( + ctx.tbor_oob(&fixture.request, &oob).is_err(), + "corrupted masked sealing key must be rejected", + ); + } + } + Mutation::TamperTrustedReport { mask } => { + let index = fixture.report_index.expect("trusted fixture has report"); + let report_len = fixture.oob_items[index].len(); + let changed = flip_byte( + &mut fixture.oob_items[index], + u16::try_from(report_len - 1).expect("bounded report length"), + mask, + ); + if changed { + assert_rejected(ctx, &fixture, TborStatus::InvalidArg); + } else { + assert_success(ctx, &fixture); + } + } + Mutation::TrustedReceiverKeyMismatch | Mutation::TrustedEvidenceLeafMismatch => { + assert_rejected(ctx, &fixture, TborStatus::InvalidArg); + } + Mutation::OneShot => { + assert_success(ctx, &fixture); + let oob: Vec<&[u8]> = fixture.oob_items.iter().map(Vec::as_slice).collect(); + ctx.expect_fw_reject_oob(&fixture.request, &oob, TborStatus::SdAlreadyInitialized); + } + } + + ctx.session_close(fixture.session_id) + .expect("close fuzz CO session"); +} + +fuzz_target!(|input: FuzzInput| { + common::common_fuzz_test(&|ctx: &TestCtx, _path: &str| { + run_case(ctx, input.mutation, input.key_scope); + }); +}); diff --git a/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_reseal_remote_backup.rs b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_reseal_remote_backup.rs new file mode 100644 index 000000000..d0cd88705 --- /dev/null +++ b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_reseal_remote_backup.rs @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![no_main] + +#[path = "../../common.rs"] +mod common; + +use azihsm_ddi_tbor_test_harness::ROTATED_CO_PSK; +use azihsm_ddi_tbor_test_harness::TestCtx; +use azihsm_ddi_tbor_test_harness::bootstrap_rotated_co; +use azihsm_ddi_tbor_test_harness::x509_fixture::CaKey; +use azihsm_ddi_tbor_test_harness::x509_fixture::GeneratedChain; +use azihsm_ddi_tbor_test_harness::x509_fixture::RAW_PUB_LEN; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_pta_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::pta_pub_from_csr; +use azihsm_ddi_tbor_types::CertDescriptor; +use azihsm_ddi_tbor_types::KEY_REPORT_DATA_LEN; +use azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN; +use azihsm_ddi_tbor_types::POLICY_MAX_KEY_LEN; +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::PolicyKeyKind; +use azihsm_ddi_tbor_types::PolicyPubKey; +use azihsm_ddi_tbor_types::ReportDescriptor; +use azihsm_ddi_tbor_types::TborKeyReportReq; +use azihsm_ddi_tbor_types::TborPartInfoReq; +use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdResealRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::tbor_int::U16; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use zerocopy::IntoBytes; +use zerocopy::TryFromBytes; + +/// Every case starts with real keys, evidence, policy, and a source backup. +/// Mutations therefore exercise handler validation and crypto paths rather +/// than spending most fuzz iterations on undecodable request bytes. +#[derive(Arbitrary, Clone, Copy, Debug)] +enum Mutation { + Valid, + ResealTwice, + MissingOob, + EmptySourceChain, + EmptyDestinationChain, + PolicyHashMismatch, + SourceReportOutOfRange, + DestinationReportOutOfRange, + TamperSourceEvidence { mask: u8 }, + TamperDestinationEvidence { mask: u8 }, + WrongReceiverKey, + TamperSourceBackup { offset: u16, mask: u8 }, +} + +#[derive(Arbitrary, Debug)] +struct FuzzInput { + mutation: Mutation, + key_scope: common::KeyScope, +} + +impl Default for FuzzInput { + fn default() -> Self { + Self { + mutation: Mutation::Valid, + key_scope: common::KeyScope::Local, + } + } +} + +struct ResealEvidence { + oob_items: Vec>, + src_mfgr: Vec, + src_owner: Vec, + src_part_owner: Vec, + src_report: ReportDescriptor, + dest_mfgr: Vec, + dest_owner: Vec, + dest_part_owner: Vec, + dest_report: ReportDescriptor, + src_leaf_index: usize, + dest_leaf_index: usize, +} + +impl ResealEvidence { + fn oob(&self) -> Vec<&[u8]> { + self.oob_items.iter().map(Vec::as_slice).collect() + } +} + +struct Fixture { + session_id: u16, + request: TborSdResealRemoteBackupReq, + evidence: ResealEvidence, + src_remote_backup: [u8; POK_REMOTE_BACKUP_LEN], + wrong_receiver_key: Option<[u8; MASKED_SEALING_KEY_LEN]>, +} + +fn push_item(items: &mut Vec>, bytes: &[u8]) -> CertDescriptor { + let index = u8::try_from(items.len()).expect("fixture OOB item count is bounded"); + let length = u16::try_from(bytes.len()).expect("fixture evidence length fits U16"); + items.push(bytes.to_vec()); + CertDescriptor { + index, + length: U16::new(length), + } +} + +fn push_chain(items: &mut Vec>, chain: &GeneratedChain) -> Vec { + vec![ + push_item(items, &chain.root_der), + push_item(items, &chain.leaf_der), + ] +} + +fn push_evidence( + items: &mut Vec>, + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + report: &[u8], +) -> ( + Vec, + Vec, + Vec, + ReportDescriptor, + usize, +) { + let mfgr = push_chain(items, &make_chain(&CaKey::generate(), pid_pub)); + let owner = push_chain(items, &make_chain(&CaKey::generate(), pid_pub)); + let part_owner = push_chain(items, &make_chain(sata_key, pid_pub)); + let report_item = push_item(items, report); + let report = ReportDescriptor { + index: report_item.index, + length: report_item.length, + }; + ( + mfgr, + owner, + part_owner, + report, + usize::from(report_item.index), + ) +} + +fn build_reseal_evidence( + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + src_report: &[u8], + dest_report: &[u8], +) -> ResealEvidence { + let mut oob_items = Vec::new(); + let (src_mfgr, src_owner, src_part_owner, src_report, src_report_index) = + push_evidence(&mut oob_items, pid_pub, sata_key, src_report); + let src_leaf_index = usize::from(src_mfgr[1].index); + let (dest_mfgr, dest_owner, dest_part_owner, dest_report, dest_report_index) = + push_evidence(&mut oob_items, pid_pub, sata_key, dest_report); + let dest_leaf_index = usize::from(dest_mfgr[1].index); + + debug_assert_eq!(src_report_index, usize::from(src_report.index)); + debug_assert_eq!(dest_report_index, usize::from(dest_report.index)); + + ResealEvidence { + oob_items, + src_mfgr, + src_owner, + src_part_owner, + src_report, + dest_mfgr, + dest_owner, + dest_part_owner, + dest_report, + src_leaf_index, + dest_leaf_index, + } +} + +fn public_key_from_wire(pub_key: &[u8]) -> [u8; RAW_PUB_LEN] { + const COORD_LEN: usize = RAW_PUB_LEN / 2; + assert_eq!(pub_key.len(), RAW_PUB_LEN); + + let mut raw = [0u8; RAW_PUB_LEN]; + for (dst, src) in raw[..COORD_LEN] + .iter_mut() + .zip(pub_key[..COORD_LEN].iter().rev()) + { + *dst = *src; + } + for (dst, src) in raw[COORD_LEN..] + .iter_mut() + .zip(pub_key[COORD_LEN..].iter().rev()) + { + *dst = *src; + } + raw +} + +fn sealing_key_report_and_pub( + ctx: &TestCtx, + session_id: u16, + key_scope: common::KeyScope, +) -> ([u8; MASKED_SEALING_KEY_LEN], Vec, [u8; RAW_PUB_LEN]) { + let seal = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id, + scope: key_scope.to_tbor(), + }) + .expect("SdSealingKeyGen"); + let masked_key: [u8; MASKED_SEALING_KEY_LEN] = seal + .masked_key + .as_slice() + .try_into() + .expect("SdSealingKeyGen returns a fixed-length masked key"); + let pub_key = public_key_from_wire(&seal.pub_key); + let report = ctx + .tbor(&TborKeyReportReq { + session_id, + masked_key: masked_key.to_vec(), + report_data: [0u8; KEY_REPORT_DATA_LEN], + }) + .expect("KeyReport") + .report; + (masked_key, report, pub_key) +} + +fn build_policy_and_finalize( + ctx: &TestCtx, + session: &azihsm_ddi_tbor_test_harness::SessionHandshake, + sata_key: &CaKey, +) -> ([u8; PART_POLICY_LEN], [u8; RAW_PUB_LEN]) { + let part_info = ctx.tbor(&TborPartInfoReq::new()).expect("PartInfo"); + let pid_pub: [u8; RAW_PUB_LEN] = part_info + .pid_pub_key + .as_slice() + .try_into() + .expect("PartInfo public key is P-384"); + + let pota_key = CaKey::generate(); + let mut policy_bytes = common::known_good_part_policy(pota_key.raw_pub()); + let mut policy = + PartPolicy::try_read_from_bytes(&policy_bytes).expect("known-good policy must decode"); + policy.sata_pub_key = PolicyPubKey::new( + PolicyKeyKind::Ecc384, + POLICY_MAX_KEY_LEN as u16, + sata_key.raw_pub(), + ); + policy.backup_part_id.copy_from_slice(&part_info.pid); + policy.backup_part_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, POLICY_MAX_KEY_LEN as u16, pid_pub); + policy_bytes.copy_from_slice(policy.as_bytes()); + + let init = ctx + .part_init( + session, + &common::mach_seed(), + &policy_bytes, + &common::pota_thumbprint(), + ) + .expect("PartInit"); + let pta_chain = make_pta_chain(&pota_key, &pta_pub_from_csr(&init.pta_csr)); + ctx.part_final(session, &policy_bytes, &[], &pta_chain.der_items()) + .expect("PartFinal"); + + (policy_bytes, pid_pub) +} + +fn build_source_backup( + ctx: &TestCtx, + session_id: u16, + policy: &[u8; PART_POLICY_LEN], + pid_pub: &[u8; RAW_PUB_LEN], + sata_key: &CaKey, + masked_sender_key: &[u8; MASKED_SEALING_KEY_LEN], + receiver_pub: &[u8; RAW_PUB_LEN], + receiver_report: &[u8], +) -> [u8; POK_REMOTE_BACKUP_LEN] { + let mut oob_items = Vec::new(); + let receiver_chain = push_chain(&mut oob_items, &make_chain(sata_key, receiver_pub)); + let mfgr = push_chain(&mut oob_items, &make_chain(&CaKey::generate(), pid_pub)); + let owner = push_chain(&mut oob_items, &make_chain(&CaKey::generate(), pid_pub)); + let part_owner = push_chain(&mut oob_items, &make_chain(sata_key, pid_pub)); + let report_item = push_item(&mut oob_items, receiver_report); + let oob: Vec<&[u8]> = oob_items.iter().map(Vec::as_slice).collect(); + let policy = PartPolicy::try_read_from_bytes(policy).expect("policy image is canonical"); + + let request = TborSdCreateRemoteBackupReq { + session_id, + masked_sealing_key: *masked_sender_key, + receiver_cert_chain: receiver_chain, + receiver_mfgr_cert_chain: mfgr, + receiver_owner_cert_chain: owner, + receiver_part_owner_cert_chain: part_owner, + receiver_report: ReportDescriptor { + index: report_item.index, + length: report_item.length, + }, + policy, + }; + let response = ctx + .tbor_oob(&request, &oob) + .expect("create real source remote backup"); + response.pok_remote_backup +} + +fn build_fixture(ctx: &TestCtx, wrong_receiver_key: bool, key_scope: common::KeyScope) -> Fixture { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let sata_key = CaKey::generate(); + let (policy, pid_pub) = build_policy_and_finalize(ctx, &session, &sata_key); + + let (masked_receiver, receiver_report, receiver_pub) = + sealing_key_report_and_pub(ctx, session.session_id, key_scope); + let (masked_sender, sender_report, _) = + sealing_key_report_and_pub(ctx, session.session_id, key_scope); + let (_masked_destination, destination_report, _) = + sealing_key_report_and_pub(ctx, session.session_id, key_scope); + let wrong_receiver_key = wrong_receiver_key + .then(|| sealing_key_report_and_pub(ctx, session.session_id, key_scope).0); + + let src_remote_backup = build_source_backup( + ctx, + session.session_id, + &policy, + &pid_pub, + &sata_key, + &masked_sender, + &receiver_pub, + &receiver_report, + ); + + let evidence = build_reseal_evidence(&pid_pub, &sata_key, &sender_report, &destination_report); + let request = TborSdResealRemoteBackupReq { + session_id: session.session_id, + masked_sealing_key: masked_receiver, + policy: PartPolicy::try_read_from_bytes(&policy).expect("policy image is canonical"), + src_mfgr_cert_chain: evidence.src_mfgr.clone(), + src_owner_cert_chain: evidence.src_owner.clone(), + src_part_owner_cert_chain: evidence.src_part_owner.clone(), + src_report: evidence.src_report, + dest_mfgr_cert_chain: evidence.dest_mfgr.clone(), + dest_owner_cert_chain: evidence.dest_owner.clone(), + dest_part_owner_cert_chain: evidence.dest_part_owner.clone(), + dest_report: evidence.dest_report, + src_remote_backup, + }; + + Fixture { + session_id: session.session_id, + request, + evidence, + src_remote_backup, + wrong_receiver_key, + } +} + +fn assert_success(ctx: &TestCtx, fixture: &Fixture) -> [u8; POK_REMOTE_BACKUP_LEN] { + let oob = fixture.evidence.oob(); + let response = ctx + .tbor_oob(&fixture.request, &oob) + .expect("valid SdResealRemoteBackup fixture must succeed"); + assert_eq!(response.dst_remote_backup.len(), POK_REMOTE_BACKUP_LEN); + assert!( + response.dst_remote_backup.iter().any(|byte| *byte != 0), + "resealed remote backup must not be all zero", + ); + assert_ne!( + response.dst_remote_backup, fixture.src_remote_backup, + "resealing must produce a fresh HPKE encapsulation", + ); + response.dst_remote_backup +} + +fn assert_rejected(ctx: &TestCtx, fixture: &Fixture) { + let oob = fixture.evidence.oob(); + assert!( + ctx.tbor_oob(&fixture.request, &oob).is_err(), + "invalid SdResealRemoteBackup fixture must be rejected", + ); +} + +fn flip_last_byte(bytes: &mut [u8], mask: u8) -> bool { + if mask == 0 || bytes.is_empty() { + return false; + } + let last = bytes.len() - 1; + bytes[last] ^= mask; + true +} + +fn run_case(ctx: &TestCtx, mutation: Mutation, key_scope: common::KeyScope) { + if !matches!( + key_scope, + common::KeyScope::Ephemeral | common::KeyScope::Local + ) { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + common::exercise_non_partition_key_scope(ctx, &session, key_scope); + ctx.session_close(session.session_id) + .expect("close key-scope CO session"); + return; + } + + let needs_wrong_receiver = matches!(mutation, Mutation::WrongReceiverKey); + let mut fixture = build_fixture(ctx, needs_wrong_receiver, key_scope); + + match mutation { + Mutation::Valid => { + assert_success(ctx, &fixture); + } + Mutation::ResealTwice => { + let first = assert_success(ctx, &fixture); + let second = assert_success(ctx, &fixture); + assert_ne!( + first, second, + "repeated reseals must use fresh HPKE encapsulations", + ); + } + Mutation::MissingOob => { + ctx.expect_fw_reject(&fixture.request, TborStatus::InvalidArg); + } + Mutation::EmptySourceChain => { + fixture.request.src_mfgr_cert_chain.clear(); + ctx.expect_fw_reject_oob( + &fixture.request, + &fixture.evidence.oob(), + TborStatus::InvalidArg, + ); + } + Mutation::EmptyDestinationChain => { + fixture.request.dest_part_owner_cert_chain.clear(); + ctx.expect_fw_reject_oob( + &fixture.request, + &fixture.evidence.oob(), + TborStatus::InvalidArg, + ); + } + Mutation::PolicyHashMismatch => { + fixture.request.policy.info[0] ^= 1; + assert_rejected(ctx, &fixture); + } + Mutation::SourceReportOutOfRange => { + fixture.request.src_report.index = u8::MAX; + assert_rejected(ctx, &fixture); + } + Mutation::DestinationReportOutOfRange => { + fixture.request.dest_report.index = u8::MAX; + assert_rejected(ctx, &fixture); + } + Mutation::TamperSourceEvidence { mask } => { + let changed = flip_last_byte( + &mut fixture.evidence.oob_items[fixture.evidence.src_leaf_index], + mask, + ); + if changed { + assert_rejected(ctx, &fixture); + } else { + assert_success(ctx, &fixture); + } + } + Mutation::TamperDestinationEvidence { mask } => { + let changed = flip_last_byte( + &mut fixture.evidence.oob_items[fixture.evidence.dest_leaf_index], + mask, + ); + if changed { + assert_rejected(ctx, &fixture); + } else { + assert_success(ctx, &fixture); + } + } + Mutation::WrongReceiverKey => { + fixture.request.masked_sealing_key = fixture + .wrong_receiver_key + .expect("wrong receiver key generated for this case"); + assert_rejected(ctx, &fixture); + } + Mutation::TamperSourceBackup { offset, mask } => { + if mask == 0 { + assert_success(ctx, &fixture); + } else { + let index = usize::from(offset) % POK_REMOTE_BACKUP_LEN; + fixture.request.src_remote_backup[index] ^= mask; + assert_rejected(ctx, &fixture); + } + } + } + + ctx.session_close(fixture.session_id) + .expect("close fuzz CO session"); +} + +fuzz_target!(|input: FuzzInput| { + common::common_fuzz_test(&|ctx: &TestCtx, _path: &str| { + run_case(ctx, input.mutation, input.key_scope); + }); +}); diff --git a/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_restore_local_backup.rs b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_restore_local_backup.rs new file mode 100644 index 000000000..adc5047db --- /dev/null +++ b/fuzz/fuzz_targets/ddi/tbor/fuzz_tbor_sd_restore_local_backup.rs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![no_main] + +#[path = "../../common.rs"] +mod common; + +use azihsm_ddi_tbor_test_harness::ROTATED_CO_PSK; +use azihsm_ddi_tbor_test_harness::TestCtx; +use azihsm_ddi_tbor_test_harness::bootstrap_rotated_co; +use azihsm_ddi_tbor_test_harness::x509_fixture::CaKey; +use azihsm_ddi_tbor_test_harness::x509_fixture::GeneratedChain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::make_pta_chain; +use azihsm_ddi_tbor_test_harness::x509_fixture::pta_pub_from_csr; +use azihsm_ddi_tbor_types::CertDescriptor; +use azihsm_ddi_tbor_types::MASKED_SD_LEN; +use azihsm_ddi_tbor_types::PART_POLICY_LEN; +use azihsm_ddi_tbor_types::POLICY_MAX_KEY_LEN; +use azihsm_ddi_tbor_types::PartPolicy; +use azihsm_ddi_tbor_types::PolicyKeyKind; +use azihsm_ddi_tbor_types::PolicyPubKey; +use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN; +use azihsm_ddi_tbor_types::TborPartInfoReq; +use azihsm_ddi_tbor_types::TborSdCreateRemoteBackupReq; +use azihsm_ddi_tbor_types::TborSdRestoreLocalBackupReq; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::tbor_int::U16; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use zerocopy::IntoBytes; +use zerocopy::TryFromBytes; + +const ENVELOPE_CIPHERTEXT_OFFSET: usize = 8 + 12 + 192; + +#[derive(Arbitrary, Clone, Copy, Debug)] +enum Mutation { + Valid, + TamperPok { offset: u16, mask: u8 }, + TamperSdMk { offset: u16, mask: u8 }, + OneShot, + NotFinalized, +} + +#[derive(Arbitrary, Clone, Copy, Debug)] +struct FuzzInput { + mutation: Mutation, + key_scope: common::KeyScope, +} + +impl Default for FuzzInput { + fn default() -> Self { + Self { + mutation: Mutation::Valid, + key_scope: common::KeyScope::Local, + } + } +} + +struct SourceBackups { + session_id: u16, + policy: [u8; PART_POLICY_LEN], + local_mk_backup: Vec, + pok_local_backup: Vec, + sd_mk_backup: Vec, + pota: CaKey, +} + +fn push_oob_item(items: &mut Vec>, der: &[u8]) -> CertDescriptor { + let index = u8::try_from(items.len()).expect("bounded OOB item count"); + let length = u16::try_from(der.len()).expect("bounded fixture DER length"); + items.push(der.to_vec()); + CertDescriptor { + index, + length: U16::new(length), + } +} + +fn push_chain(items: &mut Vec>, chain: GeneratedChain) -> Vec { + vec![ + push_oob_item(items, &chain.root_der), + push_oob_item(items, &chain.leaf_der), + ] +} + +fn raw_pub_from_wire(pub_key: &[u8]) -> [u8; POLICY_MAX_KEY_LEN] { + const COORD_LEN: usize = POLICY_MAX_KEY_LEN / 2; + assert_eq!(pub_key.len(), POLICY_MAX_KEY_LEN); + + let mut raw = [0u8; POLICY_MAX_KEY_LEN]; + for (dst, src) in raw[..COORD_LEN] + .iter_mut() + .zip(pub_key[..COORD_LEN].iter().rev()) + { + *dst = *src; + } + for (dst, src) in raw[COORD_LEN..] + .iter_mut() + .zip(pub_key[COORD_LEN..].iter().rev()) + { + *dst = *src; + } + raw +} + +fn build_source_backups(ctx: &TestCtx, key_scope: common::KeyScope) -> SourceBackups { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let part_info = ctx.tbor(&TborPartInfoReq::new()).expect("PartInfo"); + + let pota = CaKey::generate(); + let sata = CaKey::generate(); + let mut policy = common::known_good_part_policy(pota.raw_pub()); + let mut policy_value = + ::try_read_from_bytes(&policy).expect("known policy"); + policy_value.sata_pub_key = PolicyPubKey::new( + PolicyKeyKind::Ecc384, + POLICY_MAX_KEY_LEN as u16, + sata.raw_pub(), + ); + policy_value.backup_part_id.copy_from_slice(&part_info.pid); + let pid_pub: [u8; POLICY_MAX_KEY_LEN] = part_info + .pid_pub_key + .as_slice() + .try_into() + .expect("PartInfo public key is P-384"); + policy_value.backup_part_pub_key = + PolicyPubKey::new(PolicyKeyKind::Ecc384, POLICY_MAX_KEY_LEN as u16, pid_pub); + policy.copy_from_slice(policy_value.as_bytes()); + + let init = ctx + .part_init( + &session, + &common::mach_seed(), + &policy, + &common::pota_thumbprint(), + ) + .expect("PartInit"); + let pta_chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + let local_mk_backup = ctx + .part_final(&session, &policy, &[], &pta_chain.der_items()) + .expect("PartFinal") + .local_mk_backup + .to_vec(); + + let sealing_key = ctx + .tbor(&TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: key_scope.to_tbor(), + }) + .expect("SdSealingKeyGen"); + let receiver_pub = raw_pub_from_wire(&sealing_key.pub_key); + let receiver_chain = make_chain(&sata, &receiver_pub); + let mut oob_items = Vec::new(); + let receiver_cert_chain = push_chain(&mut oob_items, receiver_chain); + let policy_value = + ::try_read_from_bytes(&policy).expect("policy image"); + let request = TborSdCreateRemoteBackupReq { + session_id: session.session_id, + masked_sealing_key: sealing_key.masked_key, + receiver_cert_chain, + receiver_mfgr_cert_chain: Vec::new(), + receiver_owner_cert_chain: Vec::new(), + receiver_part_owner_cert_chain: Vec::new(), + receiver_report: Default::default(), + policy: policy_value, + }; + let oob: Vec<&[u8]> = oob_items.iter().map(Vec::as_slice).collect(); + let response = ctx + .tbor_oob(&request, &oob) + .expect("valid SdCreateRemoteBackup fixture"); + + SourceBackups { + session_id: session.session_id, + policy, + local_mk_backup, + pok_local_backup: response.pok_local_backup.to_vec(), + sd_mk_backup: response.sd_mk_backup.to_vec(), + pota, + } +} + +fn flip_ciphertext_or_tag_byte(bytes: &mut [u8], offset: u16, mask: u8) -> bool { + if mask == 0 || bytes.len() <= ENVELOPE_CIPHERTEXT_OFFSET { + return false; + } + + let body_len = bytes.len() - ENVELOPE_CIPHERTEXT_OFFSET; + let index = ENVELOPE_CIPHERTEXT_OFFSET + usize::from(offset) % body_len; + bytes[index] ^= mask; + true +} + +fn restore_after_reboot(ctx: &TestCtx, backups: &SourceBackups, mutation: Mutation) { + ctx.session_close(backups.session_id) + .expect("close source CO session"); + ctx.erase().expect("factory-reset before restore"); + + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + let init = ctx + .part_init( + &session, + &common::mach_seed(), + &backups.policy, + &common::pota_thumbprint(), + ) + .expect("PartInit after reboot"); + let pta_chain = make_pta_chain(&backups.pota, &pta_pub_from_csr(&init.pta_csr)); + ctx.part_final( + &session, + &backups.policy, + &backups.local_mk_backup, + &pta_chain.der_items(), + ) + .expect("PartFinal restores PartLocalMK"); + + let mut pok_local_backup = backups.pok_local_backup.clone(); + let mut sd_mk_backup = backups.sd_mk_backup.clone(); + let expected_rejection = match mutation { + Mutation::TamperPok { offset, mask } => { + if flip_ciphertext_or_tag_byte(&mut pok_local_backup, offset, mask) { + Some(TborStatus::AesGcmDecryptTagDoesNotMatch) + } else { + None + } + } + Mutation::TamperSdMk { offset, mask } => { + if flip_ciphertext_or_tag_byte(&mut sd_mk_backup, offset, mask) { + Some(TborStatus::AesGcmDecryptTagDoesNotMatch) + } else { + None + } + } + Mutation::Valid => None, + Mutation::OneShot | Mutation::NotFinalized => { + unreachable!("these scenarios do not use the rebooted fixture") + } + }; + + let request = TborSdRestoreLocalBackupReq { + session_id: session.session_id, + pok_local_backup, + sd_mk_backup, + }; + if let Some(status) = expected_rejection { + ctx.expect_fw_reject(&request, status); + } else { + let response = ctx + .tbor(&request) + .expect("untampered or no-op-mutated restore should succeed"); + assert_eq!(response.pok_local_backup.len(), MASKED_SD_LEN); + assert!( + response.pok_local_backup.iter().any(|byte| *byte != 0), + "refreshed POK backup must not be all zero", + ); + assert_eq!(response.sd_mk_backup.len(), SD_MK_BACKUP_LEN); + assert!( + response.sd_mk_backup.iter().any(|byte| *byte != 0), + "refreshed SD-MK backup must not be all zero", + ); + ctx.expect_fw_reject(&request, TborStatus::SdAlreadyInitialized); + } + + ctx.session_close(session.session_id) + .expect("close restore CO session"); +} + +fn run_case(ctx: &TestCtx, mutation: Mutation, key_scope: common::KeyScope) { + if !matches!( + key_scope, + common::KeyScope::Ephemeral | common::KeyScope::Local + ) { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + common::exercise_non_partition_key_scope(ctx, &session, key_scope); + ctx.session_close(session.session_id) + .expect("close key-scope CO session"); + return; + } + + match mutation { + Mutation::NotFinalized => { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + ctx.expect_fw_reject( + &TborSdRestoreLocalBackupReq { + session_id: session.session_id, + pok_local_backup: vec![0u8; MASKED_SD_LEN], + sd_mk_backup: vec![0u8; SD_MK_BACKUP_LEN], + }, + TborStatus::InvalidArg, + ); + ctx.session_close(session.session_id) + .expect("close not-finalized CO session"); + } + Mutation::OneShot => { + let backups = build_source_backups(ctx, key_scope); + ctx.expect_fw_reject( + &TborSdRestoreLocalBackupReq { + session_id: backups.session_id, + pok_local_backup: backups.pok_local_backup, + sd_mk_backup: backups.sd_mk_backup, + }, + TborStatus::SdAlreadyInitialized, + ); + ctx.session_close(backups.session_id) + .expect("close one-shot CO session"); + } + Mutation::Valid | Mutation::TamperPok { .. } | Mutation::TamperSdMk { .. } => { + let backups = build_source_backups(ctx, key_scope); + restore_after_reboot(ctx, &backups, mutation); + } + } +} + +fuzz_target!(|data: &[u8]| { + let mut unstructured = arbitrary::Unstructured::new(data); + let input = FuzzInput::arbitrary(&mut unstructured).unwrap_or_default(); + + common::common_fuzz_test(&|ctx: &TestCtx, _path: &str| { + run_case(ctx, input.mutation, input.key_scope); + }); +});