Feat/mcu platform foundation#18
Merged
Merged
Conversation
Start the MCU platform with a clean, auditable TrustZone runtime partition for the STM32U545. The board must be segmented before any application logic, so this lays the workspace and the partition sequence first and proves it on host. Add three crates next to the driver. platform is a no_std library that programs the runtime isolation, secure and nonsecure are the two thin TrustZone binaries that link to real thumbv8m ELF images. The C mcmse NSC veneer shim is deferred until the linker and toolchain wiring lands. The partition sequence is the heart of the change. apply_partition runs the ordered bring-up from the secure side, enabling the GTZC clocks first, then the SAU regions, the TZSC peripheral security, the MPCBB SRAM blocks, the GPIO security, the GPDMA secure channels, the secure MPU, the development TZIC, and the configuration locks last. Order is load bearing, so each hazard is encoded as a test. The SE SPI link and the whole crypto block are marked secure, USB and the touch sensor stay non-secure, the secret SRAM is secure, and the SE SPI DMA channels are secure so a non-secure transfer cannot reach them. No irreversible option byte is ever written. TZEN, RDP, BOOT_LOCK, WRP and the flash option bytes are out of scope here and gated behind the hardware power-fault test. The sequence runs against a RegisterBus abstraction so a host mock records the exact ordered writes and the tests assert both order and value. The real MmioBus does volatile MMIO and is the only place unsafe appears, each access carrying a SAFETY note. The register definitions are hand rolled and minimal, every address and bit traced to the Armv8-M manual, PM0264 or RM0456, and a separate test module pins every constant to a hard coded literal so a wrong address cannot pass unnoticed. apply_partition builds and validates the region table before touching any register and fails closed, and the secure binary wedges forever rather than hand off to the non-secure world on a partition error. The workspace keeps unsafe_code forbidden by default. Only the three crates that touch MMIO downgrade it to deny in their own lint table, so unsafe stays quarantined to the crates that provably need it and every block is justified. Gates pass on host and on thumbv8m. cargo check and cargo clippy are clean on both targets, the platform suite is green with the partition order and the constant pins covered, and both binaries link.
Make the non-secure world reachable and give it one controlled gateway into the secure world, with no application logic yet. This turns the provisional skeleton layouts into the real secure and non-secure memory split and proves a secure gateway call links across the boundary. Add the C mcmse non-secure-callable shim and wire it into the Cargo build. The secure build script compiles secure_nsc.c with clang for the Cortex-M33 in cmse mode through the cc crate, and the secure link emits a CMSE import library with rust-lld so the non-secure image can resolve the gateway without any access to the secure binary. clang was chosen so the whole compile, static-analysis and link chain stays one LLVM toolchain, and the shim carries a clang-tidy cert and hardening gate. The shim holds one coarse value-in value-out gateway, patinakey_nsc_version, with zero logic, no secret, no pointer and no non-secure function pointer by construction. The non-secure binary declares it and calls it once so the link must resolve the gateway, which is the proof that the boundary works. Replace the provisional memory scripts with the real split. The secure image owns flash bank one at the secure alias and the lower secure SRAM, the non-secure image owns flash bank two and the upper SRAM, and the secure gateway window sits at the top of the secure bank where the gateway stubs are pinned. cortex-m-rt emits the gateway stub section without a fixed address, so the stub is pinned to the window with a section-start link argument and kept against section garbage collection with an explicit root. The two binaries build in two stages because no Cargo dependency edge orders two binary crates, so the non-secure build fails loudly with an actionable message if the secure import library is missing, and the documented order is to build secure first. Complete the secure side of the hand-off. After the partition applies, the secure world points the non-secure vector table base at flash bank two, loads the non-secure main stack pointer from the first vector, and branches to the non-secure reset entry with bxns. The hand-off is reachable only on the partition success path, so a partition fault still wedges the secure world and never starts the non-secure world. Add the previously missing non-secure SAU region for the RSS library so a non-secure call into it does not fault. The region is scoped to the minimal RSS library non-secure function-pointer table that the non-secure world must read to dispatch RSS library calls, taken from the device function-pointer-table bounds per RM0456 section 3.6.2 and pinned by a test. Least privilege is deliberate, the region excludes the bootloader code, the one-time-programmable area, the reserved gap and the flash ECC test word, so the non-secure world can read none of them. Gates pass on host and on thumbv8m. cargo check and cargo clippy are clean on both targets, the platform suite is green at thirty three tests with the new system-memory region pinned, the two-stage build links both ARM ELF images, the non-secure image resolves the gateway into the gateway window, the secure image carries the secure gateway stub at the window with the expected sg and branch, and the hand-off emits the non-secure stack-pointer write and bxns.
Program the banked secure-bank Armv8-M MPU so the secure world runs under write xor execute and data execution prevention. Secure code is read-only and executable, secure data and the secure peripheral aliases are read-write and execute-never. This completes the runtime isolation of the platform on top of the existing SAU and GTZC partition, and touches no irreversible or lifecycle bit. It is runtime configuration only. Add the mpu module to the platform crate. It mirrors the SAU region shape, an MpuRegion built only through a validated constructor that rejects a misaligned base or limit and an inverted range before any hardware write, with const RBAR and RLAR encoders. The region table holds three regions over the addresses the core actually emits in the secure state, the secure flash bank one alias for code, the secure half of SRAM1 for data, and the secure peripheral alias range, matching the secure linker layout rather than the alternate SRAM aliases. The apply function builds and validates the table first, then disables the MPU, writes the memory attributes with one Normal and one Device entry, programs each region in region-number order, and enables the MPU with the hard-fault and non-maskable-interrupt protection kept on. PRIVDEFENA is left clear, so there is no background map and every secure access must hit one of the three regions or fault, which is strict least privilege. The secure SRAM region deliberately covers only the secure half of SRAM1, the memory the secure binary uses. SRAM2 and SRAM4 stay secure under the GTZC partition but are intentionally not mapped, so the secure core cannot reach them at all until a future region is added with its linker change. Every register address, field shift and attribute byte is pinned to a hard-coded primary-source literal, and the sequence is host-tested through the recording bus for the exact ordered write trace, the write xor execute invariant, and the never-set background-map bit. Wire the MPU into the secure binary as the last isolation step before the hand-off. The secure entry reads the non-secure stack pointer and reset vector and points the non-secure vector table base while the MPU is still off, then applies the secure MPU, issues the data and instruction barriers so the new configuration takes effect, and only then branches to the non-secure world with bxns. No secure data access happens between the MPU enable and the branch, only secure-flash instruction fetch and register operations, so the hand-off needs no non-secure region in the secure MPU. A failure to apply the MPU wedges the secure world and never starts the non-secure world, preserving the fail-closed contract. Gates pass on host and on thumbv8m. cargo check and cargo clippy are clean on both targets, the platform suite is green at fifty tests, up from thirty three, and the two-stage build links both ARM ELF images.
Wire the whole non-secure to secure to TROPIC01 chain so the board can be flashed once and the chain watched end to end over defmt-rtt. The secure world drives the secure element on SPI1 in a blocking transfer, and the non-secure world reaches it through coarse value-out secure-gateway veneers. This is the bridge to the on-silicon driver test. It touches no irreversible or lifecycle bit, opens no secure channel, and moves no secret. It is runtime configuration plus the transport, all host-tested, with the silicon proof gated on hardware. Add the mcu-spi crate, a no_std blocking SPI1 driver for the secure world built on the same register-access seam as the platform crate. A target-gated MMIO bus is the only unsafe surface, and a scriptable test bus models the SPI status and data registers so the transfer loop is fully host-tested. The crate exposes an embedded-hal SpiDevice as a SPI bus plus a software chip-select on PA4, and a blocking wait provider for the L1 readiness polling. The SPI pins, the alternate function selection, the clock enables and the control-register setup follow the board schematic and RM0456. The driver marks no security bit, the partition already keeps SPI1 and the pins secure. Every register address and field is pinned to a hard-coded primary-source literal. The secure binary gains three extern entries that build a session-less device handle and read the chip mode and the two firmware versions, mapping any error to a fail-closed status word. The non-secure-callable shim forwards to them through three value-out veneers that carry no pointer, secret, or non-secure function pointer. The non-secure binary calls them at boot and logs the chip mode and both firmware versions over defmt-rtt, so the full chain is observable on the probe. The SPI engine is flushed and re-armed at the start of every transaction and on any error path, because only clearing the enable bit empties the receive FIFO, so a faulted transfer cannot leave a stale byte that would shift every later transfer by one byte until reset. The master automatic receive suspension is enabled so a non-secure interrupt that preempts the secure transfer cannot overrun the receive FIFO. Both behaviours are covered by host tests on the scripted bus.
Add the first piece of the MCU self-update path, the single most brick-prone component in the product. This is a pure no_std library that defines the format of a signed firmware image and verifies it against an out-of-band pinned root public key. It writes no flash, touches no option byte, sets no irreversible or lifecycle bit, holds no unsafe, and fails closed on any anomaly. The bricking critical math is not hand-rolled: the signature check is the audited ed25519 primitive, and the only new code is a bounded, fuzzed header parser in front of it. Compiling a public root key into the firmware is fully reversible by a reflash, it is not the irreversible pairing-key write. Define a compact in-house image format rather than a heavier container, because the signer and the verifier are both ours in a closed loop, so an external tooling format buys nothing while its parsing surface and dead paths cost. The layout is a fixed 24-byte header, then the payload, then a 64-byte Ed25519 signature that covers the header and the payload. The header carries a magic tag, a parser schema version, a signature-algorithm byte, the firmware version, a monotonic anti-rollback counter, the payload length, and two reserved bytes. Every multi-byte field is little-endian, matching the Cortex-M33. Every offset and constant is pinned to a literal in a test. Verify fails closed at the first anomaly in a fixed order: a length floor, the magic, the schema version, the algorithm byte, a reserved-bytes-must-be-zero check, an exact total-length check that rejects any overflowing, short, long, or trailing input, then verify_strict over the signed region. verify_strict is chosen over verify so a low-order public key is rejected. The full header is parsed up front into locals, and the result is built only after the signature passes, so no field inside the signed region is ever exposed before verification. The anti-rollback counter and the firmware version sit inside the signed region, so neither can be downgraded without breaking the signature. The signature algorithm is Ed25519 verified in software with a compiled-in pinned key. The STM32U545 PKA does not support Edwards curves, so the verify is software by construction (RM0456 section 53.3.4). A hardware P-256 path on the PKA was weighed and set aside for this stage: its only gain is offload that does not matter for a rare boot-time verify, and it would need an unwritten raw PKA driver and a clocked peripheral, making the verifier depend on init order. The algorithm byte sits inside the signed region so a future second algorithm can be added without a format break, with the hard precondition that the signature length, the key type, and the signed-region split must then be selected by that byte rather than bolted onto the fixed Ed25519 offsets. The dependency is ed25519-dalek 2.2 stable with default features off, verify only, no heap. It pins sha2 0.10 next to the workspace sha2 0.11, a small and accepted second hash copy on this path. The newer dalek release that unifies sha2 was set aside because it pulls a far larger duplicate curve crate and puts a release candidate on the bricking-critical path. A libfuzzer target drives the verifier over arbitrary attacker bytes under a fixed test key and must never panic. The host tests cover a valid round trip, each error variant, the rejection of a tampered anti-rollback counter and a tampered firmware version to prove both are bound by the signature, and a guard that the fuzz seam key is genuinely accepted so the harness cannot silently verify nothing.
…mocked flash seam Introduce crates/fw-update, a no_std host-testable state machine for the MCU's OWN firmware update, the most dangerous path in the product (the brick-the-fleet risk). It depends on image-verify and is proven REVERSIBLY behind a mockable flash seam: it emits no real flash write, no erase, no SWAP_BANK, no option-byte write, no irreversible bit. The real volatile-flash MMIO driver and the power-fault test are a separate hardware-gated step. The dangerous operations exist only as trait methods. FlashSeam covers erase, write_inactive_page, inactive_bank read-back, commit_swap, revert_swap, the flash NVCNT read and bump, the persistent pending record and the boot-count countdown. SeCounterSeam covers the regression-proof secure-element counter. The only implementations in this crate are host mocks, so it is structurally impossible to emit an irreversible operation from here, and the crate keeps forbid(unsafe_code). The inactive bank is the single source of truth. Chunks stream through write_inactive_page into the inactive bank, an explicit begin(total_len) completeness gate rejects a truncated transfer, then verify_and_accept reads the bank back through inactive_bank and verifies exactly the bytes commit_swap will make bootable. Verify and commit act on the same store by construction, so the machine can never commit bytes it did not verify. Commit is the atomic SWAP_BANK-as-commit model: the swap takes effect on the next reset and the core never sees a half-swapped map, so a power loss before the swap commits leaves the old bank booting (RM0456 section 7.5.8). The pending record is Armed with the target bank before the swap is requested. On the next boot the machine compares the running bank against the armed target: a match proves the swap took effect and enters the confirm countdown, a mismatch means the swap never happened so it keeps the old bank and arms no reverse swap. A new image must self-confirm within a fixed number of boots or the machine reverts to the previous bank. No power-loss boundary loses the old bank's bootability. Anti-rollback is a two-gate model. Gate 1 is the flash NVCNT, compared at install time against the image security counter, which sits inside the signed region so it cannot be downgraded without breaking the signature (UM2851). Gate 2 is the secure-element monotonic counter, read and compared at accept time to reject a regression the regression-proof way. The NVCNT is bumped LAST, only at the terminal confirmed transition past the revert decision, so the rollback counter and the revert path are mutually exclusive and the counter can never rise above a bank that ends up booting. Tested 29 ways on the host behind the mocks: the full happy path, a tampered image rejected as a bad signature with no commit, a downgrade and a secure-element regression both rejected with no commit, a truncated transfer rejected, a confirmation timeout reverting to the old bank, a power loss whose swap never took effect keeping the old bank without reverting into the unverified one, and every seam fault failing closed. A detached libfuzzer target drives the receive, verify, commit, boot and confirm path with attacker controlled chunk offsets and lengths and never panics nor commits a rejected image.
…a fidelity flash model Turn the by-hand tracing of the power-loss windows into a machine-checked property. Add a host-only fidelity model of the STM32U5 flash and an exhaustive fault-injection harness over the dual-bank A/B update machine, so every power-loss interleaving is proven to keep a bootable signature-verified image. Pure host, no hardware, no irreversible operation, no real flash or option-byte or SWAP_BANK write. The earlier simple mock could not represent two silicon-only faults. Its page write did a plain copy so it could raise a bit from 0 to 1, impossible on real flash, and its commit flipped the running bank instantly while real silicon stages SWAP_BANK and applies it only at the next reset. So a torn write that survives as corruption and a power loss between arming the swap and the reset were both invisible behind it. The fidelity model reproduces the real semantics, each cited to RM0456 or UM2851. A program clears bits from 1 to 0 only, an erase sets 0xFF, the program unit is a quad-word (RM0456 sec 7.3.1, sec 7.3.7). A torn quad-word reads back as detectable corruption, worst case a double-bit ECC fault, so the verifier rejects the bank (RM0456 sec 7.3.11, sec 7.3.2). SWAP_BANK is staged and applied only at a modelled reset, atomically, and a cut before the option load commits keeps the OLD bank (RM0456 sec 7.4.2, sec 7.5.8). The NVCNT is a monotone burn where a torn bump reads back at the old or new value, never below the old (UM2851 Table 7, Table 8). The model states that the bit-level NVCNT encoding is an inference consistent with stock MCUboot, not a direct manual quote, and that the relied-on property is the floor. The model holds two physically separate bank stores, so the update writes only the inactive bank and the OLD bank is provably untouched. The harness drives the whole flow begin to receive to verify-and-accept to commit to the modelled reset to on-boot to confirm or revert, and injects a power loss at every persistent-mutation boundary. A single global cut index walks every mutation across the reset boundary, so a cut fires in confirm (the SE spend, the pending clear, the NVCNT bump done last) and in revert (the reverse-swap arm, the pending clear) after the reboot, not only before it. After each cut the harness rebuilds the updater from the surviving persistent state, a modelled reboot, runs the recovery to a settled fixed point, and asserts the safety invariant: the OLD bank stays bootable until a swap is irrevocably confirmed, the booting bank always verifies and an unverified image never commits or boots, the NVCNT never rises above the booting bank counter, and no recovery leaves a staged swap pointing at the unverified bank. A per-index census asserts every reachable mutation index actually fired, the check that catches a cut span with a gap. A separate run streams a bad-signature image and proves it never reaches a confirmed swap at any cut. A dedicated case drops the secure-element channel at the spend and proves the recovery spends the counter exactly once with no half-confirmed strand. The harness adds no parser of attacker bytes. The image bytes still go through image-verify, which has its own fuzz target, and the chunk-offset path is already exercised by the drive_machine fuzz target. The hardware power-fault test plan that this model feeds is written as local knowledge for the future on-bench run on the NUCLEO-U545RE-Q and the PatinaKey board, which remains the hard gate before any RDP2 or BOOT_LOCK.
Builds the missing half of the verified update path. image-verify only VERIFIES, so until now a signed image could be produced only by the inline test fixtures. This adds the host tool that emits the signed-image format and signs HEADER || PAYLOAD with an Ed25519 private key, so real images can flow through the production signing path. The on-wire layout now has a single source of truth: image-verify gains a no_std header encoder behind a new `encode` feature, enabled only by the tool and off in every firmware build. The tool composes the header through that encoder, signs through a pluggable backend, appends the 64-byte signature, then RE-VERIFIES its own output with verify_image so a malformed image can never leave the tool. The signer lives in a detached workspace under tools/image-signer (its own empty workspace table) so it can never be pulled into the thumbv8m target build or pollute the no_std firmware. It is a lib plus a thin bin. The lib exposes an ImageSigner backend trait so a future hardware-token backend drops in without rework, with only a SoftwareSigner implemented today. Argument parsing is hand-rolled over std::env::args with no parsing dependency. The private key is a raw 32-byte Ed25519 seed, never taken as a literal argument, never hardcoded, never committed. It is supplied through --key-file, either as a path (--key-file <PATH>) or piped from gpg --decrypt on stdin (--key-file -), the cleanest production form because the seed never lands as a cleartext file. The seed is held in a zeroizing buffer and is never printed. The dev keypair already exists as the all-0x01 seed whose public key is the pinned DEV_ROOT_KEY, so no new key material and no random generation are introduced. The sign subcommand takes a payload binary, the version fields, the security counter, and a key source, and emits the signed image. An optional --expect-pubkey flag fails closed before writing if the key does not match the intended pinned public key, guarding the wrong-key-file operator error. The derive-pubkey subcommand prints the public key to pin. Every bad input fails closed with a clear message and a non-zero exit. Output to a closed pipe (for example piping into head) exits cleanly rather than panicking, and the seed and signer are dropped and zeroized before any such exit can fire. An end-to-end test signs a payload with the dev seed, confirms verify_image accepts the exact bytes, and drives them through the fw-update machine to a confirmed swap, with a sibling proving a non-dev-signed image is rejected. The signer crate takes fw-update and image-verify as path dev-dependencies for this test, an edge that stays out of the MCU target build. The host tool produces images and parses no attacker-controlled chip input, so no new fuzz target is added. The chip-side parsing stays in image-verify which is already fuzzed.
Implements the dual-bank A/B FlashSeam the fw-update machine consumes, as a hand-rolled raw-MMIO STM32U545 embedded-flash driver, host-tested against a faithful FLASH-controller model. This is the last host-buildable piece of the firmware-update path, and it pins the real bank geometry from RM0456 ch.7. The new crate crates/mcu-flash provides a FlashAccess register seam, a target-gated MmioFlash (the crate's only unsafe, each block with a SAFETY note), a regs module pinned to RM0456 literals, and Stm32FlashSeam mapping the seam to the controller sequences: page erase, quad-word program (clearing bits only, fail closed on PROGERR / SIZERR / WRPERR / PGSERR), the inactive bank read back as the exact bytes a commit boots, the NVCNT and the pending / boot-count and a new update-outcome record, and the full commit / revert option-byte path. The driver is physical-bank-centric. SWAP_BANK remaps the bank addresses but the BKER erase selector and the SECWM / WRP watermarks stay bound to the physical bank (RM0456 sec 7.5.8), so one helper pairs a physical bank with the live OPTR.SWAP_BANK to yield both the BKER bit and the mapped base, and erase and program can never target different physical banks. The NVCNT and the metadata live in a fixed physical Bank 1 band and re-derive their mapped address from SWAP_BANK on every access, so they survive the swap. The host model holds two physical stores whose address-to-store mapping flips at a modelled reset, so a bank-divergence or a metadata-lost-across-swap is observable rather than hidden. An integration test streams a real signed image through the public Updater, models the swap, and reads the NVCNT back from the correct physical bank. Brick-safety is by construction. The option-byte path (OPTR SWAP_BANK plus OPTSTRT plus OBL_LAUNCH) is the real register sequence but lives only in the target-gated MmioFlash, never compiles on the host, and is never invoked by any build or test. Nothing auto-cables an irreversible operation. The real on-silicon flip and the lifecycle lock stay gated on the hardware power-fault proof. fw-update gains the UpdateOutcome seam record so a future boot-stage can signal a non-silent auto-revert, and its fidelity model and mock track the same two-store discipline. No behaviour of the A/B machine, the two-gate anti-rollback, or the power-fault harness changes.
…lus RTT Adds firmware/scripts/bringup.sh, a runner over probe-rs for the TrustZone two-image build. It builds the secure then nonsecure crates in the required order, flashes the secure image at the 0x0C00_0000 secure alias and the nonsecure image at 0x0804_0000, then attaches the defmt-RTT decoder live, all in one command. A detect subcommand does read-only probe and chip identification. The chip target is STM32U545CEUx, confirmed against the probe-rs registry with a flash and RAM map that matches the firmware memory layout exactly. The attach steps carry a retry (ATTACH_RETRIES, default 4) because the ST-LINK connect-under-reset sequence is intermittently flaky on the first attempt, especially against a core spinning in a fault from a prior run. It writes no option byte and never touches TZEN, SECWM, RDP, BOOT_LOCK, WRP, or any irreversible state. Any lifecycle or provisioning write stays a separate step.
…CFG2 configure_spi programmed SPI1 master mode by writing CR1.SSI (bit 12) and MASRX (bit 8) AFTER the CFG2 write that sets MASTER (bit 22) and SSM (bit 26). With software slave management the internal slave-select follows SSI, so the window between those two writes left the master selected low, which arms a mode fault (MODF, SR bit 9). A mode fault hardware-clears MASTER and SPE, dropping the master back to slave mode where it never drives SCK, so every transfer stalled and surfaced as L1 Bus upstream. On the newer U5 SPI a latched mode fault is cleared only by writing 1 to IFCR.MODFC (bit 9), not the legacy read-SR-then-write-CR1 sequence, and the per-transaction IFCR clear handled only EOTC, TXTFC, and OVR, so a once-latched fault would never clear. The host recording mock did not model MODF, so the fault was invisible to host tests (silicon-only class). Write CR1.SSI and MASRX BEFORE the CFG2 MASTER+SSM write, so the internal slave-select is high before the master is ever selected and the mode-fault window never opens. The SPE=0 precondition still comes first (CFG1 and CFG2 are write-protected while SPE is set). Add SPI_IFCR_MODFC and OR it into the per-transaction IFCR clear as defense in depth, so a latched fault is cleared each transaction. This same configure_spi is the secure-world path, so the fix also unblocks the secure world on silicon. To make the silicon transition observable on the host, the recording bus now models the one MODF transition (latch SR.MODF and clear MASTER+SPE when CFG2 selects MASTER+SSM while SSI is low, clear it on an IFCR.MODFC write). The model mutates only the config-readback map, not the recorded write log, so the order and value assertions stay honest. Three tests cover the write order (SSI before MASTER), the absence of a latched fault after init, and the per- transaction MODFC clear. The mode-fault test is non-vacuous: it fails against the old ordering and passes against the fix. Bit positions SR.MODF and IFCR.MODFC are both bit 9 per RM0456 Rev 7 sec 68.8.6 and sec 68.8.7.
Add a `se-fw-update` cargo feature (OFF by default) that updates the TROPIC01 secure element from factory 1.0.0 to CPU 2.0.0 / SPECT 1.0.0, driven from the secure world over a new NSC veneer. When the feature is off the product firmware is byte-unchanged, the update code and the vendor blobs compile out. The secure veneer patinakey_nsc_se_fw_update drives the driver primitives step by step (enter bootloader, update both bank pairs, exit to Application, verify the running versions) so the returned status word names the exact failing step, and the non-secure side logs the outcome plus the new versions over RTT. The step granularity also reports, per step, whether a re-run recovers. Brick-safety envelope, enforced and reviewed: the path issues no R_Config write of any kind, so it cannot trip the factory-part permanent-Alarm-Mode errata, and it never disables Maintenance, so an interrupted update stays in Maintenance mode where a re-run re-flashes and recovers. The blobs are fed verbatim. The update runs only when the feature is built on and the veneer is called, never on a normal boot, the Rust cfg, the clang define, and the sgstubs EXTERN are all keyed off the one feature. The signed blobs are gitignored (vendor firmware), a feature-on build requires them staged under crates/secure/fw_blobs/.
se_error_code collapsed every L1 fault to 0x11 and every L2 fault to 0x12, which hid the real cause during silicon bring-up. Break them out so the non-secure log names the exact fault: the L1 sub-variants get distinct sentinels (Bus 0xE4, ChipBusy 0xE5, Alarm 0xE6, BadChipStatus 0xE7), the L2 transport faults get 0xE1 Crc / 0xE2 BadFrame / 0xE3 ShortFrame, and an L2 Status passes the raw chip status byte through. Also map the new SeError::RebootUnsuccessful to 0x63. This granularity is what surfaced the opaque bank-write failure as the readable UnknownErr 0x7E that located the root cause. Shared by both the smoke and the fw-update veneers so both encode errors the same way.
…ECC crypto on silicon Add a feature-gated (se-session, OFF by default) bring-up surface of two value-out NSC veneers driven from the secure world over SPI1. Veneer 1, patinakey_nsc_se_session_ping (crates/secure/src/se_session.rs): read STPUB from the chip X.509 cert store, open a Noise KK1 session against the factory pairing slot 0 (the libtropic SDK default production SH0 keys, public constants), run one encrypted L3 Ping with an exact echo compare, tear down with the chip-notifying abort. Granular status word: bit 31 ERR with the failing step (1 read-stpub, 2 open-session, 3 ping, 4 abort) and the error code low byte (0xF0 reserved for an echo mismatch), bit 8 OK with marker 0x51. Veneer 2, patinakey_nsc_se_crypto (crates/secure/src/se_crypto.rs): verify the full 4-cert chain from the cert store against the PINNED Tropic Square Root CA v1 public key (serial 301, P-521 SEC1 point embedded, validated on-curve), open the session with the VERIFIED STPUB via a shared open_bringup_session helper, then under the session: 32 random bytes with a non-constant sanity check, Ed25519 keygen in an erasable slot, EdDSA sign of a fixed message, ed25519-dalek verify_strict of that signature on the host side of the chip boundary, checked erase, ECDSA P-256 keygen + sign of a fixed 32-byte digest with a shape check (no P-256 verifier linked, the cryptographic verify completes with the hardware accelerator work), checked erase, chip-notifying abort. Steps 1-11 in the status word, reserved codes 0xF1 EdDSA reject, 0xF2 random sanity, 0xF3 ECDSA shape, 0xF4 pubkey length, OK marker 0x52. Tolerant pre-clean erases before both keygens mirror the vendor reference sequence. Wiring mirrors the se-fw-update pattern: feature-gated mods, build.rs appends the sgstubs EXTERNs and the clang define, secure_nsc.c holds both forwarders in one ifdef block, the nonsecure side decodes both words to defmt-RTT and adds a bounded RTT drain spin before wfi (feature-on only). The feature enables tropic01-driver/attestation and an optional ed25519-dalek. se_smoke.rs se_error_code gains a Chain arm (0x31) plus a totality wildcard (0x7F, allow(unreachable_patterns)): cargo feature unification can compile the driver Chain variant in independently of this crate's features, and a crate cannot cfg on another crate's feature. This also turns the whole-workspace target build green. Proven on silicon, one flash, both veneers: "SE L3 session + Ping OK, marker 0x51" then "SE crypto + attestation OK, marker 0x52", on chip FW CPU 2.0.0 / SPECT 1.0.0.
…he secure to non-secure data-return channel Extend the feature-gated se-session with two more value-out NSC veneers and the secure MPU support they need. se_persist (marker 0x53) proves the persistent-but-reversible SE commands under a session: monotonic counter init, get, and update including the at-zero boundary and an upward re-init, MAC-and-Destroy re-initialization determinism, and an ECC key-store Ed25519 known-answer test (import the RFC 8032 test seed, compare the chip-derived public key, verify the signature strict on-host, erase, and confirm a post-erase sign fails). No provisioning write is issued. se_readonly (marker 0x54) proves the remaining safe driver commands and closes the P-256 crypto gap: pairing, config, and user-memory reads, a single reversible user-memory erase on an empty slot, the chip identity, and a P-256 generate, sign, and erase whose public key, signature, and digest are exported so a host verifier confirms the signature cryptographically. The secure to non-secure data-return channel, the reason se_readonly first HardFaulted: the secure MPU had no region covering non-secure RAM, so the first veneer to write a non-secure buffer faulted (PRIVDEFENA is 0, no background map). Add a pinned fixed shared output buffer at the top of non-secure SRAM (0x2002_FC00, 1 KiB) and a 4th secure MPU region mapping exactly it Read-Write and execute-never, so the W^X invariant holds. The veneer writes public data only to the fixed compile-time address with no non-secure pointer. This is the product's general data-return path for future NSC responses, not a bring-up one-off. Tooling: rename scripts/bringup.sh to scripts/bench.sh, it is now a general flash, run, and detect runner rather than only a first-silicon bring-up helper. Add a FEATURES passthrough so the SE proofs build with one command, and force the shared CMSE import object to regenerate on a feature or profile change through a stamp, a stale import object otherwise breaks the non-secure link.
Docs: - Firmware README: the real status (proven-on-silicon, host-validated, and the path to the next milestone), the repository layout, and the two-image build. - Driver README: drop the "BETA - NOT silicon-validated" banner and add a per-command coverage matrix on two axes (silicon and model), with the brick semantics of the one-way provisioning writes documented. - New docs/bench-runner.md: the bench.sh guide (sub-commands, the feature-to-marker map, the environment overrides, and the brick-safety guarantee). CI: - New embedded job: lint every embedded library on thumbv8m, then the two-image build across the feature matrix (default, se-session, se-fw-update), with the CMSE import object regenerated per feature and the vendor blobs fetched from the pinned libtropic. Compile only, no hardware. - New image-signer job for the detached signing-tool workspace. - The host job now covers the whole workspace, coverage near 93 percent lines. - ci-local.sh mirrors the new stages.
Bump the report-channel and attestation dependencies. Dependencies: - defmt 1.0 -> 1.1 (now 1.1.1) and defmt-rtt 1.0 -> 1.3. - ecdsa off its release candidate to the stable 0.17. p384 and p521 stay on their 0.14 release candidates until they ship stable on the same digest generation. Supply chain (cargo-deny): - Document a tracked, temporary ignore for RUSTSEC-2026-0110. bare-metal is unmaintained but reaches the tree through cortex-m 0.7.7. The ignore goes once a cortex-m release drops bare-metal. - Mark the firmware-internal crates (secure, mcu-spi, mcu-flash, fw-update) publish = false, since only tropic01-driver is published. Their internal path dependencies are then legitimate and no longer read as wildcards. - Allow wildcard path dependencies for these private crates.
Extract private helper functions from six functions SonarQube flagged over the 15 cognitive-complexity limit. Behavior is preserved exactly. - The three SE bring-up veneers (se_readonly, se_persist, se_crypto) move their under-session steps into per-stage helpers returning a small Result. Each veneer now owns a single session teardown and one output commit. The status words, step codes, reserved codes, markers, record offsets, the erase-before-abort order, and the one unsafe write site are unchanged. - The non-secure entry moves its feature-gated report blocks into helpers. The boot and log order, the shared-buffer read, the RTT drain spin, and the final idle loop are untouched. - The dual-bank power-fault proof extracts its recovery loop and its census cross-product into helpers. The single global cut index, both the confirm and revert branches, the per-index census, and every invariant assertion keep the same effect.
0xEthamin
force-pushed
the
feat/mcu-platform-foundation
branch
from
July 10, 2026 17:58
dde034d to
5f391e1
Compare
cortex-m 0.7.7 is the last published release and pulls the unmaintained bare-metal 0.2.5 (RUSTSEC-2026-0110) non-optionally, plus duplicate copies of embedded-hal 0.2.7, rustc_version 0.2.3, and semver 0.9.0. Our whole use of it was a handful of core-instruction intrinsics. Replace them with core::arch::asm in a new zero-dependency mcu-arch crate and provide the critical-section impl defmt-rtt needs at link time ourselves. cortex-m-rt stays. The five crates above leave the lock, and the temporary deny.toml ignore for RUSTSEC-2026-0110 is removed. - mcu-arch: wfi, dsb, isb, a cycle delay, and the PRIMASK helpers, each a byte-for-byte match of the cortex-m 0.7.7 reference, host stubs for x86_64. - critical_section_impl: single-core PRIMASK save and restore, registered in the non-secure bin. Its SAFETY and TrustZone notes are grounded on PM0264 (PRIMASK_NS masks secure exceptions too while AIRCR.PRIS is 0, an obligation recorded for the interrupt bring-up). - Remove the non-secure RTT drain busy-wait. A bench run proved it useless: defmt-rtt blocks while the host is connected and SWD reads a sleeping core, so every line reaches the host without it. - SPI1 DelayNs now fails closed rather than running an unbounded NOP spin. PM0264 sec 3.11.9 gives a NOP no timing floor, and no timer is wired yet. This drops the last NOP asm block from the firmware. - asm gate: mcu-arch inline asm is cfg(target_os = none), so no host lint sees it. scripts/asm-gate.sh disassembles the built images and asserts the cpsid/cpsie masking pair, the dsb/isb/bxns boot order, and the delay countdown loop. Both scripts/ci-local.sh and the GitHub embedded job run the one shared script, so the local mirror and the pipeline cannot diverge. - publish = false on platform and image-verify closes a latent cargo-deny wildcard-path failure the moment either gains a path dependency. Runtime behaviour of the default product image is unchanged. The removal was proven on silicon (the se-session bring-up markers 0x51 through 0x54 all logged with the change applied).
0xEthamin
force-pushed
the
feat/mcu-platform-foundation
branch
from
July 10, 2026 17:59
5f391e1 to
4d8e6d2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.