Skip to content

Add support for PQC secure boot with MCXA 5xx family of MCUs - #36

Open
alamfarjadf wants to merge 25 commits into
OpenDevicePartnership:mainfrom
alamfarjadf:mcxa-577-bootloader
Open

Add support for PQC secure boot with MCXA 5xx family of MCUs#36
alamfarjadf wants to merge 25 commits into
OpenDevicePartnership:mainfrom
alamfarjadf:mcxa-577-bootloader

Conversation

@alamfarjadf

@alamfarjadf alamfarjadf commented Apr 2, 2026

Copy link
Copy Markdown

Adding ROM API support for MCXA 5xx family with Post-Quantum Cryptography hybrid secure boot (ML-DSA-87 and EC-DSA P384).

Added ec-slimloader-mcxa under libs and mcxa-577 examples (bootloader and blinky).

Validated on MCXA577 eval kit with hybrid signed AHAB MBI images which successfully authenticate and boot up.

Not yet added to PR: Imaging tools for MCXA.

BSD-3 license is acceptable (e.g. NXP-PAC), need to update deny.toml.

To-dos:

  1. Move journal to external flash with flexSPI IP.
  2. Use memory.toml for memory mapping.
  3. Split MCXA ROM APIs as independent library under /libs
  4. Update readme.

@jerrysxie jerrysxie assigned jerrysxie and unassigned jerrysxie Apr 7, 2026
Comment thread libs/ec-slimloader-mcxa/src/lib.rs
@alamfarjadf
alamfarjadf force-pushed the mcxa-577-bootloader branch from 08f268c to f185706 Compare April 27, 2026 20:14

@diondokter diondokter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any split image support yet? If there is I can't find it (so please point me to it 😊)
Or are we assuming the ROM memory mapped it for us already?

