Repository navigation
Harden dev mode value #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mcxa-secure-boot
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,6 @@ use embassy_mcxa::rom::NbootBoolValue; | |
| use embassy_sync::blocking_mutex::raw::NoopRawMutex; | ||
| use embedded_storage_async::nor_flash::{ErrorType, NorFlash, NorFlashErrorKind, ReadNorFlash}; | ||
| use heapless::Vec; | ||
| use mcxa_ifr::{LifeCycleState, SecBootEn}; | ||
| use mcxa_security_provisioning::Provisioner; | ||
| use partition_manager::{Partition, PartitionManager, RO, RW}; | ||
| use static_cell::StaticCell; | ||
|
|
@@ -638,8 +637,7 @@ impl<C: McxaConfig + BootStatePolicy> Board for Mcxa<C> { | |
| } | ||
|
|
||
| let Err(if_result) = protected_if::<{ NbootBoolValue::True as u32 }, _, _, _>( | ||
| || match verification::verify_authenticity(self.sgi.reborrow(), image_base, || Self::dev_mode() as u32) | ||
| { | ||
| || match verification::verify_authenticity(self.sgi.reborrow(), image_base, || Self::dev_mode()) { | ||
| Ok(result) => result as u32, | ||
| Err(error) => error as u32, // Error has no overlap in value with Nboot bool, so this is safe to do | ||
| }, | ||
|
|
@@ -666,17 +664,37 @@ impl<C: McxaConfig + BootStatePolicy> Mcxa<C> { | |
| } | ||
|
|
||
| /// Check if the device authenticity should be checked in dev mode (where the keys are self-generated instead of taken from the programmed value) | ||
| fn dev_mode() -> bool { | ||
| /// Dev mode is active when the returned value is [DEV_VAL] | ||
| fn dev_mode() -> u32 { | ||
| let ifr = unsafe { mcxa_ifr::IFR::current() }; | ||
| let secure_boot_enabled = ifr.cmpa.secure_boot_cfg.sec_boot_en() != SecBootEn::OnlyPki; | ||
| let develop_lifecycle = ifr.cfpa.header.cfpa_lc_state() == Ok(LifeCycleState::Develop); | ||
| // Don't enable dev mode if secure boot is active. That way a flashed key can be tested | ||
| develop_lifecycle && !secure_boot_enabled | ||
|
|
||
| let secure_boot_cfg_ptr = &raw const ifr.cmpa.secure_boot_cfg; | ||
|
|
||
| // Read secure boot twice to harden against fault injection where they saturate a register | ||
| let secure_boot1 = unsafe { secure_boot_cfg_ptr.read_volatile() } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What fault model are we protecting against here? The two volatile reads will remain separate accesses, but what are we assuming about the duration and scope of the fault you're protecting against? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All zero attractors on the bus/register would mimic an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All one faults would pass, though. No?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's why it's read twice since supposedly the hardware would catch it if the injection was strong enough to do it twice in a row There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, they would "pass". But all ones would trigger SEC_BOOT_EN as OnlyPki, at which point DEV mode is NOT enabled, which means no root of trust bypass, which means an attacker trust chain cannot authenticate the image. ROM only trusts what is in IFR. So that attack fails safe to exactly what we need. The whole point of the glitch hardening here is NOT to let an attacker glitch into DEV mode and have their RoT take over.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify my earlier “all ones” question: I meant saturation of the secure-boot samples while the lifecycle bytes remain unaffected, not saturation of the entire packed word. With the policy corrected, all-ones samples should prevent dev mode. My remaining question is about common-mode faults: two reads can agree on the same incorrect value. The RM documents ROM-configured GDET/aGDET protection, but §63.1 also requires system characterization, and §63.1.3 requires reconfiguration when core voltage changes. That does not establish that every fault affecting both reads will be detected. Can we just document the assumed fault scope/duration and detector configuration? I’m asking for a bounded hardening claim, not immunity to every injection: redundant sampling helps against faults confined to one sample; coverage of faults affecting both needs separate justification. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IIRC boot is configured on Normal mode (from SYSCON), so GDET should be on VDD_CORE normal mode too. The SBL will operate as such (please ignore the current board.rs config, it's too fast for flexSPI anyway and I have PR coming changing all that back to Normal). But I have asked NXP to clarify and then we can document the boundaries.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, I think that should be a good addition here. |
||
| .sec_boot_en() | ||
| .raw_value() | ||
| .value() as u32; | ||
| let secure_boot2 = unsafe { secure_boot_cfg_ptr.read_volatile() } | ||
| .sec_boot_en() | ||
| .raw_value() | ||
| .value() as u32; | ||
|
|
||
| let lifecycle = match ifr.cfpa.header.cfpa_lc_state() { | ||
| Ok(val) => val.raw_value(), | ||
| Err(e) => e, | ||
| } as u32; | ||
| let inv_lifecycle = match ifr.cfpa.header.inv_cfpa_lc_state() { | ||
| Ok(val) => val.raw_value(), | ||
| Err(e) => e, | ||
| } as u32; | ||
|
|
||
| (secure_boot1 << 24) | (secure_boot2 << 16) | (lifecycle << 8) | (inv_lifecycle) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we split the reads from the decoding/packing so we can actually test this without dereferencing IFR? At least cover the intended dev configurations, Develop + The tests need to exercise the helper used here, not a separate copy of the packing expression. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a question that sort of circles back to the double read in my original PR: read twice via volatile then compare and if mismatch, return Err. The concern was although
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the compiler trully follows its own documentation and that of LLVM, I don't think we can get a situation where the compiler reuses the first value for the second. We will definitely get two reads and two fresh values. @diondokter? |
||
| } | ||
|
|
||
| /// Check if the secure boot settings are good | ||
| fn follows_policy() -> bool { | ||
| if Self::dev_mode() { | ||
| if Self::dev_mode() == DEV_VAL { | ||
| true | ||
| } else { | ||
| unsafe { Provisioner::new_from_page(true) } | ||
|
|
@@ -686,6 +704,12 @@ impl<C: McxaConfig + BootStatePolicy> Mcxa<C> { | |
| } | ||
| } | ||
|
|
||
| pub const DEV_VAL: u32 = 0b00000011_00000011_00000011_11111100; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't this still the opposite of what #54 fixes? Both secure-boot bytes here are Also, #54 allows every value except There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@felipebalbi is correct here (thanks!!), it reverses the fix from PR 54. We want DEV mode when NOT secure boot on with OnlyPki, the hybrid SB mode.
We need "NOT |
||
| // SecSecBootEn::OnlyPki ------- ^^ | ||
| // SecSecBootEn::OnlyPki ---------------- ^^ | ||
| // LifeCycleState::Develop ----------------- ^^^^^^^^ | ||
| // InvLifeCycleState::Develop ----------------------- ^^^^^^^^ | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ use fault_rejection::protected_if; | |
| use mcxa_ifr::{IFR, RotkEn, RotkUsageVal}; | ||
| use mcxa_security_provisioning::certificate::derive_image_rkth_pair; | ||
|
|
||
| use crate::board::DEV_VAL; | ||
| use crate::error::map_nboot_status_to_boot_error; | ||
|
|
||
| fn load_nboot_auth_parms_from_ifr(ifr: &IFR) -> Result<NbootImgAuthParms, BootError> { | ||
|
|
@@ -83,7 +84,7 @@ pub fn verify_authenticity<'d>( | |
| defmt_or_log::trace!("Initializing NBOOT context"); | ||
| let mut n_boot_api = embassy_mcxa::rom::get().nboot().map_err(|_| BootError::Authenticate)?; | ||
|
|
||
| let result = protected_if::<{ true as u32 }, Result<(), BootError>, _, _>(dev_mode, || { | ||
| let result = protected_if::<DEV_VAL, Result<(), BootError>, _, _>(dev_mode, || { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also changes the meaning of the public callback without changing its type. A caller returning We can still encode the observed fields into a word at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DEV mode is a very specific scenario that MSFT needs, a normal bootloader ideally should not bypass its secure roots of trust. So, passing a 1 which compiles and bypasses DEV mode here would be international I believe, as you'd want to NOT use DEV mode. Only the select pattern lets you though, all else verifies against the devices fused root of trust. |
||
| // In dev mode, we're going to lie to nboot and derive the rkth's ourselves so they're always correct | ||
| // This makes the provisioning process easier. Once no longer in dev mode, this branch is not taken and | ||
| // only the flashed keys are used | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like returning a raw
u32here. Can we have aModestruct withPartialEq, Eq, named secure-boot and lifecycle fields, and constants built from the existing enums instead?Something like below ought to do it? Maybe add a
to_raw() -> u32method to it for the boundary?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, you would return a raw constant here, a specific made-up pattern that meets certain requirements e.g. the likes of NbootBool values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A specific fault-resistant pattern makes sense, I agree. I’m not proposing replacing it with an ordinary Boolean. My concern is preserving the contract for future maintainers: a typed wrapper with private construction can enforce sample agreement and lifecycle/inverse consistency, while retaining the observed encoding for protected_if.
My original struct sketch does not enforce those relationships by itself, it needs a little more work :-). The goal is to validate at the boundary and preserve those invariants downstream, rather than make every caller understand an unrestricted u32. The exact representation can be designed in parallel, but the point remains: I would like the invariants to preserved at a constructor that refuses invalid representation centrally. Downstream users use the validated, readable representation only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, I see what you mean. Agreed.