diff --git a/README.md b/README.md index 78ac862d..d237e8a6 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ A light-weight bootloader written in Rust with a fail-safe NOR-flash backed stat This framework can run on any platform if support for the platform is implemented. It is only opinionated with regards to how the state is stored. -Currently only supports the NXP IMXRT685S and IMXRT633S where it acts as a stage-two bootloader and copies the program to application RAM. -Also contains a tool for signing images, flashing them to the device, setting fuses (or shadow registers) containing crypto keys, -and an example application to showcase the bootloaders A/B state functionality for this family of chipsets. - ## Organisation This repository is split up into three parts: @@ -16,28 +12,28 @@ This repository is split up into three parts: * bootloader-tool: a command-line utility only used to perform operations related to the NXP RT685S platform. It uses the NXP SPSDK tooling to generate keys, sign images, and flash them to the target device. Also integrates probe-rs and allows for attaching to the RTT buffer for displaying `defmt` output. This tool is not relevant if you want to use `ec-slimloader` with any other platform. +* examples/mcxa-577app: example bootloader and blinky demo application, can be run on MCXA5xx evalutation kit. The libraries are split out as follows: * ec-slimloader: general library crate providing a basic structure to build your bootloader binary application. * ec-slimloader-state: library crate with all code relating to managing the state journal. Used by both the bootloader and the application to change which image slot should be booted. -* ec-slimloader-imxrt: library crate implementing support for the NXP IMXRT685S and IMXRT633S. -* imxrt-rom: library crate implementing Rust support for the NXP ROM API which provides access to fuses and allows calling into a verification routine for images. +* ec-slimloader-mcxa: library crate implementing support for the NXP MCXA5xx family with PQC cryptography support. ## How it works Assuming your platform is already supported, you can define: * a region of NOR-flash memory containing at least 2 pages for the bootloader state. * at least two regions of any memory that will fit an application image. -Using the library crate for your platform (like `ec-slimloader-imxrt`) you can then implement your own bootloader binary by calling the `start` function in the `ec-slimloader` library crate. +Using the library crate for your platform (e.g., `ec-slimloader-mcxa`) you can implement your own bootloader binary by calling the `start` function in the `ec-slimloader` library crate. The `ec-slimloader` crate will handle for you: * it will read from the state journal what image slot will be booted. * on subsequent reboots, it will fall back to your defined backup slot if you do not mark your current application image as `confirmed`. However, some aspects are handled by the platform support crate (and can differ from project-to-project): -* how application images are loaded. For `ec-slimloader-imxrt` images are copied to RAM in a quite chip-specific way. Typically for other platforms you might want to swap images between on-chip NOR flash and external NOR flash. The latter method is not implemented in this repository (yet). -* how application images are verified. By default the images themselves are not checked at all. `ec-slimloader-imxrt` leverages the native NXP authentication routines to check image integrity. -* how application images are bootloaded, or in other words are jumped to. This differs for cortex-m or RISCV processors. +* how application images are loaded. +* how application images are verified. +* how application images are bootloaded, or in other words are jumped to. Even when using `ec-slimloader-imxrt`, you will still have to implement a few details: * from what memory is the `ec-slimloader` started, and what memory range is used for the bootloader data? @@ -49,7 +45,7 @@ Finally, your application needs to also work with the state journal to: * after rebooting, mark the current image slot from which the application is running as `confirmed`. If the application does not do this, the bootloader will load the old 'backup' image and mark the current boot as `failed`. -For a full tour on how to use this framework, please refer to the `examples/rt685s` folder. +For a full tour on how to use this framework for MCXA5xx, refer to the MCX examples. ## Quick guide This guide details how to use this repository on the NXP MIMXRT685S-EVK. First step is compiling the bootloader and application: @@ -123,3 +119,6 @@ cargo run -- run application -i ../examples/rt685s/target/thumbv8m.main-none-eab You can use the `USER_1` button to change the state journal to either `confirmed` or try the other slot in state `initial` if the current image is already `confirmed`. You can use the `USER_2` button the reboot into the bootloader, which will set an image to `failed` if it does not verify or if it was in `attempting` without putting the state in `confirmed`. + +The following describes the process for generating artifacts and signing/flashing for MCXA: +... \ No newline at end of file diff --git a/examples/mcxa-577app/app/.cargo/config.toml b/examples/mcxa-577app/app/.cargo/config.toml new file mode 100644 index 00000000..d57a9122 --- /dev/null +++ b/examples/mcxa-577app/app/.cargo/config.toml @@ -0,0 +1,14 @@ +[build] +target = "thumbv8m.main-none-eabihf" + +[target.thumbv8m.main-none-eabihf] +runner = "echo" + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +rustflags = [ + "-C", "linker=flip-link", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "link-arg=--nmagic", + "-C", "force-frame-pointers=yes", +] diff --git a/examples/mcxa-577app/app/Cargo.toml b/examples/mcxa-577app/app/Cargo.toml new file mode 100644 index 00000000..20c6edef --- /dev/null +++ b/examples/mcxa-577app/app/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mcxa-577app" +version = "0.1.0" +edition = "2021" + +[dependencies] +cortex-m = { version = "0.7", features = ["critical-section-single-core"] } +cortex-m-rt = { version = "0.7", features = ["set-sp", "set-vtor"] } +defmt = "1.0" +defmt-rtt = "1.0" +embassy-mcxa = { git = "https://github.com/embassy-rs/embassy", default-features = false, features = ["rt", "defmt", "mcxa5xx"] } +embassy-executor = { git = "https://github.com/embassy-rs/embassy", default-features = false, features = ["platform-cortex-m", "executor-thread"] } +embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime"] } +panic-probe = { version = "1.0", features = ["print-defmt"] } + +[profile.dev] +panic = "abort" + +[profile.release] +lto = false # Disable LTO to prevent stripping debug patterns +opt-level = 2 # Standard optimization + + diff --git a/examples/mcxa-577app/app/build.rs b/examples/mcxa-577app/app/build.rs new file mode 100644 index 00000000..555cdf68 --- /dev/null +++ b/examples/mcxa-577app/app/build.rs @@ -0,0 +1,21 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/examples/mcxa-577app/app/memory.x b/examples/mcxa-577app/app/memory.x new file mode 100644 index 00000000..6926f2b9 --- /dev/null +++ b/examples/mcxa-577app/app/memory.x @@ -0,0 +1,11 @@ +MEMORY +{ + /* MCXA577 app memory map */ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + /* Bootloader uses 0x0000_0000..0x0000_FFFF (64KiB). App starts at slot_a = 0x0001_0000. */ + FLASH (rx) : ORIGIN = 0x00010000, LENGTH = 0x001F0000 + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K +} + +/* Stack grows down from end of RAM */ +_stack_start = ORIGIN(RAM) + LENGTH(RAM); diff --git a/examples/mcxa-577app/app/src/main.rs b/examples/mcxa-577app/app/src/main.rs new file mode 100644 index 00000000..0795c754 --- /dev/null +++ b/examples/mcxa-577app/app/src/main.rs @@ -0,0 +1,67 @@ +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_time::Timer; +use hal::bind_interrupts; +use hal::dma::DmaChannel; +use hal::gpio::{DriveStrength, Level, Output, SlewRate}; +use hal::peripherals::SGI0; +use hal::sgi::hash::{DmaHasher, HashSize}; +use hal::sgi::{InterruptHandler, Sgi}; +use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; + +bind_interrupts!(struct Irqs { + SGI => InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut p = hal::init(hal::config::Config::default()); + + defmt::info!("Blinky example with a sprinkle of SGI hashing"); + + let mut dma_ch0 = DmaChannel::new(p.DMA0_CH0.reborrow()); + let mut hash_result = [0u8; 48]; + let mut input_data = [0u8; 256]; + + for (index, byte) in input_data.iter_mut().enumerate() { + *byte = index as u8; + } + + let sgi = Sgi::new(p.SGI0.reborrow(), Irqs).unwrap(); + match DmaHasher::start_and_finalize(sgi, &mut dma_ch0, HashSize::Sha384, &input_data, &mut hash_result) + .await + { + Ok(()) => defmt::info!("DMA hash: {=[u8]:x}", &hash_result), + Err(e) => defmt::error!("DMA hash failed: {:?}", defmt::Debug2Format(&e)), + } + + let mut red = Output::new(p.P2_14, Level::High, DriveStrength::Normal, SlewRate::Fast); + let mut green = Output::new(p.P2_22, Level::High, DriveStrength::Normal, SlewRate::Fast); + let mut blue = Output::new(p.P2_23, Level::High, DriveStrength::Normal, SlewRate::Fast); + + let mut rate = 250; + + defmt::info!("It's showtime..."); + + loop { + if rate > 1000 { + rate = 250; // wrap rate to avoid overflow and excessively long timers. + } + red.toggle(); + Timer::after_millis(rate).await; + + red.toggle(); + green.toggle(); + Timer::after_millis(rate).await; + + green.toggle(); + blue.toggle(); + Timer::after_millis(rate).await; + blue.toggle(); + + Timer::after_millis(rate).await; + rate = rate.wrapping_add(100); + } +} diff --git a/examples/mcxa-577app/bootloader/.cargo/config.toml b/examples/mcxa-577app/bootloader/.cargo/config.toml new file mode 100644 index 00000000..87b247bf --- /dev/null +++ b/examples/mcxa-577app/bootloader/.cargo/config.toml @@ -0,0 +1,17 @@ +[build] +target = "thumbv8m.main-none-eabihf" + +[target.thumbv8m.main-none-eabihf] +runner = "probe-rs run --chip MCXA577 --chip-description-path MCXA_custom.yaml" + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +rustflags = [ + "-C", "linker=flip-link", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", + "-C", "link-arg=--nmagic", + "-C", "force-frame-pointers=yes", +] + +[env] +DEFMT_LOG = "trace" diff --git a/examples/mcxa-577app/bootloader/Cargo.toml b/examples/mcxa-577app/bootloader/Cargo.toml new file mode 100644 index 00000000..3274ef98 --- /dev/null +++ b/examples/mcxa-577app/bootloader/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "mcxa-577app-bootloader" +version = "0.1.0" +edition = "2021" + +[features] +default = ["defmt"] +defmt = ["dep:defmt", "dep:defmt-or-log", "dep:defmt-rtt", "defmt-or-log/defmt"] +log = ["dep:defmt-or-log", "defmt-or-log/log"] + +[[bin]] +name = "mcxa-577app-bootloader" +path = "src/main.rs" + +[dependencies] +cortex-m = { version = "0.7", features = ["critical-section-single-core"], default-features = false } +cortex-m-rt = "0.7" +panic-halt = "0.2" +defmt = { version = "1.0", optional = true } +defmt-or-log = { version = "0.2.3", optional = true } +defmt-rtt = { version = "1.0", optional = true } + +ec-slimloader-mcxa = { path = "../../../libs/ec-slimloader-mcxa", default-features = false, features = ["internal-only", "mcxa5xx", "defmt"] } +ec-slimloader = { path = "../../../libs/ec-slimloader", default-features = false } + +embassy-executor = { git = "https://github.com/embassy-rs/embassy", default-features = false, features = ["platform-cortex-m", "executor-thread"] } + +[patch.crates-io] +embassy-time = { git = "https://github.com/embassy-rs/embassy" } +embassy-time-driver = { git = "https://github.com/embassy-rs/embassy" } +embassy-time-queue-utils = { git = "https://github.com/embassy-rs/embassy" } + +[profile.release] +debug = 2 # Full DWARF info for defmt/probe-rs decoding +lto = false # Disable LTO to prevent stripping debug patterns +opt-level = 2 # Standard optimization +panic = "abort" # Smaller binaries diff --git a/examples/mcxa-577app/bootloader/build.rs b/examples/mcxa-577app/bootloader/build.rs new file mode 100644 index 00000000..124b7d9c --- /dev/null +++ b/examples/mcxa-577app/bootloader/build.rs @@ -0,0 +1,18 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // Only re-run if memory.x changes. + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/examples/mcxa-577app/bootloader/memory.x b/examples/mcxa-577app/bootloader/memory.x new file mode 100644 index 00000000..70c4aaed --- /dev/null +++ b/examples/mcxa-577app/bootloader/memory.x @@ -0,0 +1,15 @@ +MEMORY +{ + /* MCXA577 has 2MB flash total. + * This example reserves the first 64KB for the bootloader. + * Secure alias: 0x1000_0000 (Matrix0 Target Port0, Secure, All Initiators). + */ + FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K + + /* Secure SRAM alias: 0x3000_0000 (Matrix0 Target Port 4, Secure, All Initiators). + */ + RAM (rwx) : ORIGIN = 0x30000000, LENGTH = 64K +} + +/* Stack grows down from end of RAM */ +_stack_start = ORIGIN(RAM) + LENGTH(RAM); diff --git a/examples/mcxa-577app/bootloader/src/main.rs b/examples/mcxa-577app/bootloader/src/main.rs new file mode 100644 index 00000000..e0de1177 --- /dev/null +++ b/examples/mcxa-577app/bootloader/src/main.rs @@ -0,0 +1,21 @@ +#![no_std] +#![no_main] + +#[cfg(any(feature = "defmt", feature = "log"))] +use defmt_or_log::info; +#[cfg(feature = "defmt")] +use defmt_rtt as _; +use embassy_executor::Spawner; +use panic_halt as _; + +const JOURNAL_BUFFER_SIZE: usize = 4096; + +#[cfg(feature = "defmt")] +defmt::timestamp!("{=u32}", 0); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) -> ! { + #[cfg(any(feature = "defmt", feature = "log"))] + info!("Starting MCXA bootloader"); + ec_slimloader::start::(ec_slimloader_mcxa::Config).await +} diff --git a/libs/Cargo.toml b/libs/Cargo.toml index 350df9de..038cc93d 100644 --- a/libs/Cargo.toml +++ b/libs/Cargo.toml @@ -4,6 +4,7 @@ members = [ "ec-slimloader", "ec-slimloader-imxrt", "ec-slimloader-state", + "ec-slimloader-mcxa", "imxrt-rom", ] @@ -21,6 +22,20 @@ defmt = "0.3.7" defmt-or-log = { version = "0.2.2", default-features = false } embedded-storage-async = "0.4.1" embassy-sync = "0.7.2" +embassy-futures = "0.1" +embassy-time = "0.5" +embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt.git", default-features = false, features = ["mimxrt685s"] } +embassy-mcxa = { git = "https://github.com/embassy-rs/embassy", rev = "30d627365bc9c62cf391aa02b691a4ae1a95e6cc", default-features = false } log = "0.4" cortex-m = { version = "0.7.7" } +panic-probe = "0.3" +arbitrary = "1.4" +crc = "3.2" +num_enum = { version = "0.7", default-features = false } +device-driver = "1.0" + +[patch.crates-io] +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "30d627365bc9c62cf391aa02b691a4ae1a95e6cc" } +embassy-time-driver = { git = "https://github.com/embassy-rs/embassy", rev = "30d627365bc9c62cf391aa02b691a4ae1a95e6cc" } +embassy-time-queue-utils = { git = "https://github.com/embassy-rs/embassy", rev = "30d627365bc9c62cf391aa02b691a4ae1a95e6cc" } diff --git a/libs/ec-slimloader-imxrt/src/lib.rs b/libs/ec-slimloader-imxrt/src/lib.rs index a264ed22..3d56eb34 100644 --- a/libs/ec-slimloader-imxrt/src/lib.rs +++ b/libs/ec-slimloader-imxrt/src/lib.rs @@ -124,7 +124,7 @@ impl Board for Imxrt { &mut self.journal } - async fn check_and_boot(&mut self, slot: &Slot) -> BootError { + async fn check_and_boot(&mut self, slot: &Slot) -> BootError { let Some(slot_partition) = self.slots.get_mut(u8::from(*slot) as usize) else { return BootError::SlotUnknown; }; @@ -204,4 +204,16 @@ impl Board for Imxrt { cortex_m::asm::wfi(); } } + + fn arm_mcu_reset(&mut self) -> ! { + const AIRCR: *mut u32 = 0xE000ED0C as *mut u32; + const AIRCR_VECTKEY: u32 = 0x5FA << 16; + const AIRCR_SYSRESETREQ: u32 = 1 << 2; + unsafe { + core::ptr::write_volatile(AIRCR, AIRCR_VECTKEY | AIRCR_SYSRESETREQ); + } + loop { + cortex_m::asm::wfi(); + } + } } diff --git a/libs/ec-slimloader-mcxa/Cargo.toml b/libs/ec-slimloader-mcxa/Cargo.toml new file mode 100644 index 00000000..e4e895fc --- /dev/null +++ b/libs/ec-slimloader-mcxa/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ec-slimloader-mcxa" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +device-driver.workspace = true +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true, optional = true } +log = { workspace = true, optional = true } +ec-slimloader = { path = "../ec-slimloader", default-features = false } +ec-slimloader-state = { path = "../ec-slimloader-state", default-features = false } +cortex-m = "0.7" +embassy-sync = { workspace = true } +embassy-time = { workspace = true } +embassy-mcxa = { workspace = true, features = ["rt"] } +embedded-storage-async = { workspace = true } + +[features] +default = ["internal-only", "mcxa5xx"] +internal-only = [] +defmt = ["dep:defmt", "dep:defmt-or-log", "defmt-or-log/defmt", "ec-slimloader/defmt"] +log = ["dep:log", "dep:defmt-or-log", "defmt-or-log/log", "ec-slimloader/log"] +certificate-logging = [] +verification-logging = [] +mcxa2xx = ["embassy-mcxa/mcxa2xx"] +mcxa5xx = ["embassy-mcxa/mcxa5xx"] + + diff --git a/libs/ec-slimloader-mcxa/src/certificate.rs b/libs/ec-slimloader-mcxa/src/certificate.rs new file mode 100644 index 00000000..61b58f12 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/certificate.rs @@ -0,0 +1,1009 @@ +// AHAB container + certificate parsing for MCXA family with PQC support. +// Supports hybrid keys: ECDSA P-384 and ML-DSA-87. +use core::mem::size_of; +use embassy_mcxa::{peripherals, Peri}; + +macro_rules! cert_trace { + ($($arg:tt)*) => { + #[cfg(feature = "certificate-logging")] + { + defmt_or_log::trace!($($arg)*); + } + }; +} + +macro_rules! cert_error { + ($($arg:tt)*) => { + #[cfg(feature = "certificate-logging")] + { + defmt_or_log::error!($($arg)*); + } + }; +} + +// 384-bit Root Key Table Hash (SHA-384 digest of RoTK public key X||Y) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Rkth([u8; 48]); + +impl Rkth { + pub fn as_be_words(&self) -> [u32; 12] { + let mut w = [0u32; 12]; + for (i, chunk) in self.0.chunks_exact(4).enumerate() { + w[i] = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + w + } + + pub fn as_le_words(&self) -> [u32; 12] { + let mut w = [0u32; 12]; + for (i, chunk) in self.0.chunks_exact(4).enumerate() { + w[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + w + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug)] +pub enum CertError { + TooLarge, + Magic, + SizeField, + Bounds, + EcType, + Offset, + SignatureMissing, + Align, + Tag, + Version, +} + +// ===== AHAB structures (per MCXN556S RM tables) ===== + +/// Minimal parsed view to feed ROM authentication +pub struct AhabParsed { + pub container: *const AhabContainerHeaderRaw, + pub images: *const AhabImageEntryRaw, + pub images_count: usize, + pub sigblk: *const AhabSignatureBlockRaw, + pub srk_array_ptr: *const u8, + pub srk_array_len: usize, + pub cert_ptr: *const u8, + pub cert_len: usize, + pub sig_ptr: *const u8, + pub sig_len: usize, +} + +/// Deeper parser structures +pub struct ParsedSrkRecord<'a> { + pub hdr: &'a AhabSrkRecordRaw, +} + +pub struct ParsedSrkTable<'a> { + pub hdr: &'a AhabSrkTableHeaderRaw, + pub records: &'a [AhabSrkRecordRaw], + pub raw_table_bytes: &'a [u8], // Complete 308-byte SRK table for RKTH calculation +} + +pub struct ParsedSrkArray<'a> { + pub hdr: &'a AhabSrkArrayHeaderRaw, + pub ecdsa_table: ParsedSrkTable<'a>, // ECDSA table (up to 4 ECDSA SRK records) + pub mldsa_table: ParsedSrkTable<'a>, // ML-DSA table (up to 4 ML-DSA SRK records) +} + +pub struct ParsedCertificate<'a> { + pub hdr: &'a AhabCertificateHeaderRaw, + pub payload: &'a [u8], + pub signature_region: &'a [u8], +} + +pub struct ParsedSignatures<'a> { + pub hdr: &'a AhabSignatureHeaderRaw, + pub payload: &'a [u8], +} + +fn sha512_rkth_48(peri: Peri<'_, peripherals::SGI0>, input: &[u8]) -> Option<[u8; 48]> { + let sgi = embassy_mcxa::sgi::Sgi::new_blocking(peri).ok()?; + let mut blocking_hasher = crate::BlockingHasher::new(sgi); + + blocking_hasher.hsm_sha512_rkth(input) +} + +#[repr(C)] +pub struct AhabContainerHeaderRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: Flags + pub flags: u32, // SRK set (3-0), SRK selection (5-4), reserved (31-6) + // Word 2: # of images (31-24), Fuse version (23-16), SW version (15-0) + pub word2: u32, // Images(31-24) | Fuse ver(23-16) | SW ver(15-0) + // Word 3: Reserved (31-24), Cert version (23-16), Signature block offset (15-0) + pub word3: u32, // Reserved(31-24) | Cert ver(23-16) | Sigblk offset(15-0) +} + +impl AhabContainerHeaderRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get the total container length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get image count from word2 (bits 31-24) + pub fn image_count(&self) -> u8 { + (self.word2 >> 24) as u8 + } + + /// Get fuse version from word2 (bits 23-16) + pub fn fuse_version(&self) -> u8 { + ((self.word2 >> 16) & 0xFF) as u8 + } + + /// Get software version from word2 (bits 15-0) + pub fn sw_version(&self) -> u16 { + (self.word2 & 0xFFFF) as u16 + } + + /// Get certificate version from word3 (bits 23-16) + pub fn cert_version(&self) -> u8 { + ((self.word3 >> 16) & 0xFF) as u8 + } + + /// Get the signature block offset from word3 (bits 15-0) + pub fn signature_block_offset(&self) -> u32 { + (self.word3 & 0xFFFF) as u32 + } + + /// Get SRK set from flags (bits 3-0) + pub fn srk_set(&self) -> u8 { + (self.flags & 0xF) as u8 + } + + /// Get SRK selection from flags (bits 5-4) + pub fn srk_selection(&self) -> u8 { + ((self.flags >> 4) & 0x3) as u8 + } + + /// Compute combined version for anti-rollback check + pub fn combined_version(&self) -> u16 { + (self.sw_version() << 8) | (self.fuse_version() as u16) + } +} + +#[repr(C)] +pub struct AhabImageEntryRaw { + // Word 0: Image offset (32 bits) + pub offset: u32, // Offset in bytes from start of container header to beginning of image + // Word 1: Image size (32 bits) + pub size: u32, // Size of the image in bytes + // Words 2-3: Load address (64 bits - high word set to zero on MCXN556S) + pub load_address: u64, // Address where image is copied to in memory by ROM + // Words 4-5: Entry point (64 bits - high word set to zero on MCXN556S) + pub entry_point: u64, // Entry point of the image (absolute address) + // Word 6: Flags (32 bits) + pub flags: u32, // Image type (3-0), Core ID (7-4), Hash type (11-8), Reserved (31-12) + // Word 7: Reserved (32 bits) + pub reserved1: u32, // Reserved + // Words 8-23: Hash (512 bits = 64 bytes, left-aligned and zero-padded) + pub hash: [u8; 64], // Hash of image (SHA2_384 or SHA2_512) + // Words 24-31: Reserved (256 bits = 32 bytes, set to 0) + pub reserved2: [u8; 32], // Unused, set to 256'b0 +} + +impl AhabImageEntryRaw { + /// Get image type from flags (bits 3-0) + pub fn image_type(&self) -> u8 { + (self.flags & 0xF) as u8 + } + + /// Check if this is an SB4 file + pub fn is_sb4_file(&self) -> bool { + self.image_type() == 0xF + } + + /// Get core ID from flags (bits 7-4) - Reserved on MCXN556S + pub fn core_id(&self) -> u8 { + ((self.flags >> 4) & 0xF) as u8 + } + + /// Get hash type from flags (bits 11-8) + pub fn hash_type(&self) -> u8 { + ((self.flags >> 8) & 0xF) as u8 + } + + /// Check if hash type is SHA2_384 + pub fn is_sha384(&self) -> bool { + self.hash_type() == 0x1 + } + + /// Check if hash type is SHA2_512 + pub fn is_sha512(&self) -> bool { + self.hash_type() == 0x2 + } +} + +#[repr(C)] +pub struct AhabSignatureBlockRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: SRK table array offset (31-16), Certificate offset (15-0) + pub word1: u32, // SRK offset(31-16) | Cert offset(15-0) + // Word 2: Reserved (31-16), Signature offset (15-0) + pub word2: u32, // Reserved(31-16) | Signature offset(15-0) + // Word 3: Reserved (48 bits total, continued from Word 2) + pub reserved2: u32, // bits 31-0: Reserved (completion of 48-bit reserved field) +} + +impl AhabSignatureBlockRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get SRK array offset from word1 (bits 31-16) + pub fn srk_array_offset(&self) -> u16 { + (self.word1 >> 16) as u16 + } + + /// Get certificate offset from word1 (bits 15-0) + pub fn cert_offset(&self) -> u16 { + (self.word1 & 0xFFFF) as u16 + } + + /// Get signature offset from word2 (bits 15-0) + pub fn signature_offset(&self) -> u16 { + (self.word2 & 0xFFFF) as u16 + } + + /// Get SRK array offset as u32 for compatibility + pub fn srk_array_offset_u32(&self) -> u32 { + self.srk_array_offset() as u32 + } + + /// Get certificate offset as u32 for compatibility + pub fn cert_offset_u32(&self) -> u32 { + self.cert_offset() as u32 + } + + /// Get signature offset as u32 for compatibility + pub fn signature_offset_u32(&self) -> u32 { + self.signature_offset() as u32 + } + + /// Check if SRK table array is present (offset != 0x0000) + pub fn has_srk_table(&self) -> bool { + self.srk_array_offset() != 0x0000 + } + + /// Check if certificate is present (offset != 0x0000) + /// If false, only SRK is used for signature verification + pub fn has_certificate(&self) -> bool { + self.cert_offset() != 0x0000 + } +} + +// Note: Component ordering in signature block: +// 1. SRK table array (Required, starts on 64 bit boundary) +// 2. Certificate (optional, starts on 64 bit boundary) +// 3. Signature (required, starts on 64 bit boundary) +// Container signature covers all data from container header start to signature start. +// All padding must be zeros to maintain 64 bit alignment. + +// Note: In hybrid mode, SRK selection targets both ECDSA and ML-DSA keys. +// For example, when selecting SRK0, both first SRKs of first and second +// SRK table will be used. No SRK index mix/match is supported. +// +// SRK table array structure: +// Single mode: 1 SRK table (ECDSA only) +// SRK table 0: Non-quantum resistant key (ECDSA) +// Hybrid mode: 2 SRK tables (ECDSA + ML-DSA) +// SRK table 0: Non-quantum resistant key (ECDSA) +// SRK table 1: Quantum-resistant key (ML-DSA) +// Keys must match signing key in algorithm and key length. + +#[repr(C)] +pub struct AhabSrkArrayHeaderRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: Reserved (31-8), # of SRK Tables (7-0) + pub word1: u32, // Reserved(31-8) | SRK table count(7-0) + // Sequential layout after header: + // ECDSA table (SRK table 0) - starts immediately after header + // ML-DSA table (SRK table 1) - starts immediately after ECDSA data which is after ECDSA table + // SRK data sections follow after respective tables +} + +impl AhabSrkArrayHeaderRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get SRK table count from word1 (bits 7-0) + pub fn srk_table_count(&self) -> u8 { + (self.word1 & 0xFF) as u8 + } + + /// Check if this is hybrid mode (2 SRK tables: ECDSA + ML-DSA) + pub fn is_hybrid_mode(&self) -> bool { + self.srk_table_count() == 2 + } + + /// Check if this is single signature mode (1 SRK table: ECDSA only) + pub fn is_single_mode(&self) -> bool { + self.srk_table_count() == 1 + } + + /// Get number of signature algorithms supported + pub fn signature_count(&self) -> u8 { + self.srk_table_count() + } +} + +#[repr(C)] +pub struct AhabSrkTableHeaderRaw { + // Word 0: Version (31-24), Length (23-8), Tag (7-0) + pub word0: u32, // Version(31-24) | Length(23-8) | Tag(7-0) + // Words 1-4: SRK records 0-3 (each record is 32 bits) + // Followed by actual SRK record structures +} + +impl AhabSrkTableHeaderRaw { + /// Get version from word0 (bits 31-24) + pub fn version(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get tag from word0 (bits 7-0) + pub fn tag(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get the size of SRK table excluding SRK data + pub fn table_size(&self) -> u16 { + self.length() + } + + /// Calculate number of SRK records in this table + /// Each SRK record is a fixed size structure + pub fn record_count(&self) -> usize { + // After header (4 bytes), remaining bytes are SRK records + let record_area_size = self.length().saturating_sub(4) as usize; + record_area_size / core::mem::size_of::() + } +} + +#[repr(C)] +pub struct AhabSrkRecordRaw { + // Word 0: Sign alg (31-24), Length (23-8), Tag (7-0) + pub word0: u32, // Sign alg(31-24) | Length(23-8) | Tag(7-0) + // Word 1: SRK flags (31-24), Reserved (23-16), Key size (15-8), Hash alg (7-0) + pub word1: u32, // SRK flags(31-24) | Reserved(23-16) | Key size(15-8) | Hash alg(7-0) + // Word 2: Parameter lengths (format type dependent) + pub param_lens: u32, // ECDSA: X size (15-0), Y size (31-16) | ML-DSA: Raw key size (15-0) + // Word 3+: Hash of public key (SRK data) - 512 bits = 16 words + pub srk_data_hash: [u8; 64], // Hash of SRK Data, 512-bit, left-aligned and zero-padded +} + +impl AhabSrkRecordRaw { + /// Get sign algorithm from word0 (bits 31-24) + pub fn sign_alg(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get tag from word0 (bits 7-0) + pub fn tag(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get SRK flags from word1 (bits 31-24) + pub fn srk_flags(&self) -> u8 { + (self.word1 >> 24) as u8 + } + + /// Get reserved field from word1 (bits 23-16) + pub fn reserved(&self) -> u8 { + ((self.word1 >> 16) & 0xFF) as u8 + } + + /// Get key size from word1 (bits 15-8) + pub fn key_size(&self) -> u8 { + ((self.word1 >> 8) & 0xFF) as u8 + } + + /// Get hash algorithm from word1 (bits 7-0) + pub fn hash_alg(&self) -> u8 { + (self.word1 & 0xFF) as u8 + } + + // Check if this is an ECDSA key + pub fn is_ecdsa(&self) -> bool { + self.sign_alg() == 0x27 + } + + // Check if this is an ML-DSA key + pub fn is_mldsa(&self) -> bool { + self.sign_alg() == 0xD2 + } + + // Check if this is a SEC384R1 key + pub fn is_sec384r1(&self) -> bool { + self.key_size() == 0x2 + } + + // Check if this is an MLDSA87 key + pub fn is_mldsa87(&self) -> bool { + self.key_size() == 0xA + } + + // Check if CA flags are set + pub fn is_ca(&self) -> bool { + self.srk_flags() & 0x80 != 0 + } + + // Check if hash algorithm is SHA2_384 + pub fn is_sha384(&self) -> bool { + self.hash_alg() == 0x1 + } + + // Check if hash algorithm is SHA2_512 + pub fn is_sha512(&self) -> bool { + self.hash_alg() == 0x2 + } + + // Get X parameter size for ECDSA keys (bits 15-0) + pub fn ecdsa_x_size(&self) -> u16 { + (self.param_lens & 0xFFFF) as u16 + } + + // Get Y parameter size for ECDSA keys (bits 31-16) + pub fn ecdsa_y_size(&self) -> u16 { + (self.param_lens >> 16) as u16 + } + + // Get raw key size for ML-DSA keys (bits 15-0) + pub fn mldsa_key_size(&self) -> u16 { + (self.param_lens & 0xFFFF) as u16 + } +} + +#[repr(C)] +pub struct AhabSrkDataHeaderRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: Reserved (31-8), SRK record # (7-0) + pub word1: u32, // Reserved(31-8) | SRK record index(7-0) + // Word 2+: Key data (format is type dependent) + // ECDSA: X (big endian), Y (big endian) - parameter size aligned, padded with leading zeros + // ML-DSA: Raw key (big endian) - parameter size aligned, padded with leading zeros +} + +impl AhabSrkDataHeaderRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get record index from word1 (bits 7-0) + pub fn record_index(&self) -> u8 { + (self.word1 & 0xFF) as u8 + } + + /// Get the total size of SRK data including header + pub fn total_size(&self) -> u16 { + self.length() + } + + /// Get the size of key data (excluding 8-byte header) + pub fn key_data_size(&self) -> u16 { + self.length().saturating_sub(8) + } + + /// Get the SRK record number this data is associated with + pub fn srk_record_number(&self) -> u8 { + self.record_index() + } + + /// Check if this SRK data is for ECDSA (expects 96 bytes: 48 X + 48 Y) + pub fn is_ecdsa_size(&self) -> bool { + self.key_data_size() == 96 + } + + /// Check if this SRK data is for ML-DSA-87 (expects 2592 bytes) + pub fn is_mldsa87_size(&self) -> bool { + self.key_data_size() == 2592 + } +} + +#[repr(C)] +pub struct AhabSignatureHeaderRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: Reserved + pub reserved: u32, // Reserved + // Word 2+: Signature data (format is type dependent) + // ECDSA: r and s components (curve size aligned, padded with zeros) + // ML-DSA: Raw signature + // Signatures are in the same order as associated SRK tables +} + +impl AhabSignatureHeaderRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get the total size of signature block including header + pub fn total_size(&self) -> u16 { + self.length() + } + + /// Get the size of signature data (excluding 8-byte header) + pub fn signature_data_size(&self) -> u16 { + self.length().saturating_sub(8) + } + + /// Check if this signature data is for ECDSA P-384 (expects 96 bytes: 48 r + 48 s) + pub fn is_ecdsa_size(&self) -> bool { + self.signature_data_size() == 96 + } + + /// Check if this signature data is for ML-DSA-87 + pub fn is_mldsa_size(&self) -> bool { + // ML-DSA-87 signatures are variable length but typically around 4627 bytes + let size = self.signature_data_size(); + size >= 4000 && size <= 5000 // Reasonable range for ML-DSA-87, search says ~4564 bytes. + } +} + +// Certificate format per Tables 168-169 +#[repr(C)] +pub struct AhabCertificateHeaderRaw { + // Word 0: Tag (31-24), Length (23-8), Version (7-0) + pub word0: u32, // Tag(31-24) | Length(23-8) | Version(7-0) + // Word 1: Perm (31-16), Signature offset (15-0) + pub word1: u32, // Permissions(31-16) | Signature offset(15-0) + // Word 2-4: Permission data (96 bits = 12 bytes) + pub perm_data: [u8; 12], // 96 bits of complementary information for debug auth + // Word 5: Fuse version (8 bits, position TODO) + reserved (24 bits) + pub fuse_version_word: u32, // Word 5: fuse version field (bit layout TBD) + // Word 6-9: UUID (128 bits = 16 bytes) + pub uuid: [u8; 16], // unique ID of targeted device +} + +impl AhabCertificateHeaderRaw { + /// Get tag from word0 (bits 31-24) + pub fn tag(&self) -> u8 { + (self.word0 >> 24) as u8 + } + + /// Get length from word0 (bits 23-8) + pub fn length(&self) -> u16 { + ((self.word0 >> 8) & 0xFFFF) as u16 + } + + /// Get version from word0 (bits 7-0) + pub fn version(&self) -> u8 { + (self.word0 & 0xFF) as u8 + } + + /// Get permissions from word1 (bits 31-16) + pub fn permissions(&self) -> u16 { + (self.word1 >> 16) as u16 + } + + /// Get signature offset from word1 (bits 15-0) + pub fn signature_offset(&self) -> u32 { + (self.word1 & 0xFFFF) as u32 + } + + /// Check if this is a valid certificate (tag 0xAF, version 0x02) + pub fn is_valid(&self) -> bool { + self.tag() == 0xAF && self.version() == 0x02 + } + + /// Get permission data as slice + pub fn perm_data_slice(&self) -> &[u8] { + &self.perm_data + } + + /// Check if permissions are valid (no validation needed without perm_inv) + pub fn permissions_valid(&self) -> bool { + true // No perm_inv field to validate against + } + + /// Get fuse version (bit layout TODO - assuming lower 8 bits for now) + pub fn fuse_version(&self) -> u8 { + (self.fuse_version_word & 0xFF) as u8 + } +} + +fn is_aligned_4(ptr: *const u8) -> bool { + (ptr as usize) % 4 == 0 +} + +#[inline(always)] +fn checked_end(start: usize, len: usize) -> Result { + start.checked_add(len).ok_or(CertError::Bounds) +} + +/// Parse AHAB container from given base pointer and offsets, returning structured views of components. Used to derive the SRK array table to compute RoTKH hashes. +/// If certificate is absent (SRK-only mode), cert_ptr will be null and cert_len will be 0. Signature block is still required in SRK-only mode to provide signature offset and SRK array offset. +pub unsafe fn parse_ahab_container( + base: *const u8, + container_offset: u32, + image_len: u32, +) -> Result { + if container_offset >= image_len { + return Err(CertError::Bounds); + } + let start = base.add(container_offset as usize); + let ch = start as *const AhabContainerHeaderRaw; + + if (*ch).tag() != 0x87 { + return Err(CertError::Tag); + } + + if (*ch).version() != 0x02 { + return Err(CertError::Version); + } + let total = (*ch).length() as usize; + + let container_end = checked_end(container_offset as usize, total)?; + if total == 0 || container_end > image_len as usize { + return Err(CertError::Bounds); + } + + if !is_aligned_4(start) { + return Err(CertError::Align); + } + + // Image array begins after header; calculate length from sigblk_offset + let image_array_start = start.add(size_of::()); + let _image_entry_size = size_of::(); + let sigblk_offset = (*ch).signature_block_offset() as usize; + if sigblk_offset < size_of::() { + return Err(CertError::Bounds); + } + if checked_end(sigblk_offset, size_of::())? > total { + return Err(CertError::Bounds); + } + let _image_array_size = sigblk_offset - size_of::(); + let images_len = (*ch).image_count() as usize; + + // Store image array pointer and count + let images_ptr = image_array_start as *const AhabImageEntryRaw; + + // Signature block + let sigblk_ptr = start.add(sigblk_offset); + + if !is_aligned_4(sigblk_ptr) { + return Err(CertError::Align); + } + let sigblk = sigblk_ptr as *const AhabSignatureBlockRaw; + + if (*sigblk).tag() != 0x90 || (*sigblk).version() != 0x01 { + return Err(CertError::Tag); + } + + // SRK array raw slice + let srk_array_offset = (*sigblk).srk_array_offset() as usize; + if checked_end(sigblk_offset, srk_array_offset)? > total { + return Err(CertError::Bounds); + } + if checked_end(sigblk_offset + srk_array_offset, size_of::())? > total { + return Err(CertError::Bounds); + } + let srk_array_ptr = sigblk_ptr.add(srk_array_offset); + + if !is_aligned_4(srk_array_ptr) { + return Err(CertError::Align); + } + // Method 1: Use SRK array header's own length field + let srk_hdr = srk_array_ptr as *const AhabSrkArrayHeaderRaw; + let srk_header_len = (*srk_hdr).length() as usize; + + // Method 2: Calculate from offsets (signature_offset - srk_array_offset) + let sig_offset = (*sigblk).signature_offset() as usize; + if checked_end(sigblk_offset, sig_offset)? > total { + return Err(CertError::Bounds); + } + let srk_offset_len = sig_offset.saturating_sub(srk_array_offset); + + // Use the smaller of the two for safety (prevent buffer overrun) + let srk_array_len = core::cmp::min(srk_header_len, srk_offset_len); + + // Certificate raw slice + //Note that certificate may be absent (SRK-only mode), it is optional. + let (cert_ptr, cert_len) = if (*sigblk).cert_offset() == 0 { + // SRK-only mode: no certificate present + (core::ptr::null(), 0) + } else { + let cert_offset = (*sigblk).cert_offset() as usize; + if checked_end(sigblk_offset, cert_offset)? > total { + return Err(CertError::Bounds); + } + if checked_end(sigblk_offset + cert_offset, size_of::())? > total { + return Err(CertError::Bounds); + } + let cert_ptr = sigblk_ptr.add(cert_offset); + if !is_aligned_4(cert_ptr) { + return Err(CertError::Align); + } + // Read certificate header to get its total length + let cert_hdr = cert_ptr as *const AhabCertificateHeaderRaw; + + if (*cert_hdr).tag() != 0xAF || (*cert_hdr).version() != 0x02 { + return Err(CertError::Tag); + } + let cert_len = (*cert_hdr).length() as usize; + if checked_end(sigblk_offset + cert_offset, cert_len)? > total { + return Err(CertError::Bounds); + } + (cert_ptr, cert_len) + }; + // Signatures raw slice + if checked_end(sigblk_offset + sig_offset, size_of::())? > total { + return Err(CertError::Bounds); + } + let sig_ptr = sigblk_ptr.add(sig_offset); + if !is_aligned_4(sig_ptr) { + return Err(CertError::Align); + } + // Read signature header to measure length + let sig_hdr = sig_ptr as *const AhabSignatureHeaderRaw; + + if (*sig_hdr).tag() != 0xD8 || (*sig_hdr).version() != 0x00 { + return Err(CertError::Tag); + } + let sig_len = (*sig_hdr).length() as usize; + if checked_end(sigblk_offset + sig_offset, sig_len)? > total { + return Err(CertError::Bounds); + } + cert_trace!( + "Parsed AHAB container: images={}, srk_array_len={}, cert_len={}, sig_len={}", + images_len, + srk_array_len, + cert_len, + sig_len + ); + Ok(AhabParsed { + container: ch, + images: images_ptr, + images_count: images_len, + sigblk, + srk_array_ptr, + srk_array_len, + cert_ptr, + cert_len, + sig_ptr, + sig_len, + }) +} + +/// Deeper parsers: SRK array, certificate internals, signatures +/// Parse SRK array: expects header followed by arrays of table offsets and data offsets. +pub unsafe fn parse_srk_array<'a>( + srk_array_ptr: *const u8, + srk_array_len: usize, +) -> Result, CertError> { + let base = srk_array_ptr; + let hdr = &*(base as *const AhabSrkArrayHeaderRaw); + + if srk_array_len < size_of::() + 16 { + return Err(CertError::Bounds); + } + if hdr.tag() != 0x5A || hdr.version() != 0x00 { + return Err(CertError::Tag); + } + + let count = hdr.srk_table_count() as usize; + if count != 2 { + return Err(CertError::Bounds); + } // Only hybrid mode supported (ECDSA + ML-DSA) + + // Sequential layout: ECDSA table starts immediately after header + let ecdsa_tbl_ptr = base.add(size_of::()); + let ecdsa_tbl_hdr = &*(ecdsa_tbl_ptr as *const AhabSrkTableHeaderRaw); + + if ecdsa_tbl_hdr.tag() != 0xD7 || ecdsa_tbl_hdr.version() != 0x43 { + return Err(CertError::Tag); + } + let ecdsa_rec_count = ecdsa_tbl_hdr.record_count(); + let ecdsa_rec_base = ecdsa_tbl_ptr.add(size_of::()) as *const AhabSrkRecordRaw; + + // Check if ECDSA table fits within bounds + let ecdsa_tbl_offset = size_of::(); + let ecdsa_tbl_total_size = checked_end( + size_of::(), + ecdsa_rec_count + .checked_mul(size_of::()) + .ok_or(CertError::Bounds)?, + )?; + + if checked_end(ecdsa_tbl_offset, ecdsa_tbl_total_size)? > srk_array_len { + return Err(CertError::Bounds); + } + + let ecdsa_records = core::slice::from_raw_parts(ecdsa_rec_base, ecdsa_rec_count); + let ecdsa_raw_table = core::slice::from_raw_parts(ecdsa_tbl_ptr, ecdsa_tbl_total_size); + + // Parse ML-DSA table (table 1) - starts after ECDSA table + ECDSA data + // Find ECDSA data size first + let ecdsa_data_ptr = ecdsa_tbl_ptr.add(ecdsa_tbl_total_size); + let ecdsa_data_offset = checked_end(ecdsa_tbl_offset, ecdsa_tbl_total_size)?; + if checked_end(ecdsa_data_offset, size_of::())? > srk_array_len { + return Err(CertError::Bounds); + } + let ecdsa_data_hdr = &*(ecdsa_data_ptr as *const AhabSrkDataHeaderRaw); + let ecdsa_data_total_size = ecdsa_data_hdr.length() as usize; + if ecdsa_data_total_size < size_of::() { + return Err(CertError::Bounds); + } + if checked_end(ecdsa_data_offset, ecdsa_data_total_size)? > srk_array_len { + return Err(CertError::Bounds); + } + + let mldsa_tbl_ptr = ecdsa_data_ptr.add(ecdsa_data_total_size); + let mldsa_tbl_offset = checked_end(ecdsa_data_offset, ecdsa_data_total_size)?; + if checked_end(mldsa_tbl_offset, size_of::())? > srk_array_len { + return Err(CertError::Bounds); + } + let mldsa_tbl_hdr = &*(mldsa_tbl_ptr as *const AhabSrkTableHeaderRaw); + + if mldsa_tbl_hdr.tag() != 0xD7 || mldsa_tbl_hdr.version() != 0x43 { + return Err(CertError::Tag); + } + + let mldsa_rec_count = mldsa_tbl_hdr.record_count(); + let mldsa_rec_base = mldsa_tbl_ptr.add(size_of::()) as *const AhabSrkRecordRaw; + + // Check if ML-DSA table fits within bounds + let mldsa_tbl_total_size = checked_end( + size_of::(), + mldsa_rec_count + .checked_mul(size_of::()) + .ok_or(CertError::Bounds)?, + )?; + + if checked_end(mldsa_tbl_offset, mldsa_tbl_total_size)? > srk_array_len { + return Err(CertError::Bounds); + } + + let mldsa_records = core::slice::from_raw_parts(mldsa_rec_base, mldsa_rec_count); + let mldsa_raw_table = core::slice::from_raw_parts(mldsa_tbl_ptr, mldsa_tbl_total_size); + cert_trace!( + "Parsed SRK array: ECDSA records={}, ML-DSA records={}", + ecdsa_rec_count, + mldsa_rec_count + ); + Ok(ParsedSrkArray { + hdr, + ecdsa_table: ParsedSrkTable { + hdr: ecdsa_tbl_hdr, + records: ecdsa_records, + raw_table_bytes: ecdsa_raw_table, + }, + mldsa_table: ParsedSrkTable { + hdr: mldsa_tbl_hdr, + records: mldsa_records, + raw_table_bytes: mldsa_raw_table, + }, + }) +} + +/// Derive RKTH values for both ECDSA and ML-DSA from the AHAB container's SRK array. Returns the leftmost 48 bytes of the SHA-512 digest of the complete SRK table (header + records) for each algorithm. +/// If any step fails, returns None for that RKTH. +pub fn derive_image_rkth_pair<'d>( + mut peri: Peri<'d, peripherals::SGI0>, + image_base: *const u8, + container_offset: u32, + image_len: u32, +) -> (Option, Option) { + // Parse AHAB container once and extract both ECDSA and ML-DSA RKTH values + unsafe { + if let Ok(ahab) = parse_ahab_container(image_base, container_offset, image_len) { + if let Ok(srk_parsed) = parse_srk_array(ahab.srk_array_ptr, ahab.srk_array_len) { + // Derive ECDSA RKTH from complete ECDSA table (header + records) + let ecdsa_rkth = { + let table = &srk_parsed.ecdsa_table; + + // Use the complete SRK table for RKTH calculation + let table_bytes = table.raw_table_bytes; + match sha512_rkth_48(peri.reborrow(), table_bytes) { + //TODO: Verify if SHA-384 is correct here, SRM vs. SPSDK mismatch + Some(digest) => Some(Rkth(digest)), + None => { + cert_error!("SHA-512 unavailable for ECDSA RKTH"); + None + } + } + }; + + // Derive ML-DSA RKTH from complete ML-DSA table (header + records) + let mldsa_rkth = { + let table = &srk_parsed.mldsa_table; + + // Use the complete SRK table for RKTH calculation + let table_bytes = table.raw_table_bytes; + + match sha512_rkth_48(peri.reborrow(), table_bytes) { + Some(digest) => Some(Rkth(digest)), + None => { + cert_error!("SHA-512 unavailable for PQC RKTH"); + None + } + } + }; + cert_trace!("Derived both ECDSA and ML-DSA RKTH values"); + return (ecdsa_rkth, mldsa_rkth); + } else { + cert_error!("Failed to parse SRK array"); + } + } else { + cert_error!("Failed to parse AHAB container"); + } + } + (None, None) +} diff --git a/libs/ec-slimloader-mcxa/src/error.rs b/libs/ec-slimloader-mcxa/src/error.rs new file mode 100644 index 00000000..f19678e7 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/error.rs @@ -0,0 +1,320 @@ +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use ec_slimloader::BootError; + +const KSTATUS_SUCCESS: u32 = 0; +const KSTATUS_FAIL: u32 = 1; +const KSTATUS_INVALID_ARGUMENT: u32 = 4; + +const KSTATUS_FLASH_SUCCESS: u32 = 0; +const KSTATUS_FLASH_INVALID_ARGUMENT: u32 = 4; +const KSTATUS_FLASH_ALIGNMENT_ERROR: u32 = 101; +const KSTATUS_FLASH_ADDRESS_ERROR: u32 = 102; +const KSTATUS_FLASH_SIZE_ERROR: u32 = 100; +const KSTATUS_FLASH_COMMAND_FAILURE: u32 = 105; +const KSTATUS_FLASH_UNKNOWN_PROPERTY: u32 = 106; +const KSTATUS_FLASH_ERASE_KEY_ERROR: u32 = 107; +const KSTATUS_FLASH_REGION_EXECUTE_ONLY: u32 = 108; +const KSTATUS_FLASH_COMMAND_NOT_SUPPORTED: u32 = 111; +const KSTATUS_FLASH_READ_ONLY_PROPERTY: u32 = 112; +const KSTATUS_FLASH_INVALID_PROPERTY_VALUE: u32 = 113; +const KSTATUS_FLASH_ECC_ERROR: u32 = 116; +const KSTATUS_FLASH_COMPARE_ERROR: u32 = 117; +const KSTATUS_FLASH_INVALID_WAIT_STATE_CYCLES: u32 = 119; + +// SPI flash driver status codes +const KSTATUS_SPIFLASH_SUCCESS: u32 = KSTATUS_SUCCESS; +const KSTATUS_SPIFLASH_FAIL: u32 = KSTATUS_FAIL; + +// FlexSPI flash driver status codes +const KSTATUS_FLEXSPI_SUCCESS: u32 = KSTATUS_SUCCESS; +const KSTATUS_FLEXSPI_FAIL: u32 = KSTATUS_FAIL; +const KSTATUS_FLEXSPI_INVALID_ARGUMENT: u32 = KSTATUS_INVALID_ARGUMENT; +const KSTATUS_FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT: u32 = 6000; +const KSTATUS_FLEXSPI_INVALID_SEQUENCE: u32 = 6001; +const KSTATUS_FLEXSPI_DEVICE_TIMEOUT: u32 = 6002; + +const KSTATUS_FLEXSPINOR_PROGRAM_FAIL: u32 = 20100; +const KSTATUS_FLEXSPINOR_ERASE_SECTOR_FAIL: u32 = 20101; +const KSTATUS_FLEXSPINOR_ERASE_ALL_FAIL: u32 = 20102; +const KSTATUS_FLEXSPINOR_WAIT_TIMEOUT: u32 = 20103; +const KSTATUS_FLEXSPINOR_WRITE_ALIGNMENT_ERROR: u32 = 20105; +const KSTATUS_FLEXSPINOR_COMMAND_FAILURE: u32 = 20106; +const KSTATUS_FLEXSPINOR_SFDP_NOT_FOUND: u32 = 20107; +const KSTATUS_FLEXSPINOR_UNSUPPORTED_SFDP_VERSION: u32 = 20108; +const KSTATUS_FLEXSPINOR_FLASH_NOT_FOUND: u32 = 20109; +const KSTATUS_FLEXSPINOR_DTR_READ_DUMMY_PROBE_FAILED: u32 = 20110; + +const KSTATUS_NBOOT_SUCCESS: u64 = 0x5A5A_5A5A; +const KSTATUS_NBOOT_FAIL: u64 = 0x5A5A_A5A5; +const KSTATUS_NBOOT_INVALID_ARGUMENT: u64 = 0x5A5A_A5F0; + +// NBOOT API status codes (MCXA ROM, Table 46 / 9.2.5.11) +// These are returned by APIs such as `nboot_mem_crypt_range_checker`. +const KNBOOT_OPERATION_ALLOWED: u64 = 0x3C5A_33CC; +const KNBOOT_OPERATION_DISALLOWED: u64 = 0x5AA5_CC33; +const KSTATUS_NBOOT_KEY_NOT_AVAILABLE: u64 = 0x5A5A_A5E6; + +const KSTATUS_ROMLDR_DATA_UNDERRUN: u32 = 10109; +const KSTATUS_ROMLDR_JUMP_RETURNED: u32 = 10110; +const KSTATUS_ROMLDR_ROLLBACK_BLOCKED: u32 = 10115; +const KSTATUS_ROMLDR_PENDING_JUMP_COMMAND: u32 = 10119; + +// ROM API status codes +const KSTATUS_ROM_API_BUFFER_SIZE_NOT_ENOUGH: u32 = 10802; +const KSTATUS_ROM_API_INVALID_BUFFER: u32 = 10803; + +// KBoot (KB) status codes (Table 35); KB reuses generic/ROM loader/ROM API status space; these aliases make callsites clearer. +const KSTATUS_KB_SUCCESS: u32 = KSTATUS_SUCCESS; +const KSTATUS_KB_FAIL: u32 = KSTATUS_FAIL; +const KSTATUS_KB_INVALID_ARGUMENT: u32 = KSTATUS_INVALID_ARGUMENT; + +const KSTATUS_KB_ROMLDR_DATA_UNDERRUN: u32 = KSTATUS_ROMLDR_DATA_UNDERRUN; +const KSTATUS_KB_ROMLDR_JUMP_RETURNED: u32 = KSTATUS_ROMLDR_JUMP_RETURNED; +const KSTATUS_KB_ROMLDR_ROLLBACK_BLOCKED: u32 = KSTATUS_ROMLDR_ROLLBACK_BLOCKED; +const KSTATUS_KB_ROMLDR_PENDING_JUMP_COMMAND: u32 = KSTATUS_ROMLDR_PENDING_JUMP_COMMAND; + +const KSTATUS_KB_BUFFER_SIZE_NOT_ENOUGH: u32 = KSTATUS_ROM_API_BUFFER_SIZE_NOT_ENOUGH; +const KSTATUS_KB_INVALID_BUFFER: u32 = KSTATUS_ROM_API_INVALID_BUFFER; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashStatus { + Success, + InvalidArgument, + AlignmentError, + AddressError, + SizeError, + CommandFailure, + UnknownProperty, + EraseKeyError, + RegionExecuteOnly, + CommandNotSupported, + ReadOnlyProperty, + InvalidPropertyValue, + EccError, + CompareError, + InvalidWaitStateCycles, + Unknown(u32), +} + +impl FlashStatus { + pub const fn from_raw(raw: u32) -> Self { + match raw { + KSTATUS_FLASH_SUCCESS => Self::Success, + KSTATUS_FLASH_INVALID_ARGUMENT => Self::InvalidArgument, + KSTATUS_FLASH_ALIGNMENT_ERROR => Self::AlignmentError, + KSTATUS_FLASH_ADDRESS_ERROR => Self::AddressError, + KSTATUS_FLASH_SIZE_ERROR => Self::SizeError, + KSTATUS_FLASH_COMMAND_FAILURE => Self::CommandFailure, + KSTATUS_FLASH_UNKNOWN_PROPERTY => Self::UnknownProperty, + KSTATUS_FLASH_ERASE_KEY_ERROR => Self::EraseKeyError, + KSTATUS_FLASH_REGION_EXECUTE_ONLY => Self::RegionExecuteOnly, + KSTATUS_FLASH_COMMAND_NOT_SUPPORTED => Self::CommandNotSupported, + KSTATUS_FLASH_READ_ONLY_PROPERTY => Self::ReadOnlyProperty, + KSTATUS_FLASH_INVALID_PROPERTY_VALUE => Self::InvalidPropertyValue, + KSTATUS_FLASH_ECC_ERROR => Self::EccError, + KSTATUS_FLASH_COMPARE_ERROR => Self::CompareError, + KSTATUS_FLASH_INVALID_WAIT_STATE_CYCLES => Self::InvalidWaitStateCycles, + other => Self::Unknown(other), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SpiFlashStatus { + Success, + Fail, + Unknown(u32), +} + +impl SpiFlashStatus { + pub const fn from_raw(raw: u32) -> Self { + match raw { + KSTATUS_SPIFLASH_SUCCESS => Self::Success, + KSTATUS_SPIFLASH_FAIL => Self::Fail, + other => Self::Unknown(other), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiStatus { + Success, + Fail, + InvalidArgument, + SequenceExecutionTimeout, + InvalidSequence, + DeviceTimeout, + ProgramFail, + EraseSectorFail, + EraseAllFail, + WaitTimeout, + WriteAlignmentError, + CommandFailure, + SfdpNotFound, + UnsupportedSfdpVersion, + FlashNotFound, + DtrReadDummyProbeFailed, + Unknown(u32), +} + +impl FlexspiStatus { + pub const fn from_raw(raw: u32) -> Self { + match raw { + KSTATUS_FLEXSPI_SUCCESS => Self::Success, + KSTATUS_FLEXSPI_FAIL => Self::Fail, + KSTATUS_FLEXSPI_INVALID_ARGUMENT => Self::InvalidArgument, + KSTATUS_FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT => Self::SequenceExecutionTimeout, + KSTATUS_FLEXSPI_INVALID_SEQUENCE => Self::InvalidSequence, + KSTATUS_FLEXSPI_DEVICE_TIMEOUT => Self::DeviceTimeout, + KSTATUS_FLEXSPINOR_PROGRAM_FAIL => Self::ProgramFail, + KSTATUS_FLEXSPINOR_ERASE_SECTOR_FAIL => Self::EraseSectorFail, + KSTATUS_FLEXSPINOR_ERASE_ALL_FAIL => Self::EraseAllFail, + KSTATUS_FLEXSPINOR_WAIT_TIMEOUT => Self::WaitTimeout, + KSTATUS_FLEXSPINOR_WRITE_ALIGNMENT_ERROR => Self::WriteAlignmentError, + KSTATUS_FLEXSPINOR_COMMAND_FAILURE => Self::CommandFailure, + KSTATUS_FLEXSPINOR_SFDP_NOT_FOUND => Self::SfdpNotFound, + KSTATUS_FLEXSPINOR_UNSUPPORTED_SFDP_VERSION => Self::UnsupportedSfdpVersion, + KSTATUS_FLEXSPINOR_FLASH_NOT_FOUND => Self::FlashNotFound, + KSTATUS_FLEXSPINOR_DTR_READ_DUMMY_PROBE_FAILED => Self::DtrReadDummyProbeFailed, + other => Self::Unknown(other), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum NbootStatus { + Success, + Fail, + InvalidArgument, + OperationAllowed, + OperationDisallowed, + KeyNotAvailable, + Unknown(u64), +} + +impl NbootStatus { + pub const fn from_raw(raw: u64) -> Self { + // The ROM returns a usable 32-bit value; upper 32 bits are likely security related metadata/ fault attack protection, need to mask it out. + let raw = raw & 0xFFFF_FFFF; + match raw { + KSTATUS_NBOOT_SUCCESS => Self::Success, + KSTATUS_NBOOT_FAIL => Self::Fail, + KSTATUS_NBOOT_INVALID_ARGUMENT => Self::InvalidArgument, + KNBOOT_OPERATION_ALLOWED => Self::OperationAllowed, + KNBOOT_OPERATION_DISALLOWED => Self::OperationDisallowed, + KSTATUS_NBOOT_KEY_NOT_AVAILABLE => Self::KeyNotAvailable, + other => Self::Unknown(other), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KbStatus { + Success, + Fail, + InvalidArgument, + RomLdrDataUnderrun, + RomLdrJumpReturned, + RomLdrRollbackBlocked, + RomLdrPendingJumpCommand, + RomApiBufferSizeNotEnough, + RomApiInvalidBuffer, + Unknown(u32), +} + +impl KbStatus { + pub const fn from_raw(raw: u32) -> Self { + match raw { + KSTATUS_KB_SUCCESS => Self::Success, + KSTATUS_KB_FAIL => Self::Fail, + KSTATUS_KB_INVALID_ARGUMENT => Self::InvalidArgument, + KSTATUS_KB_ROMLDR_DATA_UNDERRUN => Self::RomLdrDataUnderrun, + KSTATUS_KB_ROMLDR_JUMP_RETURNED => Self::RomLdrJumpReturned, + KSTATUS_KB_ROMLDR_ROLLBACK_BLOCKED => Self::RomLdrRollbackBlocked, + KSTATUS_KB_ROMLDR_PENDING_JUMP_COMMAND => Self::RomLdrPendingJumpCommand, + KSTATUS_KB_BUFFER_SIZE_NOT_ENOUGH => Self::RomApiBufferSizeNotEnough, + KSTATUS_KB_INVALID_BUFFER => Self::RomApiInvalidBuffer, + other => Self::Unknown(other), + } + } +} + +#[allow(dead_code)] +pub fn map_flash_status_to_boot_error(status: FlashStatus) -> BootError { + match status { + FlashStatus::InvalidArgument + | FlashStatus::AlignmentError + | FlashStatus::AddressError + | FlashStatus::SizeError + | FlashStatus::RegionExecuteOnly + | FlashStatus::ReadOnlyProperty => BootError::MemoryRegion, + FlashStatus::EccError | FlashStatus::CompareError | FlashStatus::CommandFailure => BootError::Integrity, + FlashStatus::EraseKeyError + | FlashStatus::UnknownProperty + | FlashStatus::CommandNotSupported + | FlashStatus::InvalidPropertyValue + | FlashStatus::InvalidWaitStateCycles => BootError::Markers, + _ => BootError::IO, + } +} + +#[allow(dead_code)] +pub fn map_spiflash_status_to_boot_error(status: SpiFlashStatus) -> BootError { + match status { + SpiFlashStatus::Fail => BootError::IO, + _ => BootError::IO, + } +} + +#[allow(dead_code)] +pub fn map_flexspi_status_to_boot_error(status: FlexspiStatus) -> BootError { + match status { + FlexspiStatus::InvalidArgument + | FlexspiStatus::InvalidSequence + | FlexspiStatus::SfdpNotFound + | FlexspiStatus::UnsupportedSfdpVersion => BootError::Markers, + FlexspiStatus::WriteAlignmentError => BootError::MemoryRegion, + FlexspiStatus::ProgramFail + | FlexspiStatus::EraseSectorFail + | FlexspiStatus::EraseAllFail + | FlexspiStatus::CommandFailure + | FlexspiStatus::DtrReadDummyProbeFailed => BootError::Integrity, + FlexspiStatus::Fail + | FlexspiStatus::Success + | FlexspiStatus::SequenceExecutionTimeout + | FlexspiStatus::DeviceTimeout + | FlexspiStatus::WaitTimeout + | FlexspiStatus::FlashNotFound => BootError::IO, + _ => BootError::IO, + } +} + +pub fn map_nboot_status_to_boot_error(status: NbootStatus) -> BootError { + match status { + NbootStatus::OperationDisallowed => BootError::MemoryRegion, + NbootStatus::InvalidArgument => BootError::Markers, + NbootStatus::KeyNotAvailable => BootError::Authenticate, + NbootStatus::Fail => BootError::IO, + _ => BootError::Authenticate, + } +} + +#[allow(dead_code)] +pub fn map_kb_status_to_boot_error(status: KbStatus) -> BootError { + match status { + KbStatus::InvalidArgument | KbStatus::RomApiBufferSizeNotEnough | KbStatus::RomApiInvalidBuffer => { + BootError::Markers + } + KbStatus::Fail => BootError::IO, + KbStatus::RomLdrRollbackBlocked => BootError::Markers, + // Data underrun / pending-jump are usually flow control for streaming loaders, + // but if surfaced as an error, treat as I/O. + KbStatus::RomLdrDataUnderrun | KbStatus::RomLdrJumpReturned | KbStatus::RomLdrPendingJumpCommand => { + BootError::IO + } + _ => BootError::IO, + } +} diff --git a/libs/ec-slimloader-mcxa/src/flash_internal.rs b/libs/ec-slimloader-mcxa/src/flash_internal.rs new file mode 100644 index 00000000..f16e135b --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/flash_internal.rs @@ -0,0 +1,152 @@ +use crate::error::FlashStatus; +use crate::memory::{INTERNAL_FLASH_PAGE_SIZE, INTERNAL_FLASH_SECTOR_SIZE, JOURNAL_SIZE, JOURNAL_START}; +use crate::rom_api::{ + flash_driver, FlashConfig, FlashFfrConfig, FlashModeConfig, FlashRampControlOption, FlashReadDmaccOption, + FlashReadEccOption, FlashReadMarginOption, FlashReadSingleWordConfig, FlashSetReadModeConfig, + FlashSetWriteModeConfig, FLASH_API_ERASE_KEY, +}; +use embedded_storage_async::nor_flash::NorFlash; +use embedded_storage_async::nor_flash::{ErrorType, NorFlashErrorKind, ReadNorFlash}; + +pub struct InternalFlash { + pub cfg: FlashConfig, + initialized: bool, +} + +impl InternalFlash { + pub const fn new() -> Self { + Self { + cfg: FlashConfig { + pflash_block_base: 0, + pflash_total_size: 0, + pflash_block_count: 0, + pflash_page_size: 0, + pflash_sector_size: 0, + ffr_config: FlashFfrConfig { + ffr_block_base: 0, + ffr_total_size: 0, + ffr_page_size: 0, + sector_size: 0, + cfpa_page_version: 0, + cfpa_page_offset: 0, + }, + mode_config: FlashModeConfig::new( + 0, + FlashReadSingleWordConfig::new( + FlashReadEccOption::On, + FlashReadMarginOption::Normal, + FlashReadDmaccOption::Disabled, + ), + FlashSetWriteModeConfig::new(FlashRampControlOption::Reserved, FlashRampControlOption::Reserved), + FlashSetReadModeConfig::new(0, 0, 0), + ), + nboot_ctx: core::ptr::null_mut(), + use_ahb_read: true, + }, + initialized: false, + } + } + + fn ensure_init(&mut self) -> Result<(), NorFlashErrorKind> { + if self.initialized { + return Ok(()); + } + let flash_driver_api = flash_driver(); + let status = flash_driver_api.flash_init(&mut self.cfg); + if status != FlashStatus::Success { + return Err(NorFlashErrorKind::Other); + } + self.initialized = true; + Ok(()) + } +} + +impl ErrorType for InternalFlash { + type Error = NorFlashErrorKind; +} + +impl ReadNorFlash for InternalFlash { + const READ_SIZE: usize = 1; + + async fn read(&mut self, offset: u32, buf: &mut [u8]) -> Result<(), Self::Error> { + self.ensure_init()?; + if offset + buf.len() as u32 > JOURNAL_SIZE { + return Err(NorFlashErrorKind::OutOfBounds); + } + let flash_driver_api = flash_driver(); + let abs = JOURNAL_START + offset; + let status = flash_driver_api.flash_read(&mut self.cfg, abs, buf.as_mut_ptr(), buf.len() as u32); + if status == FlashStatus::Success { + Ok(()) + } else { + Err(NorFlashErrorKind::Other) + } + } + + fn capacity(&self) -> usize { + JOURNAL_SIZE as usize + } +} + +impl NorFlash for InternalFlash { + const WRITE_SIZE: usize = INTERNAL_FLASH_PAGE_SIZE as usize; // use page alignment + const ERASE_SIZE: usize = INTERNAL_FLASH_SECTOR_SIZE as usize; + + async fn write(&mut self, offset: u32, data: &[u8]) -> Result<(), Self::Error> { + self.ensure_init()?; + if offset + data.len() as u32 > JOURNAL_SIZE { + return Err(NorFlashErrorKind::OutOfBounds); + } + if offset % INTERNAL_FLASH_PAGE_SIZE != 0 { + return Err(NorFlashErrorKind::NotAligned); + } + if data.len() > INTERNAL_FLASH_PAGE_SIZE as usize { + return Err(NorFlashErrorKind::OutOfBounds); + } + let flash_driver_api = flash_driver(); + let abs_start = JOURNAL_START + offset; + + // Page was erased prior to this write — fill rest with 0xFF and program. + let mut page_buf = [0xFFu8; INTERNAL_FLASH_PAGE_SIZE as usize]; + page_buf[..data.len()].copy_from_slice(data); //safe as we checked bounds above. + + let status = + flash_driver_api.flash_program_page(&mut self.cfg, abs_start, page_buf.as_ptr(), INTERNAL_FLASH_PAGE_SIZE); + if status != FlashStatus::Success { + return Err(NorFlashErrorKind::Other); + } + + // Verify programmed data via ROM API. + let mut failed_address = 0u32; + let mut failed_data = 0u32; + let status = flash_driver_api.flash_verify_program( + &mut self.cfg, + abs_start, + INTERNAL_FLASH_PAGE_SIZE, + page_buf.as_ptr(), + &mut failed_address, + &mut failed_data, + ); + if status != FlashStatus::Success { + return Err(NorFlashErrorKind::Other); + } + Ok(()) + } + + async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + self.ensure_init()?; + // Round to sector boundaries rather than rejecting unaligned inputs. + let from_aligned = (from / INTERNAL_FLASH_SECTOR_SIZE) * INTERNAL_FLASH_SECTOR_SIZE; + let to_aligned = + ((to + INTERNAL_FLASH_SECTOR_SIZE - 1) / INTERNAL_FLASH_SECTOR_SIZE) * INTERNAL_FLASH_SECTOR_SIZE; + let len = to_aligned - from_aligned; + let flash_driver_api = flash_driver(); + let abs = JOURNAL_START + from_aligned; + let status = flash_driver_api.flash_erase_sector(&mut self.cfg, abs, len, FLASH_API_ERASE_KEY); + if status == FlashStatus::Success { + Ok(()) + } else { + Err(NorFlashErrorKind::Other) + } + } +} diff --git a/libs/ec-slimloader-mcxa/src/header.rs b/libs/ec-slimloader-mcxa/src/header.rs new file mode 100644 index 00000000..833237de --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/header.rs @@ -0,0 +1,89 @@ +use crate::memory::INTERNAL_FLASH_PAGE_SIZE; + +#[repr(C)] +pub struct VectorAndHeaderRaw { + // ARM CortexM vector table interleaved with NXP bootloader fields + pub initial_sp: u32, // 0x00 Stack pointer + pub reset: u32, // 0x04 Reset handler + pub nmi: u32, // 0x08 NMI + pub hard_fault: u32, // 0x0C HardFault + pub mem_manage: u32, // 0x10 MemManageFault + pub bus_fault: u32, // 0x14 BusFault + pub usage_fault: u32, // 0x18 UsageFault + pub secure_fault: u32, // 0x1C SecureFault + pub image_length: u32, // 0x20 Image length (total length - including signature) + pub image_type: u32, // 0x24 Image type: 0x0=plain XIP, 0x4=signed XIP, 0x5=CRC XIP + pub extended_header_offset: u32, // 0x28 Offset to extended header (AHAB container) + pub svc: u32, // 0x2C SVCall + pub debug_mon: u32, // 0x30 DebugMonitor + pub load_address: u32, // 0x34 Load address (image link address) + pub pendsv: u32, // 0x38 PendSV + pub systick: u32, // 0x3C SysTick +} + +impl VectorAndHeaderRaw { + pub const SIZE: usize = core::mem::size_of::(); // 0x40 bytes (16 x u32) +} + +pub struct ImageHeader<'a> { + pub raw: &'a VectorAndHeaderRaw, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum HeaderError { + LengthZero, + LengthTooLarge, + CertOffset, + Type, +} + +impl<'a> ImageHeader<'a> { + pub unsafe fn from_ptr(ptr: *const u8, slot_size: u32) -> Result { + let raw = &*(ptr as *const VectorAndHeaderRaw); + if raw.image_length == 0 { + return Err(HeaderError::LengthZero); + } + if raw.image_length > slot_size { + return Err(HeaderError::LengthTooLarge); + } + if raw.extended_header_offset >= raw.image_length { + return Err(HeaderError::CertOffset); + } + // Enforce signed XIP image type (0x04) for cold boot (lower 6 bits == 0x04) + let img_type_low = (raw.image_type & 0x3F) as u32; + if img_type_low != 0x04 { + return Err(HeaderError::Type); + } + Ok(Self { raw }) + } + + pub fn image_length(&self) -> u32 { + self.raw.image_length + } + pub fn cert_block_offset(&self) -> u32 { + self.raw.extended_header_offset + } + pub fn load_address(&self) -> u32 { + self.raw.load_address + } + pub fn container_header_offset(&self) -> u32 { + self.raw.extended_header_offset + } + pub fn extended_header_offset(&self) -> u32 { + self.raw.extended_header_offset + } + pub fn certificate_offset(&self) -> u32 { + self.cert_block_offset() + } + pub fn manifest_offset(&self) -> u32 { + // assume manifest immediately after certificate block + self.cert_block_offset() + 0 // caller will add certificate size once parsed + } + pub fn aligned_copy_length(&self) -> u32 { + let len = self.image_length(); + ((len + INTERNAL_FLASH_PAGE_SIZE - 1) / INTERNAL_FLASH_PAGE_SIZE) * INTERNAL_FLASH_PAGE_SIZE + } +} + +// TODO: Active image protection (GLBAC/XOM): determine start/end addresses to protect +// after successful verification. Likely protect Slot A region or [image_base .. image_base+image_length]. diff --git a/libs/ec-slimloader-mcxa/src/jump.rs b/libs/ec-slimloader-mcxa/src/jump.rs new file mode 100644 index 00000000..95d1ebbe --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/jump.rs @@ -0,0 +1,95 @@ +use core::arch::asm; +use cortex_m::asm::{dsb, isb}; + +#[cfg(any(feature = "defmt", feature = "log"))] +macro_rules! jump_error { + ($($arg:tt)*) => { + defmt_or_log::error!($($arg)*); + }; +} + +#[cfg(not(any(feature = "defmt", feature = "log")))] +macro_rules! jump_error { + ($($arg:tt)*) => {}; +} + +pub unsafe fn jump_to_image(entry: u32) -> ! { + // entry points to vector table base + let initial_sp = *(entry as *const u32); + let reset = *((entry + 4) as *const u32); + // info!( + // "jump: entry=0x{:08X}, initial_sp=0x{:08X}, reset=0x{:08X}", + // entry, initial_sp, reset + // ); + + // Guards: validate image header fields (Table 204 Nx4x security reference manual) + + let image_len = *((entry + 0x20) as *const u32); + let cert_off = *((entry + 0x28) as *const u32); + + // Basic sanity: image length should be at least a vector table (>= 0x40), + // cert header offset must be 4-byte aligned and within image length. + if image_len < 0x40 || (cert_off & 0x3) != 0 || cert_off >= image_len { + // Invalid header; halt to avoid jumping to a potentially corrupt image. + jump_error!( + "jump: invalid header image_len=0x{:X}, cert_off=0x{:X}", + image_len, + cert_off + ); + loop { + core::hint::spin_loop() + } + } + // (similar to imxrt): disable interrupts & timer + // Disable all maskable interrupts + // info!("jump: disabling interrupts"); + #[cfg(target_arch = "arm")] + asm!("cpsid i", options(nostack, preserves_flags)); + + // Disable SysTick (if previously configured by loader) + const SYST_CSR: *mut u32 = 0xE000E010 as *mut u32; // SysTick Control and Status Register + core::ptr::write_volatile(SYST_CSR, 0); + // info!("jump: SysTick disabled"); + + // Disable NVIC interrupts & clear any pending bits (MCXN556s up to IRQ 155 → 5 * 32 blocks) + const NVIC_ICER_BASE: *mut u32 = 0xE000E180 as *mut u32; // Interrupt Clear/Enable Registers + const NVIC_ICPR_BASE: *mut u32 = 0xE000E280 as *mut u32; // Interrupt Clear/Pending Registers + for i in 0..5 { + core::ptr::write_volatile(NVIC_ICER_BASE.add(i), 0xFFFF_FFFF); + core::ptr::write_volatile(NVIC_ICPR_BASE.add(i), 0xFFFF_FFFF); + } + // info!("jump: NVIC interrupts disabled & pending cleared"); + + // Clear selected system handler pending bits (SecureFault, PendSV) in SHCSR + const SCB_SHCSR: *mut u32 = 0xE000ED24 as *mut u32; // System Handler Control and State Register + // Write-1-to-clear for pending bits is not supported; instead, clear enable bits to avoid servicing. + // Ensure SVC/Debug/PendSV/SysTick not enabled by loader. + core::ptr::write_volatile(SCB_SHCSR, 0); + // info!("jump: SHCSR cleared"); + + // Ensure privileged thread mode & use MSP (clear CONTROL.nPRIV & CONTROL.SPSEL) + #[cfg(target_arch = "arm")] + asm!("msr CONTROL, {0}", in(reg) 0u32, options(nostack, preserves_flags)); + #[cfg(target_arch = "arm")] + asm!("isb", options(nostack, preserves_flags)); + + // Set MSP to application's initial stack pointer + #[cfg(target_arch = "arm")] + asm!("msr MSP, {0}", in(reg) initial_sp, options(nostack, preserves_flags)); + // info!("jump: MSP set to 0x{:08X}", initial_sp); + + // Set VTOR to application's vector table + const SCB_VTOR: *mut u32 = 0xE000ED08 as *mut u32; + core::ptr::write_volatile(SCB_VTOR, entry); + // info!("jump: VTOR set to 0x{:08X}", entry); + + // Data / instruction sync barriers before branch + dsb(); + isb(); + + // Branch to application's reset handler + let reset_fn: extern "C" fn() = core::mem::transmute(reset as usize); + // info!("jump: branching to reset handler 0x{:08X}", reset); + reset_fn(); + loop {} // should not return +} diff --git a/libs/ec-slimloader-mcxa/src/lib.rs b/libs/ec-slimloader-mcxa/src/lib.rs new file mode 100644 index 00000000..69657857 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/lib.rs @@ -0,0 +1,319 @@ +#![no_std] + +mod flash_internal; + +#[cfg(not(feature = "internal-only"))] +use crate::rom_api::{flash_driver, run_bootloader_uart, FLASH_API_ERASE_KEY}; + +pub mod certificate; +pub mod error; +pub mod header; +pub mod jump; +pub mod lifecycle; +pub mod memory; +pub mod rom_api; +pub mod verification; + +use ec_slimloader::{Board, BootStatePolicy}; +use ec_slimloader_state::flash::FlashJournal; +use ec_slimloader_state::state::{Slot, State, Status}; +use embassy_mcxa::clocks::config::{ + CoreSleep, Div8, FircConfig, FircFreqSel, FlashSleep, MainClockConfig, MainClockSource, VddDriveStrength, VddLevel, +}; +use embassy_mcxa::clocks::PoweredClock; +use embassy_mcxa::{peripherals, Peri}; +use embedded_storage_async::nor_flash::NorFlash; +use flash_internal::InternalFlash; + +pub use embassy_mcxa::sgi::hash::{BlockingHasher, DmaHasher, HashMode, HashOptions, HashSize, StreamingHasher}; +pub use embassy_mcxa::sgi::{Async, Blocking, InterruptHandler, SgiError, SetupError as SgiSetupError, Sgi}; +pub use embassy_mcxa::sgi; +pub use embassy_mcxa::sgi::hash; + +#[cfg(any(feature = "defmt", feature = "log"))] +macro_rules! mcxa_error { + ($($arg:tt)*) => { + defmt_or_log::error!($($arg)*); + }; +} + +#[cfg(not(any(feature = "defmt", feature = "log")))] +macro_rules! mcxa_error { + ($($arg:tt)*) => {}; +} + +pub struct Config; + +impl BootStatePolicy for Config { + fn default_state() -> State { + #[cfg(feature = "internal-only")] + { + State::new(Status::Initial, Slot::S0, Slot::S0) + } + + #[cfg(not(feature = "internal-only"))] + { + State::new(Status::Initial, Slot::S0, Slot::S1) + } + } + + fn is_valid_state(state: &State) -> bool { + let target: u8 = state.target().into(); + let backup: u8 = state.backup().into(); + + #[cfg(feature = "internal-only")] + { + target == 0 && backup == 0 + } + + #[cfg(not(feature = "internal-only"))] + { + (target == 0 && backup == 1) || (target == 1 && backup == 0) + } + } +} + +pub struct McxaBoard { + journal: FlashJournal, + sgi: Peri<'static, peripherals::SGI0>, +} + +impl Board for McxaBoard { + type Config = Config; + + async fn init(_config: Self::Config) -> Self { + let mut bl_cfg = embassy_mcxa::config::Config::default(); + + // Enable 192M FIRC, NOTE that this following configuration is intended for MCXA5xx family of MCUs. + // Feature-gate as needed for other family of MCXA MCUs. + + let mut fcfg = FircConfig::default(); + fcfg.frequency = FircFreqSel::Mhz192; + fcfg.power = PoweredClock::NormalEnabledDeepSleepDisabled; + fcfg.fro_hf_enabled = true; + fcfg.clk_hf_fundamental_enabled = false; + fcfg.fro_hf_div = None; // Not sure what we would need the hf_div clock for here. + bl_cfg.clock_cfg.firc = Some(fcfg); + + // Enable 12M osc to use as ostimer clock + bl_cfg.clock_cfg.sirc.fro_12m_enabled = true; + bl_cfg.clock_cfg.sirc.fro_lf_div = None; + bl_cfg.clock_cfg.sirc.power = PoweredClock::AlwaysEnabled; + + // Disable 16K osc + bl_cfg.clock_cfg.fro16k = None; + + // Disable external osc + bl_cfg.clock_cfg.sosc = None; + + // Disable PLL + bl_cfg.clock_cfg.spll = None; + + // Feed core from 192M osc + bl_cfg.clock_cfg.main_clock = MainClockConfig { + source: MainClockSource::FircHfRoot, + power: PoweredClock::NormalEnabledDeepSleepDisabled, + ahb_clk_div: Div8::no_div(), + }; + + // Set the core in high power active mode + bl_cfg.clock_cfg.vdd_power.active_mode.level = VddLevel::OverDriveMode; + bl_cfg.clock_cfg.vdd_power.active_mode.drive = VddDriveStrength::Normal; + // Set the core in low power sleep mode + bl_cfg.clock_cfg.vdd_power.low_power_mode.level = VddLevel::MidDriveMode; + bl_cfg.clock_cfg.vdd_power.low_power_mode.drive = VddDriveStrength::Low { enable_bandgap: false }; + + // Set "deep sleep" mode + bl_cfg.clock_cfg.vdd_power.core_sleep = CoreSleep::DeepSleep; + + // Set flash doze, allowing internal flash clocks to be gated on sleep + bl_cfg.clock_cfg.vdd_power.flash_sleep = FlashSleep::FlashDoze; + + let p = embassy_mcxa::init(bl_cfg); + + let flash = InternalFlash::new(); + let journal = match FlashJournal::new::(flash).await { + Ok(journal) => journal, + Err(_) => { + mcxa_error!("Critical: failed to initialize flash journal"); + loop { + cortex_m::asm::wfi(); + } + } + }; + + Self { journal, sgi: p.SGI0 } + } + + fn journal(&mut self) -> &mut FlashJournal { + &mut self.journal + } + + #[cfg(feature = "internal-only")] + async fn check_and_boot(&mut self, slot: &Slot) -> ec_slimloader::BootError { + let slot_i: u8 = (*slot).into(); + let (base_addr, slot_size) = match slot_i { + 0 => (memory::SLOT_A_START, memory::SLOT_A_SIZE), + _ => return ec_slimloader::BootError::SlotUnknown, + }; + + let base = base_addr as *const u8; + let image_header = match unsafe { header::ImageHeader::from_ptr(base, slot_size) } { + Ok(header) => header, + Err(_) => return ec_slimloader::BootError::Markers, + }; + + let image_len = image_header.image_length(); + let cert_offset = image_header.cert_block_offset(); + if image_len < 0x40 || image_len > slot_size || (cert_offset & 0x3) != 0 || cert_offset >= image_len { + return ec_slimloader::BootError::Markers; + } + + match verification::verify_authenticity(self.sgi.reborrow(), base) { + Ok(()) => unsafe { + jump::jump_to_image(base_addr); + }, + Err(error) => error, + } + } + + #[cfg(not(feature = "internal-only"))] + async fn check_and_boot(&mut self, slot: &Slot) -> ec_slimloader::BootError { + let slot_i: u8 = (*slot).into(); + match slot_i { + 0 => { + let base = memory::SLOT_A_START as *const u8; + let image_header = match unsafe { header::ImageHeader::from_ptr(base, memory::SLOT_A_SIZE) } { + Ok(header) => header, + Err(_) => return ec_slimloader::BootError::Markers, + }; + + let image_len = image_header.image_length(); + let cert_offset = image_header.cert_block_offset(); + if image_len < 0x40 + || image_len > memory::SLOT_A_SIZE + || (cert_offset & 0x3) != 0 + || cert_offset >= image_len + { + return ec_slimloader::BootError::Markers; + } + + match verification::verify_authenticity(self.sgi.reborrow(), base) { + Ok(()) => unsafe { + jump::jump_to_image(memory::SLOT_A_START); + }, + Err(error) => error, + } + } + 1 => { + let base_ext = memory::SLOT_B_START as *const u8; + let image_header_ext = match unsafe { header::ImageHeader::from_ptr(base_ext, memory::SLOT_B_SIZE) } { + Ok(header) => header, + Err(_) => return ec_slimloader::BootError::Markers, + }; + + let image_len_ext = image_header_ext.image_length(); + let cert_offset_ext = image_header_ext.cert_block_offset(); + if image_len_ext < 0x40 + || image_len_ext > memory::SLOT_B_SIZE + || (cert_offset_ext & 0x3) != 0 + || cert_offset_ext >= image_len_ext + { + return ec_slimloader::BootError::Markers; + } + + // This verify call assumes memory mapped flash access for Slot B, which is true for the internal flash part built into MCXA, + // but may not be true for all future use cases of Slot B, so may need to be revisited if we want to support more flexible loading scenarios in the future. + if verification::verify_authenticity(self.sgi.reborrow(), base_ext).is_err() { + // unsafe { run_bootloader_uart() } // TODO: well, what should we actually do? entering ISP isn't necessarily best idea. + return ec_slimloader::BootError::Authenticate; + } + + let mut internal = InternalFlash::new(); // Will use this to access the flash config. + let flash = flash_driver(); + let flash_init_status = flash.flash_init(&mut internal.cfg); + if flash_init_status != crate::error::FlashStatus::Success { + return crate::error::map_flash_status_to_boot_error(flash_init_status); + } + + // Erase the whole destination slot so data from a previous larger image cannot survive + // beyond the end of the newly copied image. + // TODO: eventually other persistent states may need to be preserved across updates, in which case we would want to be more surgical with our erases. + // For now just wipe the whole slot. + let erase_len = memory::SLOT_A_SIZE; + let erase_status = + flash.flash_erase_sector(&mut internal.cfg, memory::SLOT_A_START, erase_len, FLASH_API_ERASE_KEY); + if erase_status != crate::error::FlashStatus::Success { + return crate::error::map_flash_status_to_boot_error(erase_status); + } + + let aligned_len = image_header_ext.aligned_copy_length(); + let mut offset = 0u32; + while offset < aligned_len { + let remaining_image_len = image_len_ext.saturating_sub(offset) as usize; + let chunk_len = remaining_image_len.min(memory::INTERNAL_FLASH_PAGE_SIZE as usize); + let mut page_buf = [0xffu8; memory::INTERNAL_FLASH_PAGE_SIZE as usize]; + // only used for bounds checking. + let _src_end = match offset.checked_add(chunk_len as u32) { + Some(end) if end <= memory::SLOT_B_SIZE => end, + _ => return ec_slimloader::BootError::Markers, + }; + let src = unsafe { + // The BIG assumption here is that the external flash is memory-mapped and can be read via normal pointers. + // This is true for the Internal flash part built into MCXA, but may not be true for all future use cases of Slot B, + // so may need to be revisited if we want to support more flexible loading scenarios in the future. + // TODO: Ideally we would have a buffer of memory::INTERNAL_FLASH_PAGE_SIZE bytes and read directly into that via FLEX SPI flash ROM API. + core::slice::from_raw_parts((memory::SLOT_B_START + offset) as *const u8, chunk_len) + }; + let dst = match page_buf.get_mut(..chunk_len) { + Some(dst) => dst, + None => return ec_slimloader::BootError::Markers, + }; + dst.copy_from_slice(src); + + let program_status = flash.flash_program_page( + &mut internal.cfg, + memory::SLOT_A_START + offset, + page_buf.as_ptr(), + memory::INTERNAL_FLASH_PAGE_SIZE, + ); + if program_status != crate::error::FlashStatus::Success { + return crate::error::map_flash_status_to_boot_error(program_status); + } + + offset += memory::INTERNAL_FLASH_PAGE_SIZE; + } + + let new_state = State::new(Status::Initial, Slot::S0, Slot::S1); + if self.journal.set::(&new_state).await.is_err() { + run_bootloader_uart(); + // If we can't update the journal, we have no idea what state the bootloader will be in on next boot, + // so just enter ISP to be safe. + } + + ec_slimloader::BootError::SlotRetryRequired + } + _ => ec_slimloader::BootError::SlotUnknown, + } + } + + fn abort(&mut self) -> ! { + loop { + cortex_m::asm::wfi(); + } + } + + fn arm_mcu_reset(&mut self) -> ! { + const AIRCR: *mut u32 = 0xE000ED0C as *mut u32; + const AIRCR_VECTKEY: u32 = 0x5FA << 16; + const AIRCR_SYSRESETREQ: u32 = 1 << 2; + + unsafe { + core::ptr::write_volatile(AIRCR, AIRCR_VECTKEY | AIRCR_SYSRESETREQ); + } + + loop { + cortex_m::asm::wfi(); + } + } +} diff --git a/libs/ec-slimloader-mcxa/src/lifecycle.rs b/libs/ec-slimloader-mcxa/src/lifecycle.rs new file mode 100644 index 00000000..bfef34f9 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/lifecycle.rs @@ -0,0 +1,618 @@ +use core::mem; + +use crate::rom_api::{NbootLifecycleDiscriminator, NbootLifecycleState, NbootRootKeyRevocation, NbootRootKeyUsage}; + +// MCXA configuration flash layout (CFG vs SCRATCH) +// +// NOTE: +// - Reads should use the CFG area. +// - Updates should be staged into the SCRATCH area (then committed by the proper +// ROM/programming flow; this module only provides addresses and helpers). +// +// CFG (read): +// CFPA 0x0100_0000 - 0x0100_01FF +// CMPA 0x0100_0200 - 0x0100_03FF +// CMPA customer-defined 0x0100_0400 - 0x0100_17FF +// +// SCRATCH (write staging): +// CFPA 0x0100_2000 - 0x0100_21FF +// CMPA 0x0100_2200 - 0x0100_23FF +// CMPA customer-defined 0x0100_2400 - 0x0100_37FF + +// CFG bases (use for reading) +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IFRConfigAreaBase { + Cfpa = 0x0100_0000, + Cmpa = 0x0100_0200, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IFRPage { + Cfpa, + Cmpa, + CmpaCustDefined, + CmpaAll, +} + +impl IFRPage { + #[inline(always)] + pub const fn start_offset(self) -> u32 { + match self { + Self::Cfpa => 0x0000, + Self::Cmpa => 0x0200, + Self::CmpaCustDefined => 0x0400, + Self::CmpaAll => 0x0200, // start of CMPA, used for operations that cover the entire CMPA including customer-defined area. + } + } + + #[inline(always)] + pub const fn end_offset_inclusive(self) -> u32 { + match self { + Self::Cfpa => 0x01FF, + Self::Cmpa => 0x03FF, + Self::CmpaCustDefined => 0x17FF, + Self::CmpaAll => 0x17FF, // end of CMPA customer-defined area, used for operations that cover the entire CMPA including customer-defined area. + } + } + + #[inline(always)] + pub const fn byte_len(self) -> usize { + (self.end_offset_inclusive() - self.start_offset() + 1) as usize + } +} + +// RoTKH locations in CMPA (absolute addresses provided): +// - ROTKH @ 0x0100_0260 (i.e. IFRConfigAreaBase::Cmpa + 0x60) +// - PQC_ROTKH @ 0x0100_02C0 (i.e. IFRConfigAreaBase::Cmpa + 0xC0) +// CMPA secure-boot related fields (MCXA): +// Base is 0x0100_0200 and offsets below come from the reference table. +// Unused fields from the reference table are omitted here but can be added as needed for future features. +// const CMPA_BOOT_LED_STATUS: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x0008; // 0x0100_0208 +// const CMPA_BOOT_TIMERS: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x000C; // 0x0100_020C + +#[inline(always)] +pub fn load_cmpa_boot_cfg0() -> u32 { + const CMPA_BOOT_CFG0: u32 = IFRConfigAreaBase::Cmpa as u32; // 0x0100_0200 + unsafe { core::ptr::read_volatile(CMPA_BOOT_CFG0 as *const u32) } +} + +#[inline(always)] +pub fn load_cmpa_boot_cfg1() -> u32 { + const CMPA_BOOT_CFG1: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x0004; // 0x0100_0204 + unsafe { core::ptr::read_volatile(CMPA_BOOT_CFG1 as *const u32) } +} + +pub fn is_cmpa_erased() -> bool { + // NOTE: ifr_verify_erase_page is NOT a read-only check; it erases then verifies (destructive). + // Therefore it cannot be used to check erased state of CFG area (protected by ROM). + // read_volatile is the correct approach for a non-destructive erased check. + let base = IFRConfigAreaBase::Cmpa as u32; + let word_count = IFRPage::Cmpa.byte_len() / core::mem::size_of::(); + for i in 0..word_count { + let addr = base + (i as u32 * 4); + let val = unsafe { core::ptr::read_volatile(addr as *const u32) }; + if val != ERASED_WORD { + return false; + } + } + true +} + +#[inline(always)] +fn load_cmpa_header_marker() -> u16 { + let word = load_cmpa_boot_cfg0(); + (word >> 16) as u16 +} + +#[inline(always)] +pub fn cmpa_header_marker_is_valid() -> bool { + // CMPA BOOT_CFG0 header marker semantics (MCXA): + // Marker should be set to 0x5963. After this header is set, all non-zero values will take effect; + // leaving all settings at 0xFF will cause undefined behavior. It is recommended to set all values + // to 0x00 before setting the CMPA header marker. + // + // Layout assumed consistent with CFPA header marker usage: marker stored in bits [31:16]. + const CMPA_HEADER_MARKER: u16 = 0x5963; + load_cmpa_header_marker() == CMPA_HEADER_MARKER +} + +// The following CMPA fields are defined in the reference table but not yet used in this module; they can be added as needed for future features: +// const CMPA_ERR_LOG_ADDR: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x005C; // 0x0100_025C +// const CMPA_CUST_MK_SK_KEY_BLOB_START: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x0090; // 0x0100_0290 +// const CMPA_CUST_MK_SK_KEY_BLOB_WORDS: usize = 12; + +#[inline(always)] +fn load_cmpa_secure_boot_cfg() -> u32 { + const CMPA_SECURE_BOOT_CFG: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x0050; // 0x0100_0250 + unsafe { core::ptr::read_volatile(CMPA_SECURE_BOOT_CFG as *const u32) } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecureBootLevel { + AllAllowed = 0, // b00 + CrcOrSigned = 1, // b01 + SignedOnly = 2, // b10 (CMAC or ECDSA) + EcdsaMldsaOnly = 3, // b11 +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CnsaLevel { + NotEnforced = 0, // b00 (non-CNSA or no enforcement) + CnsaOne = 1, // b01: CNSA1.0 (ECDSA p384 and SHA-384, AES-256) + CnsaTwo = 2, // b10 or b11 (hybrid PQC with ECDSA-384 and MLDSA-87, SHA-384, ML-KEM, AES-256) +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LpWakePolicy { + FullAuthentication = 0, // b00 (same as normal secure boot, applies to low-power wake too) + CrcOnly = 1, // b01 (CRC check only for LP wake, full auth for normal boot) + Jump = 2, // b10 (jump to CFPA LP wake address without authentication) + Cmac = 3, // b11 (CMAC auth for LP wake, full hybrid auth for normal boot) +} +/// Helper function to load the secure boot enforcement level from CMPA and interpret it according to the reference table for MCXA. +pub fn secure_boot_level() -> SecureBootLevel { + // CMPA.SECURE_BOOT_CFG: SEC_BOOT_EN is a 2-bit field in bits [1:0]. + match cmpa_secure_boot_cfg().sec_boot_en { + 0 => SecureBootLevel::AllAllowed, + 1 => SecureBootLevel::CrcOrSigned, + 2 => SecureBootLevel::SignedOnly, + 3 => SecureBootLevel::EcdsaMldsaOnly, + _ => SecureBootLevel::AllAllowed, // treat invalid values as most permissive that way will be caught by policy validation. + } +} +/// Helper function that returns true only if secure boot is strictest with hybrid ECDSA+MLDSA AND IFR region not left in an unprovisioned/erased state that could cause undefined behavior. +pub fn secure_boot_enforced() -> bool { + // If CMPA is erased/unprovisioned, SEC_BOOT_EN bits will read back as 0b11 (all ones) + // and would incorrectly look like "enforced". Treat erased CMPA as "not enforced". + !is_cmpa_erased() && cmpa_header_marker_is_valid() && matches!(secure_boot_level(), SecureBootLevel::EcdsaMldsaOnly) +} + +/// Helper function that returns true if CNSA2.0 is enfocred via CMPA SEC_BOOT_CFG.ENF_CNSA field, and false if not enforced or if CMPA is erased/unprovisioned. +pub fn cnsa_enforced() -> bool { + let cnsa_level = match cmpa_secure_boot_cfg().enf_cnsa { + 0 => CnsaLevel::NotEnforced, + 1 => CnsaLevel::CnsaOne, + 2 | 3 => CnsaLevel::CnsaTwo, + _ => CnsaLevel::NotEnforced, + }; + !is_cmpa_erased() && cmpa_header_marker_is_valid() && cnsa_level == CnsaLevel::CnsaTwo +} + +/// Helper function that returns true if fast boot is enabled via CMPA SEC_BOOT_CFG.FAST_BOOT_EN field, and false if disabled or if CMPA is erased/unprovisioned. +pub fn fast_boot_enabled() -> bool { + // Fast boot is enabled when FAST_BOOT_EN field is 0b00, and disabled otherwise (full auth flow required) + !is_cmpa_erased() && cmpa_header_marker_is_valid() && cmpa_secure_boot_cfg().fast_boot_en == 0 +} + +/// Helper function that returns true if low-power wake authentication is enforced via CMPA SEC_BOOT_CFG.LP_SEC_BOOT field, and false if not enforced or if CMPA is erased/unprovisioned. +pub fn low_power_authentication_enforced() -> bool { + let lp_wake_policy = match cmpa_secure_boot_cfg().lp_sec_boot { + 0 => LpWakePolicy::FullAuthentication, + 1 => LpWakePolicy::CrcOnly, + 2 => LpWakePolicy::Jump, + 3 => LpWakePolicy::Cmac, + _ => LpWakePolicy::Cmac, + }; + !is_cmpa_erased() && cmpa_header_marker_is_valid() && lp_wake_policy == LpWakePolicy::FullAuthentication +} + +// CMPA.SECURE_BOOT_CFG decoder (MCXA) +// +// 2-bit fields, with 1 bit at bit 2 and bit 5: +// - [1:0] SEC_BOOT_EN +// - [2] (reserved) +// - [4:3] LP_SEC_BOOT +// - [5] (reserved) +// - [7:6] DICE_CSR_KEY_TYPE +// - [9:8] ENF_CNSA +// - [11:10] ENF_TZM_PRESET +// - [13:12] FAST_BOOT_EN +// - [15:14] ACTIVE_IMG_PROT +// - [17:16] FIPS_SHA_STEN +// - [19:18] FIPS_AES_STEN +// - [21:20] FIPS_ECDSA_STEN +// - [23:22] FIPS_DRBG_STEN +// - [25:24] FIPS_CMAC_STEN +// - [27:26] FIPS_KDF_STEN +// - [29:28] Reserved (2-bit) +// - [31:30] DIS_NXP_FW + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct CmpaSecureBootCfgDecode { + raw: u32, + sec_boot_en: u8, + lp_sec_boot: u8, + dice_csr_key_type: u8, + enf_cnsa: u8, + enf_tzm_preset: u8, + fast_boot_en: u8, + active_img_prot: u8, + fips_sha_sten: u8, + fips_aes_sten: u8, + fips_ecdsa_sten: u8, + fips_drbg_sten: u8, + fips_cmac_sten: u8, + fips_kdf_sten: u8, + dis_nxp_fw: u8, +} + +#[inline(always)] +fn cmpa_secure_boot_cfg() -> CmpaSecureBootCfgDecode { + let raw = load_cmpa_secure_boot_cfg(); + + CmpaSecureBootCfgDecode { + raw, + sec_boot_en: ((raw >> 0) & 0x3) as u8, + // bit 2 is a hole + lp_sec_boot: ((raw >> 3) & 0x3) as u8, + // bit 5 is a hole + dice_csr_key_type: ((raw >> 6) & 0x3) as u8, + enf_cnsa: ((raw >> 8) & 0x3) as u8, + enf_tzm_preset: ((raw >> 10) & 0x3) as u8, + fast_boot_en: ((raw >> 12) & 0x3) as u8, + active_img_prot: ((raw >> 14) & 0x3) as u8, + fips_sha_sten: ((raw >> 16) & 0x3) as u8, + fips_aes_sten: ((raw >> 18) & 0x3) as u8, + fips_ecdsa_sten: ((raw >> 20) & 0x3) as u8, + fips_drbg_sten: ((raw >> 22) & 0x3) as u8, + fips_cmac_sten: ((raw >> 24) & 0x3) as u8, + fips_kdf_sten: ((raw >> 26) & 0x3) as u8, + // bits 28-29 are reserved + dis_nxp_fw: ((raw >> 30) & 0x3) as u8, + } +} + +// Additional CMPA constants +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CmpaUpdateConfigData { + BootCfg0, + BootCfg1, + SecureBootCfg, + RotkUsage, + SblStartAddr, + Rotkh, + PqcRotkh, +} + +impl CmpaUpdateConfigData { + #[inline(always)] + pub const fn start(self) -> u32 { + match self { + Self::BootCfg0 => IFRConfigAreaBase::Cmpa as u32, + Self::BootCfg1 => IFRConfigAreaBase::Cmpa as u32 + 0x04, + Self::SecureBootCfg => IFRConfigAreaBase::Cmpa as u32 + 0x50, + Self::RotkUsage => IFRConfigAreaBase::Cmpa as u32 + 0x54, + Self::SblStartAddr => IFRConfigAreaBase::Cmpa as u32 + 0x58, + Self::Rotkh => IFRConfigAreaBase::Cmpa as u32 + 0x60, + Self::PqcRotkh => IFRConfigAreaBase::Cmpa as u32 + 0xC0, + } + } + + #[inline(always)] + pub const fn byte_len(self) -> usize { + const RKTH_WORDS: usize = 12; + match self { + Self::BootCfg0 | Self::BootCfg1 | Self::SecureBootCfg | Self::RotkUsage | Self::SblStartAddr => { + mem::size_of::() + } + Self::Rotkh | Self::PqcRotkh => RKTH_WORDS * mem::size_of::(), + } + } + + #[inline(always)] + pub const fn word_len(self) -> usize { + self.byte_len() / mem::size_of::() + } + + #[inline(always)] + pub const fn byte_offset(self) -> usize { + match self { + Self::BootCfg0 => 0x00, + Self::BootCfg1 => 0x04, + Self::SecureBootCfg => 0x50, + Self::RotkUsage => 0x54, + Self::SblStartAddr => 0x58, + Self::Rotkh => 0x60, + Self::PqcRotkh => 0xC0, + } + } + + #[inline(always)] + pub const fn byte_range(self) -> core::ops::Range { + let start = self.byte_offset(); + start..(start + self.byte_len()) + } +} + +/// Load 48 words (384 bits) of ECDSA RoTKH from CMPA, returning None if: CMPA appears corrupt/partially programmed (invalid header marker but not erased/ partially valid CMPA). +/// Current provisioning uses SHA-512, but retains the leftmost 48 bytes (384 bits). +pub fn load_rotkh_from_cmpa() -> Option<[u32; CmpaUpdateConfigData::Rotkh.word_len()]> { + let region = CmpaUpdateConfigData::Rotkh; + if !cmpa_header_marker_is_valid() { + // If secure boot is not enforced, CMPA may be left unprovisioned (invalid header). + // Still allow reading the words so higher-level logic can use the image RKTH as the + // source of truth while warning on mismatch. + // Note: an erased CMPA (all 0xFF) will decode SEC_BOOT_EN as 0b11, so treat "erased" + // as unprovisioned even if `secure_boot_enforced()` appears true. + if secure_boot_enforced() && !is_cmpa_erased() { + return None; + } + } + let mut buf = [0u32; CmpaUpdateConfigData::Rotkh.word_len()]; + for i in 0..region.word_len() { + let addr = region.start() + (i as u32 * 4); + buf[i] = unsafe { core::ptr::read_volatile(addr as *const u32) }; + } + Some(buf) +} + +/// Load 48 words (384 bits) of ML-DSA RoTKH from CMPA, returning None if: CMPA appears corrupt/partially programmed (invalid header marker but not erased/ partially valid CMPA). +/// Current provisioning uses SHA-512, but retains the leftmost 48 bytes (384 bits). +pub fn load_pqc_rotkh_from_cmpa() -> Option<[u32; CmpaUpdateConfigData::PqcRotkh.word_len()]> { + let region = CmpaUpdateConfigData::PqcRotkh; + // 384 bits ML-DSA-87 root key hash, left padded to 48 bytes like the ECDSA ROTKH + if !cmpa_header_marker_is_valid() { + if secure_boot_enforced() && !is_cmpa_erased() { + return None; + } + } + let mut buf = [0u32; CmpaUpdateConfigData::PqcRotkh.word_len()]; + for i in 0..region.word_len() { + let addr = region.start() + (i as u32 * 4); + buf[i] = unsafe { core::ptr::read_volatile(addr as *const u32) }; + } + Some(buf) +} + +// CFPA offsets (MCXA) — CFG region @ 0x0100_0000 +// +// 0x00 UPD_TYPE +// 0x04 UPD_PARAM0 +// 0x08 UPD_PARAM1 +// 0x0C UPD_PARAM2 +// 0x10 Header word (marker + INV_LC + LC) +// 0x14 CFPA_PAGE_VERSION +// 0x18 IMAGE_KEY_REVOKE +// 0x1C DBG_REVOKE_VU +// 0x20.. FW version words +// 0x40 ROTK_REVOKE +// 0x50 ERR_AUTH_FAIL_COUNT +// 0x54 ERR_ITRC_COUNT + +// The following CFPA fields are documented here for reference and can be localized when +// readers/writers are added for them: +// const CFPA_PAGE_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0014; +// const CFPA_DBG_REVOKE_VU: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x001C; +// const CFPA_EE0_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0020; +// const CFPA_EE1_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0024; +// const CFPA_EE2_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0028; +// const CFPA_EE3_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x002C; +// const CFPA_RECOVERY_SB3_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0034; +// const CFPA_UPDATE_SB3_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0038; +// const CFPA_LP_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x003C; + +// Additional CMPA constants + +#[inline(always)] +fn cfpa_header_word_is_valid(header: u32) -> bool { + const CFPA_HEADER_MARKER: u16 = 0x9635; + let marker = (header >> 16) as u16; + if marker != CFPA_HEADER_MARKER { + return false; + } + + let lifecycle = (header & 0xFF) as u8; + let inv_lifecycle = ((header >> 8) & 0xFF) as u8; + inv_lifecycle == (!lifecycle) +} + +/// Load CFPA header word and check validity, returning None if header is invalid (e.g. incorrect marker, which could indicate unprovisioned/partially provisioned state or corruption). This is used as a prerequisite check for other CFPA fields since the header validity is an indicator of whether the CFPA contents can be trusted. +#[inline(always)] +pub fn load_cfpa_header_word() -> Option { + const CFPA_HEADER: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0010; + let header = unsafe { core::ptr::read_volatile(CFPA_HEADER as *const u32) }; + if !cfpa_header_word_is_valid(header) { + return None; + } + + Some(header) +} + +// Any field reading back this value should be treated as "not provisioned" rather than a real configuration. +const ERASED_WORD: u32 = 0xFFFF_FFFF; + +#[inline(always)] +fn load_cfpa_word(address: u32) -> Option { + load_cfpa_header_word()?; + Some(unsafe { core::ptr::read_volatile(address as *const u32) }) +} + +/// Load lifecycle state function: reads the image key revocation word from CFPA. +pub fn load_image_key_revocation_from_cfpa() -> Option { + const CFPA_IMAGE_KEY_REVOKE: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0018; + let word = load_cfpa_word(CFPA_IMAGE_KEY_REVOKE)?; + if word == ERASED_WORD { + return None; + } // Erased state means don't trust. + Some(word) +} + +#[inline(always)] +fn load_cfpa_rotk_revoke_word() -> Option { + const CFPA_ROTK_REVOKE: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0040; + let word = load_cfpa_word(CFPA_ROTK_REVOKE)?; + if word == ERASED_WORD { + return None; + } + Some(word) +} + +/// Load lifecycle state function: reads the root key revocation words from CFPA. Returns a [NbootRootKeyRevocation; 4] array representing the revocation state of each root key, +/// or None if the CFPA header is invalid or the ROTK_REVOKE word is erased (indicating unprovisioned/partially provisioned state that should not be trusted). +pub fn load_root_key_revocation_from_cfpa() -> Option<[NbootRootKeyRevocation; 4]> { + let word = load_cfpa_rotk_revoke_word()?; + Some(root_key_revocation_from_rotk_revoke_word(word)) +} + +// CFPA ROTK_REVOKE (word @ 0x40) bit layout (per user-provided breakdown): +// +// 31:30 ISP_ACTIVE_IMG +// 29 DICE_UPD_ALIAS_CERT +// 28 DICE_UPD_ALIAS_KEY +// 27:8 Reserved +// 7:6 RoTK3_EN (2 bits) +// 5:4 RoTK2_EN (2 bits) +// 3:2 RoTK1_EN (2 bits) +// 1:0 RoTK0_EN (2 bits) + +#[inline(always)] +fn rotk_en_fields_from_rotk_revoke_word(word: u32) -> [u8; 4] { + [ + ((word >> 0) & 0x3) as u8, + ((word >> 2) & 0x3) as u8, + ((word >> 4) & 0x3) as u8, + ((word >> 6) & 0x3) as u8, + ] +} + +#[inline(always)] +fn root_key_revocation_from_rotk_revoke_word(word: u32) -> [NbootRootKeyRevocation; 4] { + // NBOOT `soc_rootKeyRevocation[]` uses a per-key revoke/enable constant. The CFPA ROTK_REVOKE word encodes the revocation state for each root key in 2 bits, where + // 0b00/0b01 => enabled (not revoked) + // 0b10/0b11 => revoked + let mut revocation = [NbootRootKeyRevocation::Enabled; 4]; + for (i, en2) in rotk_en_fields_from_rotk_revoke_word(word).iter().copied().enumerate() { + revocation[i] = match en2 & 0x3 { + 0 | 1 => NbootRootKeyRevocation::Enabled, + 2 | 3 => NbootRootKeyRevocation::Revoked, + _ => NbootRootKeyRevocation::Enabled, + }; + } + revocation +} + +/// The following three functions load DICE and ISP configuration bits from the CFPA ROTK_REVOKE word. These functions return None if the CFPA header or ROTK_REVOKE word is invalid/unprovisioned, and otherwise return the decoded bool as option. +#[inline(always)] +pub fn load_dice_upd_alias_key_from_cfpa() -> Option { + let word = load_cfpa_rotk_revoke_word()?; + Some(((word >> 28) & 1) != 0) +} + +#[inline(always)] +pub fn load_dice_upd_alias_cert_from_cfpa() -> Option { + let word = load_cfpa_rotk_revoke_word()?; + Some(((word >> 29) & 1) != 0) +} + +#[inline(always)] +pub fn load_isp_active_img_from_cfpa() -> Option { + let word = load_cfpa_rotk_revoke_word()?; + Some(((word >> 30) & 0x3) as u8) +} + +/// Load firmware version from CFPA EE0_FW_VERSION word. Returns None if CFPA header is invalid or the EE0_FW_VERSION word is erased (indicating unprovisioned/partially provisioned state that should not be trusted). +pub fn load_firmware_version_from_cfpa() -> Option { + const CFPA_EE0_FW_VERSION: u32 = IFRConfigAreaBase::Cfpa as u32 + 0x0020; + // Use the EE0 firmware version slot so verification matches the image version field we + // expect to advance for the active execution environment. + let word = load_cfpa_word(CFPA_EE0_FW_VERSION)?; + if word == ERASED_WORD { + return None; + } + Some(word) +} + +/// Load lifecycle state from CFPA header word: returns the decoded lifecycle state if the header is valid, or None if the header is invalid (e.g. incorrect marker, which could indicate unprovisioned/partially provisioned state or corruption). This is used as a prerequisite check for other CFPA fields since the header validity is an indicator of whether the CFPA contents can be trusted. +/// Returns the decoded lifecylce to be used by the ROM API NBOOT functions, which uses different format that what is encoded in CFPA. +/// The CFPA header encodes lifecycle in the lowest byte, with a separate inverted lifecycle byte as a validity check, and a 2 byte header marker in upper half of the word. +/// The NBOOT ROM API expects a full 32-bit raw value where the lower half is the lifecycle raw value and the upper half is the !inverse. +pub fn load_lifecycle_from_cfpa() -> Option { + let header = load_cfpa_header_word()?; + NbootLifecycleDiscriminator::from_raw(header as u8).map(NbootLifecycleDiscriminator::state) +} + +/// Load key usage NbootRootKeyUsage for all four key sets from CMPA.RoTK_USAGE word, returning None if CMPA header is invalid or the RoTK_USAGE word is erased (indicating unprovisioned/partially provisioned state that should not be trusted). The mapping from the 3-bit usage fields in CMPA to the NbootRootKeyUsage enum is based on the reference table provided by the user. +/// Note that the usage applies acroess ECDSA and ML-DSA root keys, so the same usage value applies to both the ROTKH and PQC_ROTKH for each key set. +pub fn load_rotk_usage_from_cmpa() -> Option<[NbootRootKeyUsage; 4]> { + let word = cmpa_rotk_usage_word_checked()?; + fn map(bits: u32) -> NbootRootKeyUsage { + match bits & 0x7 { + 0 => NbootRootKeyUsage::All, + 1 => NbootRootKeyUsage::DebugCa, + 2 => NbootRootKeyUsage::ImageCaFwCa, + 3 => NbootRootKeyUsage::DebugCaImageCaFwCa, + 4 => NbootRootKeyUsage::ImageKeyFwKey, + 5 => NbootRootKeyUsage::ImageKey, + 6 => NbootRootKeyUsage::FwKey, + _ => NbootRootKeyUsage::Unused, + } + } + let rotk0_usage = map((word >> 0) & 0x7); + let rotk1_usage = map((word >> 3) & 0x7); + let rotk2_usage = map((word >> 6) & 0x7); + let rotk3_usage = map((word >> 9) & 0x7); + Some([rotk0_usage, rotk1_usage, rotk2_usage, rotk3_usage]) +} + +// CMPA.RoTK_USAGE bit layout (MCXA reference manual): +// +// [2:0] RoTK0_Usage +// [5:3] RoTK1_Usage +// [8:6] RoTK2_Usage +// [11:9] RoTK3_Usage +// [12] SKIP_DICE +// [13] DICE_INC_NXP_CFG +// [14] DICE_INC_CUST_CFG +// [15] DICE_INC_NXP_FIELD_CFG +// [31:16] Reserved + +#[inline(always)] +fn cmpa_rotk_usage_word_checked() -> Option { + const CMPA_ROTK_USAGE: u32 = IFRConfigAreaBase::Cmpa as u32 + 0x0054; // 0x0100_0254 + if !cmpa_header_marker_is_valid() { + return None; + } + let word = unsafe { core::ptr::read_volatile(CMPA_ROTK_USAGE as *const u32) }; + if word == ERASED_WORD { + return None; + } + Some(word) +} + +/// CMPA.RoTK_USAGE bit 12 (SKIP_DICE) +pub fn load_dice_skip_from_cmpa() -> bool { + // If CMPA isn't valid, default to "do not skip DICE" (safer). + cmpa_rotk_usage_word_checked() + .map(|word| ((word >> 12) & 1) != 0) + .unwrap_or(false) +} + +/// CMPA.RoTK_USAGE bit 13 (DICE_INC_NXP_CFG) +pub fn load_dice_inc_nxp_cfg_from_cmpa() -> bool { + cmpa_rotk_usage_word_checked() + .map(|word| ((word >> 13) & 1) != 0) + .unwrap_or(false) +} + +/// CMPA.RoTK_USAGE bit 14 (DICE_INC_CUST_CFG) +pub fn load_dice_inc_cust_cfg_from_cmpa() -> bool { + cmpa_rotk_usage_word_checked() + .map(|word| ((word >> 14) & 1) != 0) + .unwrap_or(false) +} + +/// CMPA.RoTK_USAGE bit 15 (DICE_INC_NXP_FIELD_CFG) +pub fn load_dice_inc_nxp_field_cfg_from_cmpa() -> bool { + cmpa_rotk_usage_word_checked() + .map(|word| ((word >> 15) & 1) != 0) + .unwrap_or(false) +} + +/// Decode a raw lifecycle value into the typed NBOOT lifecycle state. +pub fn decode_lifecycle(raw_value: u32) -> NbootLifecycleState { + NbootLifecycleState::from_any_raw(raw_value).unwrap_or(NbootLifecycleState::Develop) +} diff --git a/libs/ec-slimloader-mcxa/src/manifest.rs b/libs/ec-slimloader-mcxa/src/manifest.rs new file mode 100644 index 00000000..ed634136 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/manifest.rs @@ -0,0 +1,34 @@ +//! Image manifest parsing (TZM omitted, CRC present for future CMAC wake path). +use core::mem::size_of; + +#[derive(Debug)] +pub enum ManifestError { Magic, Version, Size, Bounds } + +#[repr(C)] +pub struct ManifestHeaderRaw { + pub magic: u32, // "imgm" 0x6D676D69 + pub format_version: u32, // 0x00010000 + pub firmware_version: u32, + pub manifest_size: u32, + pub flags: u32, +} + +pub struct Manifest<'a> { + pub raw: &'a ManifestHeaderRaw, + pub crc32: u32, +} + +pub unsafe fn parse_manifest(base: *const u8, offset: u32, image_len: u32) -> Result, ManifestError> { + if offset >= image_len { return Err(ManifestError::Bounds); } + let start = base.add(offset as usize); + let raw = &*(start as *const ManifestHeaderRaw); + if raw.magic != 0x6D676D69 { return Err(ManifestError::Magic); } + if raw.format_version != 0x0001_0000 { return Err(ManifestError::Version); } + if raw.manifest_size < size_of::() as u32 { return Err(ManifestError::Size); } + if offset + raw.manifest_size > image_len { return Err(ManifestError::Bounds); } + // CRC32 is last 4 bytes of manifest + let crc_off = raw.manifest_size as usize - 4; + let crc_ptr = start.add(crc_off) as *const u32; + let crc32 = *crc_ptr; + Ok(Manifest { raw, crc32 }) +} diff --git a/libs/ec-slimloader-mcxa/src/memory.rs b/libs/ec-slimloader-mcxa/src/memory.rs new file mode 100644 index 00000000..95ceff6f --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/memory.rs @@ -0,0 +1,58 @@ +// Internal flash layout for the MCXA bootloader path. +// Secure alias: 0x1000_0000 – 0x101F_FFFF (2 MB, Matrix0 Target Port0, All Initiators, Secure). +// The bootloader always accesses flash through the secure alias. +pub const INTERNAL_FLASH_START: u32 = 0x0000_0000; +pub const INTERNAL_FLASH_SIZE: u32 = 0x0020_0000; +pub const INTERNAL_FLASH_SECTOR_SIZE: u32 = 0x2000; // 8 KB +pub const INTERNAL_FLASH_PAGE_SIZE: u32 = 128; // 128 B + +// Bootloader region (64 KB) +pub const BOOTLOADER_START: u32 = INTERNAL_FLASH_START; +pub const BOOTLOADER_SIZE: u32 = 0x0001_0000; +pub const BOOTLOADER_END: u32 = BOOTLOADER_START + BOOTLOADER_SIZE - 1; + +// Slot A (internal flash application image) +pub const SLOT_A_START: u32 = BOOTLOADER_START + BOOTLOADER_SIZE; +pub const SLOT_A_SIZE: u32 = INTERNAL_FLASH_SIZE - BOOTLOADER_SIZE - JOURNAL_SIZE; // remainder minus journal +pub const SLOT_A_END: u32 = SLOT_A_START + SLOT_A_SIZE - 1; + +// Journal (last 2 sectors) +pub const JOURNAL_SIZE: u32 = INTERNAL_FLASH_SECTOR_SIZE * 2; // 16 KB +pub const JOURNAL_START: u32 = INTERNAL_FLASH_START + INTERNAL_FLASH_SIZE - JOURNAL_SIZE; // 0x101F_C000 +pub const JOURNAL_END: u32 = JOURNAL_START + JOURNAL_SIZE - 1; + +// Ensure Slot A does not overlap journal +const _: () = assert!(SLOT_A_END < JOURNAL_START); + +// Slot B (external secure FlexSPI window) +pub const SLOT_B_START: u32 = 0x9000_0000; +pub const SLOT_B_SIZE: u32 = SLOT_A_SIZE; // symmetric +pub const SLOT_B_END: u32 = SLOT_B_START + SLOT_B_SIZE - 1; + +// Image header constants +pub const IMAGE_MAGIC: u32 = 0x534C_4D43; // 'SLMC' + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SlotId { + A, + B, +} + +impl SlotId { + pub fn as_index(self) -> u8 { + match self { + SlotId::A => 0, + SlotId::B => 1, + } + } +} + +pub fn slot_a_sector_count() -> u32 { + SLOT_A_SIZE / INTERNAL_FLASH_SECTOR_SIZE +} +pub fn slot_a_sector_start(i: u32) -> u32 { + SLOT_A_START + i * INTERNAL_FLASH_SECTOR_SIZE +} +pub fn is_page_aligned(addr: u32) -> bool { + addr % INTERNAL_FLASH_PAGE_SIZE == 0 +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api.rs b/libs/ec-slimloader-mcxa/src/rom_api.rs new file mode 100644 index 00000000..fe1adfd1 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api.rs @@ -0,0 +1,344 @@ +#![allow(non_snake_case)] +#![allow(dead_code)] +use core::ffi::c_char; + +mod flash; +mod flexspi_nor; +mod kb; +mod nboot; +mod spi_flash; + +pub use flash::*; +pub use flexspi_nor::*; +pub use kb::*; +pub use nboot::*; +pub use spi_flash::*; + +use self::flash::FlashDriverRaw; +use self::flexspi_nor::FlexspiNorFlashDriverRaw; +use self::kb::KBApiDriverRaw; +use self::nboot::NbootDriverRaw; +use self::spi_flash::SpiFlashDriverRaw; + +pub type Status = u32; +pub type NbootBool = u32; +pub type NbootStatusProtected = u64; + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct StandardVersionFields { + pub bugfix: u8, + pub minor: u8, + pub major: u8, + pub name: u8, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union StandardVersion { + pub fields: StandardVersionFields, + pub version: u32, +} + +#[repr(C)] +struct RomApiRaw { + // NXP usage: uint32_t arg = ...; g_bootloaderTree->runBootloader(&arg); + // The ROM API takes a pointer to the argument word (NULL is allowed for default behavior). + pub run_bootloader: unsafe extern "C" fn(arg: *const u32), + // Flash driver interface table. + pub flash_api: *const FlashDriverRaw, + pub kb_api: *const KBApiDriverRaw, + pub nboot_api: *const NbootDriverRaw, + pub flex_spi_api: *const FlexspiNorFlashDriverRaw, + pub spi_flash_api: *const SpiFlashDriverRaw, + pub version: StandardVersion, + pub copyright: *const c_char, +} + +#[derive(Clone, Copy)] +pub struct RomApi { + raw: &'static RomApiRaw, +} + +impl RomApi { + const fn from_raw(raw: &'static RomApiRaw) -> Self { + Self { raw } + } + + pub fn run_bootloader(&self, arg: *const u32) { + unsafe { (self.raw.run_bootloader)(arg) } + } + + pub fn flash_api(&self) -> FlashDriver { + unsafe { FlashDriver::from_raw(&*self.raw.flash_api) } + } + + pub fn flash_api_opt(&self) -> Option { + let ptr = self.raw.flash_api; + if ptr.is_null() { + None + } else { + unsafe { Some(FlashDriver::from_raw(&*ptr)) } + } + } + + pub fn kb_api(&self) -> KBApiDriver { + unsafe { KBApiDriver::from_raw(&*self.raw.kb_api) } + } + + pub fn kb_api_opt(&self) -> Option { + let ptr = self.raw.kb_api; + if ptr.is_null() { + None + } else { + unsafe { Some(KBApiDriver::from_raw(&*ptr)) } + } + } + + pub fn nboot_api(&self) -> NbootDriver { + unsafe { NbootDriver::from_raw(&*self.raw.nboot_api) } + } + + pub fn nboot_api_opt(&self) -> Option { + let ptr = self.raw.nboot_api; + if ptr.is_null() { + None + } else { + unsafe { Some(NbootDriver::from_raw(&*ptr)) } + } + } + + pub fn flex_spi_api(&self) -> FlexspiNorFlashDriver { + unsafe { FlexspiNorFlashDriver::from_raw(&*self.raw.flex_spi_api) } + } + + pub fn flex_spi_api_opt(&self) -> Option { + let ptr = self.raw.flex_spi_api; + if ptr.is_null() { + None + } else { + unsafe { Some(FlexspiNorFlashDriver::from_raw(&*ptr)) } + } + } + + pub fn spi_flash_api(&self) -> SpiFlashDriver { + unsafe { SpiFlashDriver::from_raw(&*self.raw.spi_flash_api) } + } + + pub fn spi_flash_api_opt(&self) -> Option { + let ptr = self.raw.spi_flash_api; + if ptr.is_null() { + None + } else { + unsafe { Some(SpiFlashDriver::from_raw(&*ptr)) } + } + } + + pub fn version(&self) -> StandardVersion { + self.raw.version + } + + pub fn copyright(&self) -> *const c_char { + self.raw.copyright + } +} + +pub type BootloaderTree = RomApi; + +#[inline(always)] +pub fn rom_api() -> RomApi { + const ROM_API_BASE: usize = 0x1303_D800; // from MCXA Reference Manual. + unsafe { + let ptr = ROM_API_BASE as *const RomApiRaw; + RomApi::from_raw(&*ptr) + } +} + +#[inline(always)] +pub fn bootloader_tree() -> BootloaderTree { + rom_api() +} + +// runBootloader API fields (Table 31) +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootTag { + EnterBoot = 0xEB << 24, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootMode { + PrimaryMasterBoot = 0x0 << 20, + IspBoot = 0x1 << 20, + ProvFwMode = 0x2 << 20, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootIspInterface { + AutoDetection = 0x0 << 16, + Uart = 0x1 << 16, + Spi = 0x2 << 16, + I2c = 0x8 << 16, + UsbHid = 0x10 << 16, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootMasterFlashBootOption { + InternalFlash = 0x0 << 16, + FlexspiFlash = 0x2 << 16, + OneBitSpiNorFlash = 0x3 << 16, + AutoDetection = 0x1F << 16, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootInterfaceInstance { + FlexspiPortA = 0x0 << 12, + FlexspiPortB = 0x1 << 12, + FlexspiPortAAndB = 0x2 << 12, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootImageIndex { + Image0 = 0x0 << 8, + Image1 = 0x1 << 8, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootRecoveryBootCfg1 { + SpiNorBaudRate0 = 0x0 << 6, + SpiNorBaudRate1 = 0x1 << 6, + SpiNorBaudRate2 = 0x2 << 6, + SpiNorBaudRate3 = 0x3 << 6, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RunBootRecoveryBootCfg0 { + SpiNorChipSelect0 = 0x0 << 4, + SpiNorChipSelect1 = 0x1 << 4, + SpiNorChipSelect2 = 0x2 << 4, + SpiNorChipSelect3 = 0x3 << 4, +} + +/// Helper function to invoke the ROM API's run_bootloader function with the appropriate argument to enter ISP mode over UART. This can be used as a fallback if the main bootloader fails and we want to recover by flashing over UART using NXP's ISP tools. +/// The function will not return since the bootloader will take over execution after this call, but we still include an infinite loop after the call to satisfy the Rust type system since the function is declared to return ! (never). +pub fn run_bootloader_uart() -> ! { + // Build arg: tag 0xEB, mode ISP(1), interface UART(1) + let arg: u32 = RunBootTag::EnterBoot as u32 | RunBootMode::IspBoot as u32 | RunBootIspInterface::Uart as u32; + bootloader_tree().run_bootloader(&arg as *const u32); + loop { + core::hint::spin_loop() + } +} + +/// Helper function to get a pointer to the flash driver API from the ROM API tree. +pub fn flash_driver() -> FlashDriver { + // Match NXP usage: g_bootloaderTree->flashDriver->... + // The bootloader tree stores a direct pointer to the flash driver interface. + bootloader_tree().flash_api() +} + +pub fn flash_driver_opt() -> Option { + bootloader_tree().flash_api_opt() +} + +/// Helper function to get a pointer to the nboot API from the ROM API tree. +pub fn nboot() -> NbootDriver { + // Match NXP usage: g_bootloaderTree->nbootDriver->... + bootloader_tree().nboot_api() +} + +pub fn nboot_opt() -> Option { + bootloader_tree().nboot_api_opt() +} + +/// Helper function to get a pointer to the KB driver API from the ROM API tree. +pub fn kb() -> KBApiDriver { + bootloader_tree().kb_api() +} + +pub fn kb_opt() -> Option { + bootloader_tree().kb_api_opt() +} + +/// Helper function to get a pointer to the FlexSPI NOR flash driver API from the ROM API tree. +pub fn flexspi_nor() -> FlexspiNorFlashDriver { + bootloader_tree().flex_spi_api() +} + +pub fn flexspi_nor_opt() -> Option { + bootloader_tree().flex_spi_api_opt() +} + +/// Helper function to get a pointer to the SPI flash driver API from the ROM API tree. +pub fn spi_flash() -> SpiFlashDriver { + bootloader_tree().spi_flash_api() +} + +pub fn spi_flash_opt() -> Option { + bootloader_tree().spi_flash_api_opt() +} + +#[inline(always)] +/// Used to get the default FlashConfig struct to be inited by flash_init(). +pub fn flash_cfg_for_rom_api() -> FlashConfig { + FlashConfig { + pflash_block_base: 0, + pflash_total_size: 0, + pflash_block_count: 0, + pflash_page_size: 0, + pflash_sector_size: 0, + ffr_config: FlashFfrConfig { + ffr_block_base: 0, + ffr_total_size: 0, + ffr_page_size: 0, + sector_size: 0, + cfpa_page_version: 0, + cfpa_page_offset: 0, + }, + mode_config: FlashModeConfig::new( + 0, + FlashReadSingleWordConfig::new( + FlashReadEccOption::On, + FlashReadMarginOption::Normal, + FlashReadDmaccOption::Disabled, + ), + FlashSetWriteModeConfig::new(FlashRampControlOption::Reserved, FlashRampControlOption::Reserved), + FlashSetReadModeConfig::new(0, 0, 0), + ), + nboot_ctx: core::ptr::null_mut(), + use_ahb_read: true, + } +} + +// Compile-time ABI guards for MCXA ROM-facing structs. +// The ROM expects `spi_eeprom_config(uint32_t *config)` to point at exactly 2x u32 words +// (8 bytes total). ABI size checks for this and other ROM-facing structs are collected +// below in a single private guard block. + +struct AbiGuards; + +impl AbiGuards { + const CHECK: () = { + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + let _ = [0u8; core::mem::size_of::()]; + }; +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api/flash.rs b/libs/ec-slimloader-mcxa/src/rom_api/flash.rs new file mode 100644 index 00000000..6d799625 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api/flash.rs @@ -0,0 +1,328 @@ +use super::{StandardVersion, Status}; +use crate::error::FlashStatus; + +#[repr(C)] +pub struct FlashFfrConfig { + // FFR block base address. + pub ffr_block_base: u32, + // FFR total size in bytes. + pub ffr_total_size: u32, + // FFR page size in bytes. + pub ffr_page_size: u32, + // Sector size in bytes. + pub sector_size: u32, + // CFPA page version. + pub cfpa_page_version: u32, + // CFPA page offset within FFR. + pub cfpa_page_offset: u32, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashReadEccOption { + On = 0, + Off = 1, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashReadMarginOption { + Normal = 0, + VsProgram = 1, + VsErase = 2, + IllegalBitCombination = 3, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashReadDmaccOption { + Disabled = 0, + Enabled = 1, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashRampControlOption { + Reserved = 0, + DivisionFactor256 = 1, + DivisionFactor128 = 2, + DivisionFactor64 = 3, +} + +#[repr(C)] +pub struct FlashReadSingleWordConfig { + pub packed_options: u8, + pub reserved1: [u8; 3], +} + +impl FlashReadSingleWordConfig { + #[inline(always)] + pub const fn new(ecc: FlashReadEccOption, margin: FlashReadMarginOption, dmacc: FlashReadDmaccOption) -> Self { + Self { + packed_options: (ecc as u8) | ((margin as u8) << 1) | ((dmacc as u8) << 3), + reserved1: [0; 3], + } + } +} + +#[repr(C)] +pub struct FlashSetWriteModeConfig { + pub program_ramp_control: u8, + pub erase_ramp_control: u8, + pub reserved: [u8; 2], +} + +impl FlashSetWriteModeConfig { + #[inline(always)] + pub const fn new(program: FlashRampControlOption, erase: FlashRampControlOption) -> Self { + Self { + program_ramp_control: program as u8, + erase_ramp_control: erase as u8, + reserved: [0; 2], + } + } +} + +#[repr(C)] +pub struct FlashSetReadModeConfig { + pub read_interface_timing_trim: u16, + pub read_controller_timing_trim: u16, + pub read_wait_states: u8, + pub reserved: [u8; 3], +} + +impl FlashSetReadModeConfig { + #[inline(always)] + pub const fn new(read_interface_timing_trim: u16, read_controller_timing_trim: u16, read_wait_states: u8) -> Self { + Self { + read_interface_timing_trim, + read_controller_timing_trim, + read_wait_states, + reserved: [0; 3], + } + } +} + +#[repr(C)] +pub struct FlashModeConfig { + pub sys_freq_in_m_hz: u32, + pub read_single_word: FlashReadSingleWordConfig, + pub set_write_mode: FlashSetWriteModeConfig, + pub set_read_mode: FlashSetReadModeConfig, +} + +impl FlashModeConfig { + #[inline(always)] + pub const fn new( + sys_freq_in_m_hz: u32, + read_single_word: FlashReadSingleWordConfig, + set_write_mode: FlashSetWriteModeConfig, + set_read_mode: FlashSetReadModeConfig, + ) -> Self { + Self { + sys_freq_in_m_hz, + read_single_word, + set_write_mode, + set_read_mode, + } + } +} + +#[repr(C)] +pub struct FlashConfig { + // P-Flash block base address. + pub pflash_block_base: u32, + // P-Flash total size in bytes. + pub pflash_total_size: u32, + // P-Flash block count. + pub pflash_block_count: u32, + // P-Flash page size in bytes. + pub pflash_page_size: u32, + // P-Flash sector size in bytes. + pub pflash_sector_size: u32, + // FFR configuration. + pub ffr_config: FlashFfrConfig, + // Flash controller parameter configuration. + pub mode_config: FlashModeConfig, + // ROM NBOOT context pointer. + pub nboot_ctx: *mut u32, + // Use AHB read (true) or alternative read path (false), per ROM. + pub use_ahb_read: bool, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlashPropertyTag { + PflashSectorSize = 0x00, + PflashTotalSize = 0x01, + PflashBlockSize = 0x02, + PflashBlockCount = 0x03, + PflashBlockBaseAddr = 0x04, + PflashPageSize = 0x30, + PflashSystemFreq = 0x31, + FfrSectorSize = 0x40, + FfrTotalSize = 0x41, + FfrBlockBaseAddr = 0x42, + FfrPageSize = 0x43, +} + +// Flash erase key required by the ROM flash erase-sector entry point. +// Defined as FOUR_CHAR_CODE('l', 'f', 'e', 'k') = (('k' << 24) | ('e' << 16) | ('f' << 8) | ('l')) +pub const FLASH_API_ERASE_KEY: u32 = 0x6b65666c; + +#[repr(C)] +pub(super) struct FlashDriverRaw { + // Initialize flash driver/config. + pub flash_init: unsafe extern "C" fn(config: *mut FlashConfig) -> Status, + // Erase sector(s). Requires FLASH_API_ERASE_KEY. + pub flash_erase_sector: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32, key: u32) -> Status, + // Program phrase (alignment requirements apply). + pub flash_program_phrase: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, src: *const u8, length_in_bytes: u32) -> Status, + // Program page. + pub flash_program_page: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, src: *const u8, length_in_bytes: u32) -> Status, + // Verify programmed data. + pub flash_verify_program: unsafe extern "C" fn( + config: *mut FlashConfig, + start: u32, + length_in_bytes: u32, + expected_data: *const u8, + failed_address: *mut u32, + failed_data: *mut u32, + ) -> Status, + // Verify phrase erase. + pub flash_verify_erase_phrase: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Verify page erase. + pub flash_verify_erase_page: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Verify sector erase. + pub flash_verify_erase_sector: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Get flash property. + pub flash_get_property: + unsafe extern "C" fn(config: *mut FlashConfig, which_property: u32, value: *mut u32) -> Status, + // Verify phrase erase in IFR. + pub ifr_verify_erase_phrase: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Verify page erase in IFR. + pub ifr_verify_erase_page: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Verify sector erase in IFR. + pub ifr_verify_erase_sector: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> Status, + // Read flash into dest. + pub flash_read: + unsafe extern "C" fn(config: *mut FlashConfig, start: u32, dest: *mut u8, length_in_bytes: u32) -> Status, + // Flash API version. + pub version: StandardVersion, +} + +#[derive(Clone, Copy)] +pub struct FlashDriver { + raw: &'static FlashDriverRaw, +} + +impl FlashDriver { + pub(super) const fn from_raw(raw: &'static FlashDriverRaw) -> Self { + Self { raw } + } + + pub fn flash_init(&self, config: *mut FlashConfig) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_init)(config)) } + } + + pub fn flash_erase_sector( + &self, + config: *mut FlashConfig, + start: u32, + length_in_bytes: u32, + key: u32, + ) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_erase_sector)(config, start, length_in_bytes, key)) } + } + + pub fn flash_program_phrase( + &self, + config: *mut FlashConfig, + start: u32, + src: *const u8, + length_in_bytes: u32, + ) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_program_phrase)(config, start, src, length_in_bytes)) } + } + + pub fn flash_program_page( + &self, + config: *mut FlashConfig, + start: u32, + src: *const u8, + length_in_bytes: u32, + ) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_program_page)(config, start, src, length_in_bytes)) } + } + + pub fn flash_verify_program( + &self, + config: *mut FlashConfig, + start: u32, + length_in_bytes: u32, + expected_data: *const u8, + failed_address: *mut u32, + failed_data: *mut u32, + ) -> FlashStatus { + unsafe { + FlashStatus::from_raw((self.raw.flash_verify_program)( + config, + start, + length_in_bytes, + expected_data, + failed_address, + failed_data, + )) + } + } + + pub fn flash_verify_erase_phrase(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_verify_erase_phrase)(config, start, length_in_bytes)) } + } + + pub fn flash_verify_erase_page(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_verify_erase_page)(config, start, length_in_bytes)) } + } + + pub fn flash_verify_erase_sector(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_verify_erase_sector)(config, start, length_in_bytes)) } + } + + pub fn flash_get_property( + &self, + config: *mut FlashConfig, + which_property: FlashPropertyTag, + value: *mut u32, + ) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_get_property)(config, which_property as u32, value)) } + } + + pub fn ifr_verify_erase_phrase(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.ifr_verify_erase_phrase)(config, start, length_in_bytes)) } + } + + pub fn ifr_verify_erase_page(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.ifr_verify_erase_page)(config, start, length_in_bytes)) } + } + + pub fn ifr_verify_erase_sector(&self, config: *mut FlashConfig, start: u32, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.ifr_verify_erase_sector)(config, start, length_in_bytes)) } + } + + pub fn flash_read(&self, config: *mut FlashConfig, start: u32, dest: *mut u8, length_in_bytes: u32) -> FlashStatus { + unsafe { FlashStatus::from_raw((self.raw.flash_read)(config, start, dest, length_in_bytes)) } + } + + pub fn version(&self) -> StandardVersion { + self.raw.version + } +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api/flexspi_nor.rs b/libs/ec-slimloader-mcxa/src/rom_api/flexspi_nor.rs new file mode 100644 index 00000000..d468114f --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api/flexspi_nor.rs @@ -0,0 +1,408 @@ +use super::Status; +use crate::error::FlexspiStatus; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SerialNorOptionTag { + Config = 0x0C, // SDK vs. RM mismatch; TODO: confirm correct value and semantics. +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorOptionSize { + Option0Only = 0, + Option0AndOption1 = 1, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorDeviceType { + ReadSfdpSdr = 0, + ReadSfdpDdr = 1, + Hyperflash1V8 = 2, + Hyperflash3V0 = 3, + MacronixOctalDdr = 4, + MacronixOctalSdr = 5, + MicronOctalDdr = 6, + MicronOctalSdr = 7, + AdestoOctalDdr = 8, + AdestoOctalSdr = 9, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorOptionPadEncoding { + // Encoded values for option0.query_pads / option0.cmd_pads. + // These match the ROM option field encoding, not the literal kSerialFlash_*Pad values. + One = 0, + Four = 2, + Eight = 3, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorQuadModeSetting { + NotConfigured = 0, + StatusReg1Bit6 = 1, + StatusReg2Bit1 = 2, + StatusReg2Bit7 = 3, + StatusReg2Bit1Via0x31 = 4, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorMiscMode { + Disabled = 0, + Mode0_4_4 = 1, + Mode0_8_8 = 2, + DataOrderSwapped = 3, + SecondPinMux = 4, + InternalLoopback = 5, + SpiMode = 6, + ExternalDqs = 8, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiSerialClockFrequency { + NoChange = 0, + MHz30 = 1, + MHz50 = 2, + MHz60 = 3, + MHz75 = 4, + MHz80 = 5, + MHz100 = 6, + MHz133 = 7, + MHz166 = 8, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialNorFlashConnection { + SinglePortA = 0, + Parallel = 1, + SinglePortB = 2, + BothPorts = 3, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiClockSource { + // Table 60 selector values for the ROM set_clock_source API. + NoClock = 0, + Pll0 = 1, + FroHf = 3, + Pll1 = 5, + UsbPll = 6, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiClockConfigFrequency { + // Table 61 values for the ROM config_clock API. + MHz30 = 1, + MHz50 = 2, + MHz60 = 3, + MHz75 = 4, + MHz100 = 5, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiClockConfigMode { + Sdr = 0, + Ddr = 1, +} + +#[inline(always)] +pub const fn pack_serial_nor_option0( + option_size: SerialNorOptionSize, + device_type: SerialNorDeviceType, + query_pad: SerialNorOptionPadEncoding, + cmd_pad: SerialNorOptionPadEncoding, + quad_mode_setting: SerialNorQuadModeSetting, + misc_mode: SerialNorMiscMode, + max_freq: FlexspiSerialClockFrequency, +) -> u32 { + ((SerialNorOptionTag::Config as u32) << 28) + | ((option_size as u32) << 24) + | ((device_type as u32) << 20) + | ((query_pad as u32) << 16) + | ((cmd_pad as u32) << 12) + | ((quad_mode_setting as u32) << 8) + | ((misc_mode as u32) << 4) + | (max_freq as u32) +} + +#[inline(always)] +pub const fn pack_serial_nor_option1( + flash_connection: SerialNorFlashConnection, + dqs_pinmux_group: u32, + pinmux_group: u32, + status_override: u32, + dummy_cycles: u32, +) -> u32 { + ((flash_connection as u32) << 28) + | ((dqs_pinmux_group & 0xF) << 20) + | ((pinmux_group & 0xF) << 16) + | ((status_override & 0xFF) << 8) + | (dummy_cycles & 0xFF) +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct SerialNorConfigOption { + // Packed ROM ABI input for `flexspiNorDriver->get_config(...)`. + // + // Typical MCXA ROM examples: + // - Quad NOR, Quad SDR read @ 75 MHz: option0 = 0xC000_0004, option1 = 0 + // - Quad NOR, Quad DDR read @ 60 MHz: option0 = 0xC010_0003, option1 = 0 + // + // Build these words with `pack_serial_nor_option0(...)` and + // `pack_serial_nor_option1(...)`, keeping the transport struct itself raw. + // + // Example call pattern from the ROM docs: + // let mut option = SerialNorConfigOption { option0: 0xC000_0001, option1: 0 }; + // flexspi_nor().get_config(instance, &mut cfg, &mut option); + pub option0: u32, + pub option1: u32, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FlexspiOperationType { + Command = 0, + Config = 1, + Write = 2, + Read = 3, +} + +impl FlexspiOperationType { + pub const END: Self = Self::Read; +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FlashRunContextFields { + pub por_mode: u8, + pub current_mode: u8, + pub exit_no_cmd_sequence: u8, + pub restore_sequence: u8, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union FlashRunContext { + pub B: FlashRunContextFields, + pub U: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FlexspiLutSeq { + pub seq_num: u8, + pub seq_id: u8, + pub reserved: u16, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FlexspiDllTime { + pub time_100ps: u8, + pub delay_cells: u8, +} + +#[repr(C)] +pub struct FlexspiMemConfig { + pub tag: u32, + pub version: u32, + pub reserved0: u32, + pub read_sample_clk_src: u8, + pub cs_hold_time: u8, + pub cs_setup_time: u8, + pub column_address_width: u8, + pub device_mode_cfg_enable: u8, + pub device_mode_type: u8, + pub wait_time_cfg_commands: u16, + pub device_mode_seq: FlexspiLutSeq, + pub device_mode_arg: u32, + pub config_cmd_enable: u8, + pub config_mode_type: [u8; 3], + pub config_cmd_seqs: [FlexspiLutSeq; 3], + pub reserved1: u32, + pub config_cmd_args: [u32; 3], + pub reserved2: u32, + pub controller_misc_option: u32, + pub device_type: u8, + pub sflash_pad_type: u8, + pub serial_clk_freq: u8, + pub lut_custom_seq_enable: u8, + pub reserved3: [u32; 2], + pub sflash_a1_size: u32, + pub sflash_a2_size: u32, + pub sflash_b1_size: u32, + pub sflash_b2_size: u32, + pub cs_pad_setting_override: u32, + pub sclk_pad_setting_override: u32, + pub data_pad_setting_override: u32, + pub dqs_pad_setting_override: u32, + pub timeout_in_ms: u32, + pub command_interval: u32, + pub data_valid_time: [FlexspiDllTime; 2], + pub busy_offset: u16, + pub busy_bit_polarity: u16, + pub lookup_table: [u32; 64], + pub lut_custom_seq: [FlexspiLutSeq; 12], + pub dll0_cr_val: u32, + pub dll1_cr_val: u32, + pub reserved4: [u32; 2], +} + +#[repr(C)] +pub struct FlexspiNorConfig { + pub mem_config: FlexspiMemConfig, + pub page_size: u32, + pub sector_size: u32, + pub ipcmd_serial_clk_freq: u8, + pub is_uniform_block_size: u8, + pub is_data_order_swapped: u8, + pub reserved0: [u8; 1], + pub serial_nor_type: u8, + pub need_exit_no_cmd_mode: u8, + pub half_clk_for_non_read_cmd: u8, + pub need_restore_no_cmd_mode: u8, + pub block_size: u32, + pub flash_state_ctx: FlashRunContext, + pub reserved2: [u32; 10], +} + +#[repr(C)] +pub struct FlexspiXfer { + pub operation: FlexspiOperationType, + pub base_address: u32, + pub seq_id: u32, + pub seq_num: u32, + pub is_parallel_mode_enable: bool, + pub tx_buffer: *const u32, + pub tx_size: u32, + pub rx_buffer: *mut u32, + pub rx_size: u32, +} + +#[repr(C)] +pub(super) struct FlexspiNorFlashDriverRaw { + pub version: u32, + pub init: unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig) -> Status, + pub page_program: + unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, dst: u32, src: *const u32) -> Status, + pub erase_all: unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig) -> Status, + pub erase: unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, start: u32, len: u32) -> Status, + pub erase_sector: unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, addr: u32) -> Status, + pub erase_block: unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, addr: u32) -> Status, + pub get_config: + unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, opt: *mut SerialNorConfigOption) -> Status, + pub read: unsafe extern "C" fn( + instance: u32, + cfg: *mut FlexspiNorConfig, + dst: *mut u32, + start: u32, + bytes: u32, + ) -> Status, + pub xfer: unsafe extern "C" fn(instance: u32, xfer: *mut FlexspiXfer) -> Status, + pub update_lut: unsafe extern "C" fn(instance: u32, seq_index: u32, lut_base: *const u32, num_seq: u32) -> Status, + pub set_clock_source: unsafe extern "C" fn(clock_src: u32) -> Status, + pub config_clock: unsafe extern "C" fn(instance: u32, freq_option: u32, sample_clk_mode: u32), + pub partial_program: + unsafe extern "C" fn(instance: u32, cfg: *mut FlexspiNorConfig, dst: u32, src: *const u32, len: u32) -> Status, +} + +#[derive(Clone, Copy)] +pub struct FlexspiNorFlashDriver { + raw: &'static FlexspiNorFlashDriverRaw, +} + +impl FlexspiNorFlashDriver { + pub(super) const fn from_raw(raw: &'static FlexspiNorFlashDriverRaw) -> Self { + Self { raw } + } + + pub fn version(&self) -> u32 { + self.raw.version + } + + pub fn init(&self, instance: u32, cfg: *mut FlexspiNorConfig) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.init)(instance, cfg)) } + } + + pub fn page_program(&self, instance: u32, cfg: *mut FlexspiNorConfig, dst: u32, src: *const u32) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.page_program)(instance, cfg, dst, src)) } + } + + pub fn erase_all(&self, instance: u32, cfg: *mut FlexspiNorConfig) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.erase_all)(instance, cfg)) } + } + + pub fn erase(&self, instance: u32, cfg: *mut FlexspiNorConfig, start: u32, len: u32) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.erase)(instance, cfg, start, len)) } + } + + pub fn erase_sector(&self, instance: u32, cfg: *mut FlexspiNorConfig, addr: u32) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.erase_sector)(instance, cfg, addr)) } + } + + pub fn erase_block(&self, instance: u32, cfg: *mut FlexspiNorConfig, addr: u32) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.erase_block)(instance, cfg, addr)) } + } + + pub fn get_config( + &self, + instance: u32, + cfg: *mut FlexspiNorConfig, + opt: *mut SerialNorConfigOption, + ) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.get_config)(instance, cfg, opt)) } + } + + pub fn read( + &self, + instance: u32, + cfg: *mut FlexspiNorConfig, + dst: *mut u32, + start: u32, + bytes: u32, + ) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.read)(instance, cfg, dst, start, bytes)) } + } + + pub fn xfer(&self, instance: u32, xfer: *mut FlexspiXfer) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.xfer)(instance, xfer)) } + } + + pub fn update_lut(&self, instance: u32, seq_index: u32, lut_base: *const u32, num_seq: u32) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.update_lut)(instance, seq_index, lut_base, num_seq)) } + } + + pub fn set_clock_source(&self, clock_src: FlexspiClockSource) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.set_clock_source)(clock_src as u32)) } + } + + pub fn config_clock( + &self, + instance: u32, + freq_option: FlexspiClockConfigFrequency, + sample_clk_mode: FlexspiClockConfigMode, + ) { + unsafe { (self.raw.config_clock)(instance, freq_option as u32, sample_clk_mode as u32) } + } + + pub fn partial_program( + &self, + instance: u32, + cfg: *mut FlexspiNorConfig, + dst: u32, + src: *const u32, + len: u32, + ) -> FlexspiStatus { + unsafe { FlexspiStatus::from_raw((self.raw.partial_program)(instance, cfg, dst, src, len)) } + } +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api/kb.rs b/libs/ec-slimloader-mcxa/src/rom_api/kb.rs new file mode 100644 index 00000000..886caeff --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api/kb.rs @@ -0,0 +1,127 @@ +use super::Status; +use crate::error::KbStatus; +use core::ffi::c_void; + +// KBoot (KB) ROM API + +#[repr(C)] +pub struct KbRegion { + // Region base address. + pub address: u32, + // Region length in bytes. + pub length: u32, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KbOperation { + // Verify/authenticate image. + AuthenticateImage = 1, + // Load image. + LoadImage = 2, + // Number of KB operations. + OperationCount = 3, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct KbLoadSb { + // Profile selector (meaning per ROM header / implementation). + pub profile: u32, + // Minimum build number. + pub minBuildNumber: u32, + // Override SB boot section ID. + pub overrideSBBootSectionID: u32, + // User SB KEK pointer. + pub userSBKEK: *mut u32, + // Number of region descriptors. + pub regionCount: u32, + // Pointer to regions array. + pub regions: *const KbRegion, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct KbAuthenticate { + // Profile selector (meaning per ROM header / implementation). + pub profile: u32, + // Minimum build number. + pub minBuildNumber: u32, + // Maximum image length. + pub maxImageLength: u32, + // User RHK pointer. + pub userRHK: *mut u32, +} + +#[repr(C)] +pub union KbOptionsParams { + pub authenticate: KbAuthenticate, + pub loadSB: KbLoadSb, +} + +#[repr(C)] +pub struct KbOptions { + // Must be KB_API_VERSION. + pub version: u32, + // Caller-provided buffer used by ROM. + pub buffer: *mut u8, + // Length of buffer in bytes. + pub bufferLength: u32, + // Requested operation. + pub op: KbOperation, + // Operation-specific parameters. + pub params: KbOptionsParams, +} + +#[repr(C)] +pub struct KbBufferDesc { + // Buffer pointer. + pub buf: *mut u8, + // Buffer length in bytes. + pub len: u32, + // Allocated size. + pub allocated: u32, +} + +#[repr(C)] +pub struct KbSessionRef { + // Options used to create the session. + pub options: KbOptions, + // Internal buffer descriptor. + pub buffer_desc: KbBufferDesc, + // Opaque operation context. + pub op_context: *mut c_void, +} + +#[repr(C)] +pub(super) struct KBApiDriverRaw { + // Initialize KB session. + pub kb_init: unsafe extern "C" fn(session: *mut *mut KbSessionRef, options: *const KbOptions) -> Status, + // Deinitialize KB session. + pub kb_deinit: unsafe extern "C" fn(session: *mut KbSessionRef) -> Status, + // Execute KB operation over a data buffer. + pub kb_execute: unsafe extern "C" fn(session: *mut KbSessionRef, data: *const u8, dataLength: u32) -> Status, +} + +#[derive(Clone, Copy)] +pub struct KBApiDriver { + raw: &'static KBApiDriverRaw, +} + +impl KBApiDriver { + pub(super) const fn from_raw(raw: &'static KBApiDriverRaw) -> Self { + Self { raw } + } + + pub fn kb_init(&self, session: *mut *mut KbSessionRef, options: *const KbOptions) -> KbStatus { + unsafe { KbStatus::from_raw((self.raw.kb_init)(session, options)) } + } + + pub fn kb_deinit(&self, session: *mut KbSessionRef) -> KbStatus { + unsafe { KbStatus::from_raw((self.raw.kb_deinit)(session)) } + } + + pub fn kb_execute(&self, session: *mut KbSessionRef, data: *const u8, dataLength: u32) -> KbStatus { + unsafe { KbStatus::from_raw((self.raw.kb_execute)(session, data, dataLength)) } + } +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api/nboot.rs b/libs/ec-slimloader-mcxa/src/rom_api/nboot.rs new file mode 100644 index 00000000..6c51319b --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api/nboot.rs @@ -0,0 +1,508 @@ +use super::{NbootBool, NbootStatusProtected}; +use crate::error::*; + +// Boolean/result markers returned via nboot_bool_t out-parameters. +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootBoolValue { + True = 0x3C5A_C33C, + True256 = 0x3C5A_C35A, + True384 = 0x3C5A_C3A5, + False = 0x5AA5_5AA5, +} + +impl NbootBoolValue { + #[inline(always)] + pub const fn from_raw(raw: NbootBool) -> Option { + match raw { + 0x3C5A_C33C => Some(Self::True), + 0x3C5A_C35A => Some(Self::True256), + 0x3C5A_C3A5 => Some(Self::True384), + 0x5AA5_5AA5 => Some(Self::False), + _ => None, + } + } +} + +#[inline(always)] +pub const fn nboot_bool_is_true(value: NbootBool) -> bool { + matches!(NbootBoolValue::from_raw(value), Some(NbootBoolValue::True)) // Only using TRUE because only supporting hybrid mode, NO ECDSA only support. +} + +#[repr(C)] +pub struct NbootCtx { + // Opaque context buffer. Size must match NBOOT_CONTEXT_SIZE from the ROM header. + // TODO: mismatch between RM and SDK. + pub opaque: [u8; 0xA94], +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootMemCryptRegion { + Region0 = 0, + Region1 = 1, + Region2 = 2, + Region3 = 3, + Region4 = 4, + Region5 = 5, + Region6 = 6, + Region7 = 7, +} + +pub type NbootMemCryptOperation = u32; // Need to confirm exact typed values. + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootMemCryptIpedModeSelect { + Rounds12 = 0x3C5A_C33C, + Rounds22 = 0x5AA5_5AA5, + FullyPipelined = 0x5A5A_5A5A, + NotFullyPipelined = 0xA5A5_A5A5, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootMemCryptEngineType { + Npx = 0x5959_5959, + Iped = 0x9595_9595, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct NbootNpxRegionConfig { + // Must be NbootMemCryptEngineType::Npx. + pub configId: NbootMemCryptEngineType, + // Start address (inclusive). + pub startAddress: u32, + // End address (inclusive). + pub endAddress: u32, + // Subregion bitmap. + pub subregion: u64, + // IV erase counter. + pub ivEraseCounter: u32, + // Region lock flag. + pub regionLock: u8, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct NbootIpedRegionConfig { + // Must be NbootMemCryptEngineType::Iped. + pub configId: NbootMemCryptEngineType, + // Start address (inclusive). + pub startAddress: u32, + // End address (inclusive). + pub endAddress: u32, + // IV erase counter. + pub ivEraseCounter: u32, + // Region flags. + pub regionFlags: u8, + // Additional authenticated data. + pub aad: u32, +} + +#[repr(C)] +pub union NbootMemCryptRegionConfig { + pub npxConfig: NbootNpxRegionConfig, + pub ipedConfig: NbootIpedRegionConfig, +} + +#[repr(C)] +pub struct NbootRotAuthParms { + pub soc_rootKeyRevocation: [u32; 4], + pub soc_imageKeyRevocation: u32, + pub soc_rkh: [u32; 12], + pub soc_rkh_1: [u32; 12], // PQC_ROTKH (hash of hashes) + pub soc_numberOfRootKeys: u32, + pub soc_rootKeyUsage: [u32; 4], + pub soc_rootKeyTypeAndLength: u32, + pub soc_lifecycle: u32, +} + +#[repr(C)] +pub struct NbootImgAuthParms { + pub soc_RoTNVM: NbootRotAuthParms, + pub soc_trustedFirmwareVersion: u32, +} + +#[repr(C)] +pub struct NbootImgAuthenticateCmacParms { + pub expectedMAC: [u32; 4], +} + +#[repr(C)] +pub struct NbootSb4LoadManifestParms { + // Returned RoTNVM/auth parameters. + pub soc_RoTNVM: NbootRotAuthParms, + // Returned trusted firmware version. + pub soc_trustedFirmwareVersion: u32, + // Returned maximum SB block size. + pub maxBlockSize: u32, + // Returned PCK blob. + pub pckBlob: [u8; 48], +} + +// Root key configuration constants for NBOOT +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootRootKeyRevocation { + Enabled = 0xAA, + Revoked = 0xBB, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootRootKeyUsage { + All = 0x0, + DebugCa = 0x1, + ImageCaFwCa = 0x2, + DebugCaImageCaFwCa = 0x3, + ImageKeyFwKey = 0x4, + ImageKey = 0x5, + FwKey = 0x6, + Unused = 0x7, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootRootKeyType { + EcdsaP384Mldsa87 = 0x0000_FD04, // Hybrid root key type: ECDSA P-384 + ML-DSA-87 +} + +// Lifecycle state codes (low-byte discriminators) and full CFPA LC_STATE values. +// Per Table 18 (Life Cycle States): LC_STATE is a u32 like 0x9635_FC03. +// Some call sites only carry the low-byte discriminator (e.g. 0x03 for Develop), +// so we keep both representations. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootLifecycleDiscriminator { + Develop = 0x03, + Develop2 = 0x07, + InField = 0x0F, + InFieldLocked = 0xCF, + OemFieldReturn = 0x1F, + FailureAnalysis = 0x3F, + Bricked = 0x5A, +} + +impl NbootLifecycleDiscriminator { + #[inline(always)] + pub const fn from_raw(raw: u8) -> Option { + match raw { + 0x03 => Some(Self::Develop), + 0x07 => Some(Self::Develop2), + 0x0F => Some(Self::InField), + 0xCF => Some(Self::InFieldLocked), + 0x1F => Some(Self::OemFieldReturn), + 0x3F => Some(Self::FailureAnalysis), + 0x5A => Some(Self::Bricked), + _ => None, + } + } + + #[inline(always)] + pub const fn state(self) -> NbootLifecycleState { + match self { + Self::Develop => NbootLifecycleState::Develop, + Self::Develop2 => NbootLifecycleState::Develop2, + Self::InField => NbootLifecycleState::InField, + Self::InFieldLocked => NbootLifecycleState::InFieldLocked, + Self::OemFieldReturn => NbootLifecycleState::OemFieldReturn, + Self::FailureAnalysis => NbootLifecycleState::FailureAnalysis, + Self::Bricked => NbootLifecycleState::Bricked, + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NbootLifecycleState { + Develop = 0x9635_FC03, + Develop2 = 0x9635_F807, + InField = 0x9635_F00F, + InFieldLocked = 0x9635_30CF, + OemFieldReturn = 0x9635_E01F, + FailureAnalysis = 0x9635_C03F, + Bricked = 0x9635_A55A, +} + +impl NbootLifecycleState { + #[inline(always)] + pub const fn from_raw(raw: u32) -> Option { + match raw { + 0x9635_FC03 => Some(Self::Develop), + 0x9635_F807 => Some(Self::Develop2), + 0x9635_F00F => Some(Self::InField), + 0x9635_30CF => Some(Self::InFieldLocked), + 0x9635_E01F => Some(Self::OemFieldReturn), + 0x9635_C03F => Some(Self::FailureAnalysis), + 0x9635_A55A => Some(Self::Bricked), + _ => None, + } + } + + #[inline(always)] + pub const fn discriminator(self) -> NbootLifecycleDiscriminator { + match self { + Self::Develop => NbootLifecycleDiscriminator::Develop, + Self::Develop2 => NbootLifecycleDiscriminator::Develop2, + Self::InField => NbootLifecycleDiscriminator::InField, + Self::InFieldLocked => NbootLifecycleDiscriminator::InFieldLocked, + Self::OemFieldReturn => NbootLifecycleDiscriminator::OemFieldReturn, + Self::FailureAnalysis => NbootLifecycleDiscriminator::FailureAnalysis, + Self::Bricked => NbootLifecycleDiscriminator::Bricked, + } + } + + #[inline(always)] + pub const fn nboot_soc_lifecycle(self) -> u32 { + let discriminator = self.discriminator() as u16; + (((!discriminator) as u32) << 16) | (discriminator as u32) + } + + #[inline(always)] + pub const fn from_any_raw(raw: u32) -> Option { + if let Some(state) = Self::from_raw(raw) { + Some(state) + } else { + match NbootLifecycleDiscriminator::from_raw(raw as u8) { + Some(discriminator) => Some(discriminator.state()), + None => None, + } + } + } + + /// Returns a monotonic rank for forward-only progression checks. + /// Higher rank = further along the lifecycle. + #[inline(always)] + pub const fn rank(self) -> u8 { + match self { + Self::Develop => 0, + Self::Develop2 => 1, + Self::InField => 2, + Self::InFieldLocked => 2, // Same rank as InField since locking is not a lifecycle progression. + Self::OemFieldReturn => 3, + Self::FailureAnalysis => 4, + Self::Bricked => 5, + } + } + + /// Returns true if advancing to `next` is a valid forward progression (no regressions, no same state). + #[inline(always)] + pub const fn can_advance_to(self, next: Self) -> bool { + next.rank() >= self.rank() && (self as u32 != next as u32) + } +} + +// Sealed trait: CanAdvanceTo + +// Only the combinations listed below exist. Attempting to call +// `cfpa_stage_lifecycle_advance_and_reset` with any other (From, To) pair +// is a *compile* error. + +mod sealed { + pub trait Sealed {} + impl Sealed for super::Develop {} + impl Sealed for super::Develop2 {} + impl Sealed for super::InField {} + impl Sealed for super::InFieldLocked {} + impl Sealed for super::OemFieldReturn {} + impl Sealed for super::FailureAnalysis {} + impl Sealed for super::Bricked {} +} + +// Zero-sized types, one per lifecycle state, used as type parameters to enforce valid lifecycle transitions at compile time. +pub struct Develop; +pub struct Develop2; +pub struct InField; +pub struct InFieldLocked; +pub struct OemFieldReturn; +pub struct FailureAnalysis; +pub struct Bricked; + +/// Compile-time proof that advancing from `Self` to `Next` is a valid transition. +/// Only the explicit `impl` blocks below are permitted. +pub trait CanAdvanceTo: sealed::Sealed {} + +impl CanAdvanceTo for Develop {} +impl CanAdvanceTo for Develop {} +impl CanAdvanceTo for Develop2 {} +impl CanAdvanceTo for Develop2 {} +impl CanAdvanceTo for InField {} +impl CanAdvanceTo for InFieldLocked {} +impl CanAdvanceTo for InField {} +impl CanAdvanceTo for InFieldLocked {} +impl CanAdvanceTo for OemFieldReturn {} +impl CanAdvanceTo for Develop2 {} +impl CanAdvanceTo for InFieldLocked {} +impl CanAdvanceTo for InField {} +impl CanAdvanceTo for OemFieldReturn {} +impl CanAdvanceTo for FailureAnalysis {} + +// Forward declarations of subtables. +#[repr(C)] +pub(super) struct NbootDriverRaw { + // Initialize NBOOT context (must be called before other NBOOT APIs). + pub nboot_context_init: unsafe extern "C" fn(ctx: *mut NbootCtx) -> NbootStatusProtected, + // Deinitialize NBOOT context. + pub nboot_context_deinit: unsafe extern "C" fn(ctx: *mut NbootCtx) -> NbootStatusProtected, + // Set context UUID (bytes; see NXP header for exact semantics). + pub nboot_context_set_uuid: unsafe extern "C" fn(ctx: *mut NbootCtx, uuid: *const u8) -> NbootStatusProtected, + // SB4: load/parse manifest. + pub nboot_sb4_load_manifest: unsafe extern "C" fn( + ctx: *mut NbootCtx, + manifest: *const u32, + parms: *mut NbootSb4LoadManifestParms, + ) -> NbootStatusProtected, + // SB4: load next block. + pub nboot_sb4_load_block: unsafe extern "C" fn(ctx: *mut NbootCtx, block: *mut u32) -> NbootStatusProtected, + // SB4: authenticate & complete check (ROMAPI entry). + pub nboot_sb4_check_authenticity_and_completeness_romapi: unsafe extern "C" fn( + ctx: *mut NbootCtx, + address: *const u32, + parms: *mut NbootSb4LoadManifestParms, + ) -> NbootStatusProtected, + // Authenticate image (signature verification result returned via is_signature_verified). + pub nboot_img_authenticate_romapi: unsafe extern "C" fn( + ctx: *mut NbootCtx, + image_start: *const u8, + is_signature_verified: *mut NbootBool, + parms: *mut NbootImgAuthParms, + ) -> NbootStatusProtected, + + // Enable/Configure in-memory encryption for a given address range. + pub nboot_mem_crypt_enable_encrypt_for_address_range: unsafe extern "C" fn( + ctx: *mut NbootCtx, + region_number: NbootMemCryptRegion, + region_config: *mut NbootMemCryptRegionConfig, + iped_mode_select: NbootMemCryptIpedModeSelect, + ) -> NbootStatusProtected, + // Check whether an operation is allowed for an address range and return flags/IV counters. + pub nboot_mem_crypt_range_checker: unsafe extern "C" fn( + ctx: *mut NbootCtx, + operation: NbootMemCryptOperation, + address: u32, + length: u32, + flags: *mut u8, + npx_iv_erase_cntr: *mut u32, + iped_iv_erase_cntr: *mut u32, + npx_erase_check_en: u32, + ) -> NbootStatusProtected, + // Enable background hashing (ROM-managed hash engine / DMA channel selection). + pub nboot_background_hash_enable: + unsafe extern "C" fn(ctx: *mut NbootCtx, hash_dma_channel: u32) -> NbootStatusProtected, +} + +#[derive(Clone, Copy)] +pub struct NbootDriver { + raw: &'static NbootDriverRaw, +} + +impl NbootDriver { + pub(super) const fn from_raw(raw: &'static NbootDriverRaw) -> Self { + Self { raw } + } + + pub fn nboot_context_init(&self, ctx: *mut NbootCtx) -> NbootStatus { + unsafe { NbootStatus::from_raw((self.raw.nboot_context_init)(ctx)) } + } + + pub fn nboot_context_deinit(&self, ctx: *mut NbootCtx) -> NbootStatus { + unsafe { NbootStatus::from_raw((self.raw.nboot_context_deinit)(ctx)) } + } + + pub fn nboot_context_set_uuid(&self, ctx: *mut NbootCtx, uuid: *const u8) -> NbootStatus { + unsafe { NbootStatus::from_raw((self.raw.nboot_context_set_uuid)(ctx, uuid)) } + } + + pub fn nboot_sb4_load_manifest( + &self, + ctx: *mut NbootCtx, + manifest: *const u32, + parms: *mut NbootSb4LoadManifestParms, + ) -> NbootStatus { + unsafe { NbootStatus::from_raw((self.raw.nboot_sb4_load_manifest)(ctx, manifest, parms)) } + } + + pub fn nboot_sb4_load_block(&self, ctx: *mut NbootCtx, block: *mut u32) -> NbootStatus { + unsafe { NbootStatus::from_raw((self.raw.nboot_sb4_load_block)(ctx, block)) } + } + + pub fn nboot_sb4_check_authenticity_and_completeness_romapi( + &self, + ctx: *mut NbootCtx, + address: *const u32, + parms: *mut NbootSb4LoadManifestParms, + ) -> NbootStatus { + unsafe { + NbootStatus::from_raw((self.raw.nboot_sb4_check_authenticity_and_completeness_romapi)( + ctx, address, parms, + )) + } + } + + pub fn nboot_img_authenticate_romapi( + &self, + ctx: *mut NbootCtx, + image_start: *const u8, + is_signature_verified: *mut NbootBool, + parms: *mut NbootImgAuthParms, + ) -> NbootStatus { + unsafe { + NbootStatus::from_raw((self.raw.nboot_img_authenticate_romapi)( + ctx, + image_start, + is_signature_verified, + parms, + )) + } + } + + pub fn nboot_mem_crypt_enable_encrypt_for_address_range( + &self, + ctx: *mut NbootCtx, + region_number: NbootMemCryptRegion, + region_config: *mut NbootMemCryptRegionConfig, + iped_mode_select: NbootMemCryptIpedModeSelect, + ) -> NbootStatus { + unsafe { + NbootStatus::from_raw((self.raw.nboot_mem_crypt_enable_encrypt_for_address_range)( + ctx, + region_number, + region_config, + iped_mode_select, + )) + } + } + + pub fn nboot_mem_crypt_range_checker( + &self, + ctx: *mut NbootCtx, + operation: NbootMemCryptOperation, + address: u32, + length: u32, + flags: *mut u8, + npx_iv_erase_cntr: *mut u32, + iped_iv_erase_cntr: *mut u32, + npx_erase_check_en: u32, + ) -> NbootStatus { + unsafe { + NbootStatus::from_raw((self.raw.nboot_mem_crypt_range_checker)( + ctx, + operation, + address, + length, + flags, + npx_iv_erase_cntr, + iped_iv_erase_cntr, + npx_erase_check_en, + )) + } + } + + pub fn nboot_background_hash_enable(&self, ctx: *mut NbootCtx, hash_dma_channel: u32) -> NbootStatus { + // should enum for DMA channel selection be added? (Answer is yes but which channels??) For now just pass 0 for default channel. By doing an enum, + // we can impose limitations on valid values (e.g. if only 2 channels are supported, etc.) + unsafe { NbootStatus::from_raw((self.raw.nboot_background_hash_enable)(ctx, hash_dma_channel)) } + } +} diff --git a/libs/ec-slimloader-mcxa/src/rom_api/spi_flash.rs b/libs/ec-slimloader-mcxa/src/rom_api/spi_flash.rs new file mode 100644 index 00000000..6c0b6f68 --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/rom_api/spi_flash.rs @@ -0,0 +1,63 @@ +use super::Status; +use crate::error::SpiFlashStatus; +use core::ffi::c_void; + +// LPSPI external flash (SPI NOR/EEPROM) ROM API + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct SpiMemConfigOption { + pub option0: u32, + pub option1: u32, +} + +#[repr(C)] +pub(super) struct SpiFlashDriverRaw { + pub spi_eeprom_init: unsafe extern "C" fn() -> Status, + pub spi_eeprom_read: unsafe extern "C" fn(address: u32, NoOfBytes: u32, buffer: *mut u8) -> Status, + pub spi_eeprom_write: unsafe extern "C" fn(address: u32, NoOfBytes: u32, buffer: *const u8) -> Status, + pub spi_eeprom_erase: unsafe extern "C" fn(address: u32, length: u32) -> Status, + pub spi_eeprom_config: unsafe extern "C" fn(config: *mut u32) -> Status, + pub spi_eeprom_flush: unsafe extern "C" fn() -> Status, + pub reserved0: *mut c_void, + pub spi_eeprom_erase_all: unsafe extern "C" fn() -> Status, +} + +#[derive(Clone, Copy)] +pub struct SpiFlashDriver { + raw: &'static SpiFlashDriverRaw, +} + +impl SpiFlashDriver { + pub(super) const fn from_raw(raw: &'static SpiFlashDriverRaw) -> Self { + Self { raw } + } + + pub fn spi_eeprom_init(&self) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_init)()) } + } + + pub fn spi_eeprom_read(&self, address: u32, no_of_bytes: u32, buffer: *mut u8) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_read)(address, no_of_bytes, buffer)) } + } + + pub fn spi_eeprom_write(&self, address: u32, no_of_bytes: u32, buffer: *const u8) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_write)(address, no_of_bytes, buffer)) } + } + + pub fn spi_eeprom_erase(&self, address: u32, length: u32) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_erase)(address, length)) } + } + + pub fn spi_eeprom_config(&self, config: *mut u32) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_config)(config)) } + } + + pub fn spi_eeprom_flush(&self) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_flush)()) } + } + + pub fn spi_eeprom_erase_all(&self) -> SpiFlashStatus { + unsafe { SpiFlashStatus::from_raw((self.raw.spi_eeprom_erase_all)()) } + } +} diff --git a/libs/ec-slimloader-mcxa/src/verification.rs b/libs/ec-slimloader-mcxa/src/verification.rs new file mode 100644 index 00000000..0a4e96fa --- /dev/null +++ b/libs/ec-slimloader-mcxa/src/verification.rs @@ -0,0 +1,236 @@ +use crate::certificate::derive_image_rkth_pair; +use embassy_mcxa::{peripherals, Peri}; + +use crate::lifecycle::{ + cnsa_enforced, fast_boot_enabled, load_firmware_version_from_cfpa, load_image_key_revocation_from_cfpa, + load_lifecycle_from_cfpa, load_pqc_rotkh_from_cmpa, load_root_key_revocation_from_cfpa, load_rotk_usage_from_cmpa, + load_rotkh_from_cmpa, low_power_authentication_enforced, secure_boot_enforced, +}; +use crate::rom_api::{ + nboot, nboot_bool_is_true, NbootBool, NbootBoolValue, NbootCtx, NbootImgAuthParms, NbootLifecycleState, + NbootRootKeyRevocation, NbootRootKeyType, NbootRootKeyUsage, NbootRotAuthParms, +}; + +macro_rules! verify_info { + ($($arg:tt)*) => { + #[cfg(feature = "verification-logging")] + { + defmt_or_log::info!($($arg)*); + } + }; +} + +macro_rules! verify_trace { + ($($arg:tt)*) => { + #[cfg(feature = "verification-logging")] + { + defmt_or_log::trace!($($arg)*); + } + }; +} + +macro_rules! verify_warn { + ($($arg:tt)*) => { + #[cfg(feature = "verification-logging")] + { + defmt_or_log::warn!($($arg)*); + } + }; +} + +macro_rules! verify_error { + ($($arg:tt)*) => { + #[cfg(feature = "verification-logging")] + { + defmt_or_log::error!($($arg)*); + } + }; +} + +fn is_dev_mode() -> bool { + //TODO + !secure_boot_enforced() + && (load_lifecycle_from_cfpa() + .map(|lc| lc == NbootLifecycleState::Develop) + .unwrap_or(false)) + // //If SB is disabled, lifecycle MUST be in DEV state. TODO + // Note that lifecycle read failure is treated as non-DEV to be safe, but this also means that if CFPA read fails for some + // reason with secboot enforced, we won't allow dev-mode bypass. +} + +/// Verify the authenticity of the image at the given base address using the NBOOT ROM API. This includes initializing the NBOOT context, loading lifecycle and root of trust information from CFPA/CMPA, +/// deriving the image RKTH from the AHAB container, and calling nboot_img_authenticate_romapi. Returns Ok(()) if authentication is successful, or an appropriate BootError if any step fails or if authentication fails. +/// Will ONLY authenticate if CMPA secure boot settings is configured correctly, correct key set (as established by the ROTKH values) is used for signing, and the image is properly signed as an HYBRID (ECDSA + ML-DSA) image. +/// In dev mode, if the RKTH derived from the image does not match the ROTKH in CMPA, it will be copied to the ROTKH to allow authentication to proceed (this allows flexibility in dev mode since keys may not be provisioned yet), +/// but in production mode, a mismatch will cause authentication to fail (to prevent unauthorized images from being authenticated). +pub fn verify_authenticity<'d>( + mut peri: Peri<'d, peripherals::SGI0>, + image_base: *const u8, +) -> Result<(), ec_slimloader::BootError> { + let n_boot_api = nboot(); + let mut ctx: NbootCtx = unsafe { core::mem::zeroed() }; + let mut sig_ok: NbootBool = NbootBoolValue::False as u32; + + verify_trace!("Initializing NBOOT context"); + let context_init_status = n_boot_api.nboot_context_init(&mut ctx); + if context_init_status != crate::error::NbootStatus::Success { + return Err(ec_slimloader::BootError::Authenticate); + } + + let mut parms = NbootImgAuthParms { + soc_RoTNVM: NbootRotAuthParms { + soc_rootKeyRevocation: [ + NbootRootKeyRevocation::Enabled as u32, + NbootRootKeyRevocation::Enabled as u32, + NbootRootKeyRevocation::Enabled as u32, + NbootRootKeyRevocation::Enabled as u32, + ], + soc_imageKeyRevocation: 0, + soc_rkh: [0; 12], + soc_rkh_1: [0; 12], // PQC hash for hybrid keys + soc_numberOfRootKeys: 4, //TODO: Must equal 4 per NXP example code. + soc_rootKeyUsage: [ + NbootRootKeyUsage::All as u32, + NbootRootKeyUsage::All as u32, + NbootRootKeyUsage::All as u32, + NbootRootKeyUsage::All as u32, + ], + soc_rootKeyTypeAndLength: NbootRootKeyType::EcdsaP384Mldsa87 as u32, //FIXED TO THIS because we are CNSA 2.0 compliant. + soc_lifecycle: NbootLifecycleState::Develop.nboot_soc_lifecycle(), // default to DEV, gets updated with real one further below. + }, + soc_trustedFirmwareVersion: 0, + }; + + if let Some(cmpa_rotkh) = load_rotkh_from_cmpa() { + parms.soc_RoTNVM.soc_rkh = cmpa_rotkh; + verify_trace!("RKTH loaded from CMPA"); + } else { + verify_warn!("CMPA ROTKH read failed"); + return Err(ec_slimloader::BootError::RootOfTrust); + } + + // Load PQC ROTKH for hybrid keys + if let Some(cmpa_pqc_rotkh) = load_pqc_rotkh_from_cmpa() { + parms.soc_RoTNVM.soc_rkh_1 = cmpa_pqc_rotkh; + verify_trace!("PQC RKTH loaded from CMPA"); + } else { + verify_warn!("CMPA PQC ROTKH read failed"); + return Err(ec_slimloader::BootError::RootOfTrust); + } + + //Load additional lifecycle state from CFPA/CMPA + if let Some(cfpa_img_key_revocation) = load_image_key_revocation_from_cfpa() { + parms.soc_RoTNVM.soc_imageKeyRevocation = cfpa_img_key_revocation; + } + + if let Some(cfpa_root_key_revocation) = load_root_key_revocation_from_cfpa() { + parms.soc_RoTNVM.soc_rootKeyRevocation = cfpa_root_key_revocation.map(|r| r as u32); + } + + if let Some(cfpa_fw_version) = load_firmware_version_from_cfpa() { + parms.soc_trustedFirmwareVersion = cfpa_fw_version; + } + + if let Some(cmpa_root_key_usage) = load_rotk_usage_from_cmpa() { + parms.soc_RoTNVM.soc_rootKeyUsage = cmpa_root_key_usage.map(|u| u as u32); + } + + if let Some(cfpa_lifecycle) = load_lifecycle_from_cfpa() { + parms.soc_RoTNVM.soc_lifecycle = cfpa_lifecycle.nboot_soc_lifecycle(); + } + + if !is_dev_mode() { + // SecBoot enforcement check is a bit redundant since if SB is not enforced, we should be in dev mode; but keep for consistency of policy checks. + if !secure_boot_enforced() || !cnsa_enforced() || fast_boot_enabled() || !low_power_authentication_enforced() { + verify_error!("Secure Boot policy violation: secure boot enforced={}, CNSA enforced={}, fast boot enabled={}, low power auth enforced={}", + secure_boot_enforced(), cnsa_enforced(), fast_boot_enabled(), low_power_authentication_enforced()); + n_boot_api.nboot_context_deinit(&mut ctx); + return Err(ec_slimloader::BootError::Integrity); + } + } + + let image_header = unsafe { &*(image_base as *const crate::header::VectorAndHeaderRaw) }; + // Parse AHAB container once and derive both RKTH values + let (image_rkth, pqc_rkth) = derive_image_rkth_pair( + peri.reborrow(), + image_base, + image_header.extended_header_offset, + image_header.image_length, + ); + + // Process ECDSA RKTH (traditional) + if let Some(image_rkth) = image_rkth { + let image_rkth_words = image_rkth.as_le_words(); + + verify_info!("Derived image RKTH: {:x}", image_rkth_words); + if image_rkth_words != parms.soc_RoTNVM.soc_rkh { + if is_dev_mode() { + verify_warn!("Dev mode: copying from image RKTH"); + parms.soc_RoTNVM.soc_rkh.copy_from_slice(&image_rkth_words); + } else { + verify_warn!("Production: image RKTH differs; not copying, will call ecdsa_verify anyway"); + //TODO: just return Err() here? + n_boot_api.nboot_context_deinit(&mut ctx); + return Err(ec_slimloader::BootError::RootOfTrust); + } + } else { + verify_trace!("RKTH match"); + } + } else { + verify_warn!("Failed to derive image RKTH"); + n_boot_api.nboot_context_deinit(&mut ctx); + return Err(ec_slimloader::BootError::RootOfTrust); + } + + // Process PQC RKTH (ML-DSA) for hybrid keys + if let Some(pqc_rkth) = pqc_rkth { + let pqc_rkth_words = pqc_rkth.as_le_words(); + + verify_info!("Derived image PQC RKTH: {:x}", pqc_rkth_words); + if pqc_rkth_words != parms.soc_RoTNVM.soc_rkh_1 { + if is_dev_mode() { + verify_warn!("Dev mode: copying from image PQC RKTH"); + parms.soc_RoTNVM.soc_rkh_1.copy_from_slice(&pqc_rkth_words); + } else { + verify_warn!("Production: image PQC RKTH differs; not copying"); + //TODO: just return Err() here? + n_boot_api.nboot_context_deinit(&mut ctx); + return Err(ec_slimloader::BootError::RootOfTrust); + } + } else { + verify_trace!("PQC RKTH match"); + } + } else { + verify_warn!("Failed to derive image PQC RKTH (ML-DSA not found or error)"); + n_boot_api.nboot_context_deinit(&mut ctx); + return Err(ec_slimloader::BootError::RootOfTrust); + } + verify_trace!("begin auth"); + let status = n_boot_api.nboot_img_authenticate_romapi(&mut ctx, image_base, &mut sig_ok, &mut parms); + + for w in parms.soc_RoTNVM.soc_rkh.iter_mut() { + *w = 0; + } + for w in parms.soc_RoTNVM.soc_rkh_1.iter_mut() { + *w = 0; + } + for w in parms.soc_RoTNVM.soc_rootKeyRevocation.iter_mut() { + *w = 0; + } + + n_boot_api.nboot_context_deinit(&mut ctx); + //TODO: does de-init zeroize the context or do we need to do that manually for security? + + match (status, sig_ok) { + (crate::error::NbootStatus::Success, s) if nboot_bool_is_true(s) => { + verify_info!("Hybrid Auth OK"); + Ok(()) + } + (status, _) => { + let boot_error = crate::error::map_nboot_status_to_boot_error(status); + + verify_error!("Auth failed with status {:?}: {:?}", status, boot_error); + Err(boot_error) + } + } +} diff --git a/libs/ec-slimloader-state/Cargo.toml b/libs/ec-slimloader-state/Cargo.toml index d24bd2c1..f1433ca8 100644 --- a/libs/ec-slimloader-state/Cargo.toml +++ b/libs/ec-slimloader-state/Cargo.toml @@ -6,17 +6,17 @@ license.workspace = true repository.workspace = true [dependencies] -crc = "3.2.1" -num_enum = { version = "0.7.4", default-features = false } +crc = { workspace = true } +num_enum = { workspace = true, default-features = false } embedded-storage-async = { workspace = true } defmt = { workspace = true, optional = true } defmt-or-log = { workspace = true } log = { workspace = true, optional = true } -arbitrary = { version = "1.4.2", features = ["derive"], optional = true } +arbitrary = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] -embassy-futures = "0.1.1" +embassy-futures = { workspace = true } [features] defmt = ["dep:defmt", "defmt-or-log/defmt"] diff --git a/libs/ec-slimloader-state/src/flash.rs b/libs/ec-slimloader-state/src/flash.rs index ed3abd72..9809f596 100644 --- a/libs/ec-slimloader-state/src/flash.rs +++ b/libs/ec-slimloader-state/src/flash.rs @@ -87,10 +87,14 @@ impl FlashJournal { /// and are analysed, before reading the next block. /// A larger block size generally improves performance, and needs to be a non-zero multiple of 2 bytes. async fn compute_cache(inner: &mut T) -> Result { - const CHUNK_SIZE: usize = 2; + // Advance by WRITE_SIZE so each entry occupies its own programmable unit (required for + // flash with ECC; re-programming a page without erasing first corrupts ECC, i.e. MCXA...) + //Note that this does mean number of journal entires are reduced. + // Falls back to 2-byte chunk for flash with WRITE_SIZE <= 2 (e.g. exsiting IMXRT WRITE_SIZE=2). + let incremental_offset = T::WRITE_SIZE.max(2); - defmt_or_log::assert!(BLOCK_SIZE >= CHUNK_SIZE); - defmt_or_log::assert!(BLOCK_SIZE.is_multiple_of(CHUNK_SIZE)); + assert!(BLOCK_SIZE >= incremental_offset); + assert!(BLOCK_SIZE.is_multiple_of(incremental_offset)); let mut buf = [0u8; BLOCK_SIZE]; let block_count = inner.capacity().div_ceil(BLOCK_SIZE); @@ -103,10 +107,9 @@ impl FlashJournal { let slice = &mut buf[0..block_end - block_start]; inner.read(block_start as u32, slice).await?; - for (chunk_i, chunk) in slice.chunks_exact(CHUNK_SIZE).enumerate() { - // Note(unsafe): we are using chunks_exact and then cast the slice into the same size array. - let chunk: [u8; CHUNK_SIZE] = unsafe { chunk.try_into().unwrap_unchecked() }; - let address = block_start + chunk_i * CHUNK_SIZE; + for (chunk_i, chunk) in slice.chunks_exact(incremental_offset).enumerate() { + let chunk: [u8; 2] = [chunk[0], chunk[1]]; + let address = block_start + chunk_i * incremental_offset; match State::try_new(chunk) { Ok(state) => { result = Cache { diff --git a/libs/ec-slimloader/src/lib.rs b/libs/ec-slimloader/src/lib.rs index ff148a0d..fc781935 100644 --- a/libs/ec-slimloader/src/lib.rs +++ b/libs/ec-slimloader/src/lib.rs @@ -37,12 +37,15 @@ pub trait Board { /// /// Does not return if the boot is successful. /// Yields [BootError] if at any stage the boot is aborted. - async fn check_and_boot(&mut self, slot: &Slot) -> BootError; + async fn check_and_boot(&mut self, slot: &Slot) -> BootError; /// Give up booting into an application. /// /// Either shut down the device or go into an infinite loop. fn abort(&mut self) -> !; + + /// Perform ARM Cortex-M system reset via AIRCR register. + fn arm_mcu_reset(&mut self) -> !; } #[derive(Debug)] @@ -66,6 +69,12 @@ pub enum BootError { Authenticate, /// The underlying NVM threw an error. IO, + /// CMPA/CFPA integrity failure + Integrity, + /// Root of Trust verification failure + RootOfTrust, + /// Operation succeeded but requires retry with different slot + SlotRetryRequired, /// Hashing error, such as an unsupported configuration or a failure in the hashing peripheral. Hash, } @@ -143,9 +152,16 @@ pub async fn start(config: B::Config }; info!("Attempting to boot {:?} in {:?}", intent, slot); - let error = board.check_and_boot(&slot).await; // If this function returns, it implies that the boot has failed. + let error = board.check_and_boot::(&slot).await; // If this function returns, it implies that the boot has failed. warn!("Failed to boot {:?} in {:?} because {:?}", intent, slot, error); + // Handle SlotRetryRequired differently - operation succeeded, just restart + if matches!(error, BootError::SlotRetryRequired) { + info!("Slot copy completed successfully, restarting bootloader for retry"); + board.arm_mcu_reset() // Proper system reset! + } + + // Normal error handling for all other errors (only reached if NOT SlotRetryRequired) // Mark our state as [Failed] if it was not set to be so already. if state.status() != Status::Failed { set_status::<_, JOURNAL_BUFFER_SIZE>(&mut board, &mut state, Status::Failed).await; @@ -157,7 +173,7 @@ pub async fn start(config: B::Config // So attempt to boot the backup for now. info!("Attempting to boot backup in {:?}", slot); - let error = board.check_and_boot(&state.backup()).await; // If this function returns, it implies that the boot has failed. + let error = board.check_and_boot::(&state.backup()).await; // If this function returns, it implies that the boot has failed. warn!("Failed to boot backup in {:?} because {:?}", slot, error); }