Comment thread examples/mcxa-577app/bootloader/.cargo/config.toml Outdated
Comment thread libs/ec-slimloader-imxrt/src/lib.rs Outdated
Comment thread libs/ec-slimloader-mcxa/src/certificate.rs
Comment thread libs/ec-slimloader-mcxa/src/jump.rs Outdated
Comment on lines +134 to +143
let flash = InternalFlash::new();
let journal = match FlashJournal::new::<JOURNAL_BUFFER_SIZE>(flash).await {
Ok(journal) => journal,
Err(_) => {
mcxa_error!("Critical: failed to initialize flash journal");
loop {
cortex_m::asm::wfi();
}
}
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we weren't putting the journal in internal flash?
Or do we have a custom board impl for that? (privately)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is an update that's needed here. Journal should be the first partition in external flash, since the internal flash doesn't support multiwrite

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How opinionated should this be? Currently this is a very specific implementation. Another mcxa user might want to store things in different locations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, agreed this is too opinionated. I think we should follow a similar method as was done on the RT6 part, where we can specify these using the partition TOML

@JamesHuard

Copy link
Copy Markdown
Contributor

Is there any split image support yet? If there is I can't find it (so please point me to it 😊) Or are we assuming the ROM memory mapped it for us already?

The image header that the NXP ROM APIs/ROM Loader use has a section for identifying multiple images. The thought here is that it'd be a 'no-op' from an implementation perspective of the SBL firmware to support split-image, because it's already baked into the infra. The change then is in the build tooling to actually generate these additional fields and hashes.

That's also where the open question is to NXP around if the secondary image(s) are simply hash verified or included in the full image signature block.

@alamfarjadf

Copy link
Copy Markdown
Author

Is there any split image support yet? If there is I can't find it (so please point me to it 😊) Or are we assuming the ROM memory mapped it for us already?

@diondokter There is no split image support yet.

From my understanding, the split image on the verification side should be opaque to the caller, IF the image is built correctly (The idea here is that the second executable occupies the area for the CRC binary and is hashed and verified automatically by the APIs). But that idea hasn't been validated yet partly because I haven't been able to generate a split image binary yet.

We have reached out to NXP for further clarification and are awaiting their response.

When we have a split binary, the API will use the single AHAB container to hash both binaries and validate them, so infrastructure needed is mostly on the artifact generation side (which is not published as part of this public PR yet but available in private repos).

@diondokter diondokter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the responses so far!

What's the reason for trying to massage ec-slimloader into supporting both chips and a radically different way in how the code is executed?

The non-imxrt6xx parts are pretty small. All ec-slimloader does is a little bit of in-flash state management, verifying the image and then jump to it.

But for mcxa, the verification is different. And because we're gonna be running in flash, we need to be able to swap partitions, which means that the state management needs to change too. All that's left is doing the boot jump which is like 20 lines of code.

So why take on the complexity of making everything share the same codebase and structure?

Comment thread libs/ec-slimloader-mcxa/src/rom_api.rs Outdated
Comment thread libs/ec-slimloader-mcxa/src/rom_api.rs Outdated
@diondokter

Copy link
Copy Markdown
Member

Ok, I've been trying to build this PR and that just doesn't work.

@JamesHuard

Copy link
Copy Markdown
Contributor

Thanks for the responses so far!

What's the reason for trying to massage ec-slimloader into supporting both chips and a radically different way in how the code is executed?

The non-imxrt6xx parts are pretty small. All ec-slimloader does is a little bit of in-flash state management, verifying the image and then jump to it.

But for mcxa, the verification is different. And because we're gonna be running in flash, we need to be able to swap partitions, which means that the state management needs to change too. All that's left is doing the boot jump which is like 20 lines of code.

So why take on the complexity of making everything share the same codebase and structure?

This is a fair question.

The main thing I think we want is a common interface to the boot sequencing, particularly the journal management and rollback behaviors we want to be 'the same'. We'd like to have a common 'entry point' for various products/OEM lines that give a standard set of behavioral guarantees, even if the underlying hardware mechanisms (secure signing checks, memory layouts, extra steps) are radically different. Almost all, or perhaps all, of this is achieved through the Journal and banking management.

If the current structures/interfaces are too restrictive and getting in the way, then let's evolve. If that means a new interpretation of 'ec-slimloader' as just a trait and then some chip-specific impl's of that trait, I think that's fine.

What do you see as the lowest friction approach here? From my "500-meter high" view, I'd think the basic bootloading process is 'the same', that is:

  1. Setup hardware
  2. Read journal state
  3. Either load valid image, or engage rollback recovery

2&3 being 'the same' is the useful thing here. That could easily be a small helper function on top of a trait that consumes the journal and hardware instances, which I think isn't too far off from what we have today, but there might be too much coupling here if it adds friction. I'm totally open to restructuring to make our job here easier

@alamfarjadf

alamfarjadf commented Jun 15, 2026

Copy link
Copy Markdown
Author

Ok, I've been trying to build this PR and that just doesn't work.

For 1, The embassy dependencies have not been updated in a while.

Added gitignore.

For 3, The CI build for that branch fails because chip JSON files in NXP-PAC are missing the SGI clock and power gating attributes. I was going to update them but never got the time. Once the SGI PAC is re-generated with the attributes, it will build successfully (does so in local).

@diondokter

Copy link
Copy Markdown
Member

Validated on MCXA577 eval kit with hybrid signed AHAB MBI images which successfully authenticate and boot up.

@alamfarjadf do you have a script lying around with which you did this? Or do you have it written down.
If not, I have to duplicate the time looking into this

@alamfarjadf

alamfarjadf commented Jun 22, 2026

Copy link
Copy Markdown
Author

Validated on MCXA577 eval kit with hybrid signed AHAB MBI images which successfully authenticate and boot up.

@alamfarjadf do you have a script lying around with which you did this? Or do you have it written down. If not, I have to duplicate the time looking into this

@diondokter Are you interested in the AHAB signed image generation process?

Yes, I have a CLI tool that does this. I just gave you access to the private repo with the tool (ahab-mbi-tool). Build it, run the .exe with the --build path and the blinky BIN file and you will be able to generate a signed MBI image.

Caveats:

You need ECDSA keys (public and the corresponding signer) and MLDSA-87 keys. The current MLDSA keys are from TSS' auto-signer CLI tool (which you would need to separately get access to). For signing the image, you can use the tool to sign ECDSA and MLDSA.

OR are you simply interested in the signed binaries to flash? On an "un-provisioned" eval. kit, you can flash both binaries (BL and blinky app) in the corresponding flash locations and it should validate and boot (dev mode).

To provision the MCU, refer to mcxa-provisioning part of this PR.

@alamfarjadf
alamfarjadf marked this pull request as ready for review August 11, 2026 22:15
@alamfarjadf
alamfarjadf requested a review from a team as a code owner August 11, 2026 22:15
@jerrysxie jerrysxie self-assigned this Aug 13, 2026

@JamesHuard JamesHuard left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand there's a desire to merge this in quickly to unblock follow up workstreams, and while some of the changes we'll need I could see us easily following up on, I can't in good conscience sign off on this until we're at least sure the changes introduced to the generic components that touch IMXRT aren't themselves breaking changes for those platforms. Those at minimum need to be addressed prior to check in, IMHO

Comment thread examples/mcxa-577app/app/src/main.rs Outdated
#[cfg(any(feature = "defmt", feature = "log"))]
info!("CMPA and CFPA are erased, will provision with initial state, will reset");
// causes a reset, so we will not return from this function. on next reset, IFR will have been provisioned and we will continue to bootloader.
match set_ifr_initial_config_and_reset() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this valid for any potential app example platform we might run on? For example, does this work as-is for EVK?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, tested on 577 EVK, no change needed out of the box running in internal. But EVK will use port A for external flash, this configures port B. So you'd run from internal only on an EVK.

Product DKs (B** / W**) also work out of the box but they are already on port B.

@@ -0,0 +1,134 @@
# Custom probe-rs chip description for MCXA577.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these changes go upstream to MCXA.yaml?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be ideal, yes.

}

async fn check_and_boot(&mut self, slot: &Slot) -> BootError {
async fn check_and_boot<const JOURNAL_BUFFER_SIZE: usize>(&mut self, slot: &Slot) -> BootError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The internal flash journal trait was changed, so to satisfy compile errors it was added. Once journal goes back to external flash this should be gone.

@@ -0,0 +1,330 @@
#![allow(clippy::not_unsafe_ptr_arg_deref)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs in its own ROM API crate, IMO.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this is one of the listed to-dos.

Comment thread README.md
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but both of these second and third points are still true?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely botched the README... oops.

I have to redo the whole thing.

Comment thread README.md
* 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reframing the doc here seems like a meaningless change?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README is screwed up, redoing it.

Comment thread README.md
* 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw delete of content here but leaving the header doesn't make much sense to me. If this is not consistent across platforms, then lets document that accordingly

Comment thread README.md
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, seems odd to selectively change just this line but not the whole document

Comment thread README.md

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh you might want to just scrub the changes to this file, it's hard to see what the reason was for each independent edit, as they don't roll into the surrounding context meaningfully

@alamfarjadf

alamfarjadf commented Aug 13, 2026

Copy link
Copy Markdown
Author

I understand there's a desire to merge this in quickly to unblock follow up workstreams, and while some of the changes we'll need I could see us easily following up on, I can't in good conscience sign off on this until we're at least sure the changes introduced to the generic components that touch IMXRT aren't themselves breaking changes for those platforms. Those at minimum need to be addressed prior to check in, IMHO

Yes, IMXRT is untested. But the scope of impact to IMXRT is limited via the ec-slimloader crate itself, not libs/IMXRT.

Update: Working on integrating mcxa.rs External journal so all changes impacting RT6 can be undone.

Co-authored-by: Jimi Huard <james.huard@microsoft.com>

@jerrysxie jerrysxie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done with the review yet, posting comments I have so far.

// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For MCXA, WRITE_SIZE is 128, so each entry occuppies 128 bytes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't a 2 byte write be rejected if WRITE_SIZE is 128? That is, force the user to pass a 128-byte slice where the first 2 bytes contain the state and the remaining 126 are set to 0xff?

@@ -87,10 +87,14 @@ impl<T: NorFlash> FlashJournal<T> {
/// 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<const BLOCK_SIZE: usize>(inner: &mut T) -> Result<Cache, T::Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing test only covers test with small write size, we should consider change the test parameter a bit to cover this new case with 128 bytes write size.

Comment thread libs/Cargo.toml
num_enum = { version = "0.7", default-features = false }
device-driver = "1.0"

[patch.crates-io]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we point to crates-io instead? Does embassy need to do another release?

// Handle SlotRetryRequired differently - operation succeeded, just restart
if matches!(error, BootError::SlotRetryRequired) {
info!("Slot copy completed successfully, restarting bootloader for retry");
cortex_m::peripheral::SCB::sys_reset() // Proper system reset!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: we are baking in a very arch specific intruction here, which will be come a problem if we support other arch with this bootloader.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be removed as part of the design change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind detailing the design change you have in mind? I'd consider this a Blocker for merging this PR.

warn!("Failed to boot {:?} in {:?} because {:?}", intent, slot, error);

// Handle SlotRetryRequired differently - operation succeeded, just restart
if matches!(error, BootError::SlotRetryRequired) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if the backup slot is bad, we would continue copy it into A and reset. This reset/copy loop will wear out the flash. In the old case, if the backup slot fails, we abort. Is there any reason why reset is perferred in MCXA's implementation?

@alamfarjadf alamfarjadf Aug 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the original type 1 design, this will be updated with 3A slot design (no erase/copy, only bank swap), I'm working on stripping out all of this code. This was originally going to be changed as a separate PR after this one is merged, but given the risk to RT6 due to ec-slimloader crate changes, I will make that change now.


// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a NXP reference for everything here?

@alamfarjadf alamfarjadf Aug 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, MCXA Reference manual attachment called IFR.xlxs.

@@ -0,0 +1,23 @@
[package]
name = "mcxa-security-provisioning"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not covered in CI. Is this a library that we want to keep?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Will add to CI, yes this does the provisioning and fusing steps (which are now part of the SBL/app themselves).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that this app should be "complete". That is, it should open the journal and mark it Confirmed. Examples set the patterns folks will end up using.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I will add this.

Comment thread examples/mcxa-577app/app/src/main.rs Outdated
Comment on lines +28 to +30
let input_data: [u8; 256] = core::array::from_fn(|i| i as u8);

for (index, byte) in input_data.iter_mut().enumerate() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input_data is immutable. You can borrow it mutably. This PR was not compiled, was it?

@alamfarjadf alamfarjadf Aug 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was compiled, see CI results for previous commit.

Looked like

 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;
  }

The code block wasn't removed when the from_fn was added. Removing now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write path was not patched for 128 byte writes. Here's a test that causes it to panic:

#[test]
fn journal_write_size_128() {
    let mut mock: MockFlashBase<3, 128, 8> = MockFlashBase::new(None, false);
    embassy_futures::block_on(async {
        let mut journal = FlashJournal::new::<256>(&mut mock).await.unwrap();
        let state = State::new(Status::Initial, Slot::try_from(2).unwrap(), Slot::try_from(1).unwrap());
        journal.set::<256>(&state).await.unwrap();
        assert_eq!(journal.get(), Some(&state));
    });
}

Comment on lines +844 to +848
if let Some(current) = load_lifecycle_from_cfpa() {
if !current.can_advance_to(next) {
return Err(CfpaWriteError::LifecycleRegression);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you skipping a verification that current is equal to From? It seems like it. That would mean we can trick the system into transitioning to states we didn't want.

@alamfarjadf alamfarjadf Aug 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nboot.rs line 296

    pub const fn can_advance_to(self, next: Self) -> bool {
        next.rank() >= self.rank() && (self as u32 != next as u32)
    }

We allow equal weight but not identical LC advances (e.g. Develop to Develop 2, but not Infield to Infield).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants