Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 1 addition & 41 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,42 +1,2 @@
# Workspace cargo configuration.
#
# DESIGN CHOICE (host-testable lib + embeddable bins):
# We deliberately do NOT set a global `build.target`. The default target stays
# the host (x86_64-unknown-linux-gnu) so that:
# - `cargo test -p platform` runs the partition-sequence tests natively on the
# host (the platform lib is pure logic over a register-bus abstraction and
# MUST stay host-testable for the 100%-coverage goal), and
# - `cargo check` over the whole workspace stays a host check.
# The two binary crates (`secure`, `nonsecure`) gate their cortex-m-rt content
# behind `cfg(target_os = "none")`, so they compile to an empty host stub on the
# host and to real firmware only when built for the embedded target:
# cargo build --target thumbv8m.main-none-eabihf
#
# Pinning `build.target = thumbv8m...` globally WOULD break host `cargo test` for
# the lib (no std test harness on a bare-metal target), which is why we leave the
# default host target and document the explicit `--target` invocation here.
#
# The rustflags below are TARGET-SCOPED: they apply ONLY when building for the
# embedded target, never to the host build. They wire cortex-m-rt's linker script
# (link.x), which in turn pulls each bin crate's provisional memory.x (emitted by
# that crate's build.rs). The memory layout is PROVISIONAL (see each memory.x): the
# real secure / non-secure split lands with the NSC-shim wiring (C toolchain +
# linker).

[target.thumbv8m.main-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]

# Convenience alias so the embedded build.
#
# TWO-STAGE MCU BUILD: there is no Cargo dependency edge between the `secure` and
# `nonsecure` bin crates, so cargo does NOT order them. The non-secure link needs
# the CMSE import object produced by linking the secure bin, so build the secure
# crate first, then the non-secure crate:
# cargo build -p secure --target thumbv8m.main-none-eabihf
# cargo build -p nonsecure --target thumbv8m.main-none-eabihf
# A plain whole-workspace `build-mcu` may race (NS link before the secure import
# object exists, or against a stale one). nonsecure/build.rs then fails loudly with
# an actionable message. The aliases below stay as-is for single-crate builds.
[alias]
build-mcu = "build --target thumbv8m.main-none-eabihf"
check-mcu = "check --target thumbv8m.main-none-eabihf"
rustflags = ["-C", "link-arg=-Tlink.x"]
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
# An asm gate disassembles the built images and asserts the hand-rolled
# inline asm compiled to the right instructions, since no host lint sees it.
# - The host signing tool is a detached workspace, so it has its own job.
# - Live model integration: the coverage job clones the official TROPIC01 model
# (libtropic, pinned to a tag + commit) and drives tropic01-driver against it
# end-to-end (real handshake + AES-GCM). These run under coverage so the
# library paths they exercise count. The test-harness files are excluded.
# - Live model integration: the coverage job clones libtropic (pinned to a tag +
# commit) for the model config, installs the ts-tvl wheel pinned by
# crates/tropic01-driver/scripts/install-model.sh, and drives tropic01-driver
# against it end-to-end (real handshake + AES-GCM). These run under coverage so
# the library paths they exercise count. The test-harness files are excluded.
# - cargo-deny BLOCKS on findings (it now also covers the RustSec advisory DB
# that cargo-audit used to check, making a separate audit job redundant): a
# known vulnerability, a banned or yanked crate, a disallowed license, or an
Expand Down Expand Up @@ -43,9 +44,9 @@ permissions:

env:
CARGO_TERM_COLOR: always
# Official TROPIC01 model (libtropic), pinned to a release tag and its commit.
# Shared by the coverage job (the model) and the embedded job
# (the se-fw-update vendor blobs).
# Official libtropic, pinned to a release tag and its commit. Shared by the
# coverage job (the TROPIC01 model config and venv layout) and the embedded
# job (the se-fw-update vendor blobs).
LIBTROPIC_REF: v4.0.0
LIBTROPIC_SHA: 756c8ee898ed61b12272ecb22b213edf97aab751

Expand Down Expand Up @@ -269,7 +270,7 @@ jobs:
- name: Configure model location
run: echo "LIBTROPIC=$RUNNER_TEMP/libtropic" >> "$GITHUB_ENV"

- name: Clone the TROPIC01 model (libtropic, pinned)
- name: Clone the model config (libtropic, pinned)
run: |
set -euo pipefail
git clone --depth 1 --branch "$LIBTROPIC_REF" \
Expand All @@ -280,8 +281,8 @@ jobs:
exit 1
fi

- name: Install the TROPIC01 model (ts-tvl)
run: "$LIBTROPIC/scripts/tropic01_model/install_linux.sh"
- name: Install the TROPIC01 model (ts-tvl, pinned)
run: crates/tropic01-driver/scripts/install-model.sh

# Coverage over the hermetic suite PLUS the live model integration tests
# (tests/model_itest.rs, behind `model-itest`): the driver runs its real
Expand Down
13 changes: 7 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@
# TROPIC01 model server runtime artifact (default config-out dump)
.model_config_save.yaml

# Signing key material and signing artifacts: never commit a private key.
# Key material lives under a keys/ directory OR carries a key extension, and
# exactly that set is ignored. The patterns stay narrow on purpose: a broad
# pattern like *seed* would also swallow a future source file such as
# seed_tests.rs, so each form is listed explicitly. The detached signer build
# dir is kept out too.
# Signing key material and signing artifacts.
/keys/
**/keys/
*.seed
Expand All @@ -29,3 +24,9 @@ seed.bin

# Vendor SE firmware-update blobs (Tropic Square signed, from the libtropic SDK)
crates/secure/fw_blobs/

# Bank / signing / flashing artifacts.
/tools/image-signer/*.bin
/tools/image-signer/*.hex
/tools/image-signer/sig.raw
/tools/image-signer/manifest.txt
34 changes: 31 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"crates/nonsecure",
"crates/image-verify",
"crates/fw-update",
"crates/boot-stage",
]

[workspace.package]
Expand All @@ -36,13 +37,12 @@ ecdsa = { version = "0.17", default-features = false }
p384 = { version = "0.14", default-features = false, features = ["ecdsa"] }
p521 = { version = "0.14", default-features = false, features = ["ecdsa"] }

# Ed25519 verification of the signed firmware-image header (image-verify crate).
# Verify-only on the device: the pinned root public key is the trust input and
# no secret key runs here, so the failure modes are correctness, not
# side-channel leakage. default-features = false keeps it no_std and heap-free.
ed25519-dalek = { version = "3.0", default-features = false }
# ECDSA P-256 over SHA-256: the firmware-image signature algorithm
# (image-verify, and the host signing tool).
p256 = { version = "0.14", default-features = false, features = ["ecdsa"] }

# MCU platform / firmware-binary dependencies
# Ed25519 for the TROPIC01 Ed25519 signatures (the SSH ed25519-sk and PGP path).
ed25519-dalek = { version = "3.0", default-features = false }

# cortex-m-rt: the de-facto reset-vector + .data/.bss init + linker glue for
# Cortex-M. Provides #[entry] and the link.x script the two bins build against.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The workspace is a mix of one host-testable library published to crates.io, the
| [`platform`](crates/platform) | library | TrustZone partition logic (SAU / GTZC / secure MPU), the address map, and the non-secure pointer checks. Host-tested |
| [`mcu-spi`](crates/mcu-spi) | library | The SPI1 MMIO master driving the TROPIC01 |
| [`mcu-flash`](crates/mcu-flash) | library | The on-target dual-bank flash driver behind the update seam |
| [`image-verify`](crates/image-verify) | library | The signed firmware-image header verifier (Ed25519) |
| [`image-verify`](crates/image-verify) | library | The signed firmware-image verifier (ECDSA P-256 over SHA-256, streamed across a segmented image) |
| [`fw-update`](crates/fw-update) | library | The MCU A/B update state machine (verify then swap-as-commit) |
| [`secure`](crates/secure) | binary (TZ-S) | The secure-world image: partition bring-up, the SE driver, and the CMSE non-secure-callable veneers |
| [`nonsecure`](crates/nonsecure) | binary (TZ-NS) | The non-secure image: the entry that calls the veneers and reports over defmt-RTT |
Expand Down
50 changes: 50 additions & 0 deletions crates/boot-stage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "boot-stage"
edition.workspace = true
version.workspace = true
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true
publish = false
description = "STM32U545 immutable first-stage boot code: verifies the running bank, confirms or reverts a pending A/B swap, then hands off to the secure app."

[dependencies]
# The signed-image verifier. The boot stage verifies the RUNNING bank's four
# image segments against the pinned product root key before it hands off.
image-verify = { path = "../image-verify" }
# The shared A/B vocabulary: BankId, PendingFlag, UpdateOutcome, FlashError. The
# real flash driver (mcu-flash) speaks these, so the boot decision and the
# updater agree on the persistent record encoding.
fw-update = { path = "../fw-update" }

[dev-dependencies]
# encode_header mints signed test descriptors so the host tests never hardcode
# the header layout. Feature unification: the product build (no dev-deps) keeps
# image-verify without `encode`, the test build turns it on.
image-verify = { path = "../image-verify", features = ["encode"] }
# Mints signed fixtures: the tests sign a synthetic image with the known bring-up
# scalar so the four-segment health check genuinely accepts it.
p256 = { workspace = true }
# Derives the bring-up test signing key (SHA-256 of the phrase) for the signed
# health-check fixtures in mock.rs.
sha2 = { workspace = true }

# Embedded-only wiring. On the host the bin is an empty main, so these compile
# only for the target. The real flash driver, the core barriers, the reset
# vector, and the halting panic handler are all untestable silicon glue.
[target.'cfg(target_os = "none")'.dependencies]
mcu-flash = { path = "../mcu-flash" }
mcu-arch = { path = "../mcu-arch" }
cortex-m-rt = { workspace = true }
panic-halt = { workspace = true }

# Lint quarantine: the target glue holds the untestable MMIO wiring (VTOR write,
# the MSP-load-and-branch hand-off, the real driver port).
# The pure boot logic has no unsafe.
[lints.rust]
unsafe_code = "deny"
missing_docs = "warn"

[lints.clippy]
all = "deny"
23 changes: 23 additions & 0 deletions crates/boot-stage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# boot-stage

Immutable first-stage boot code for the A/B update model. Runs from pages 2-8 of
whichever bank the hardware boots (SECBOOTADD0 = 0x0C004000, selected by
SWAP_BANK). It reads the image DESCRIPTOR on page 9 of the active bank (the signed
header and signature), verifies the four logical segments with the P-256 verifier
(header, secure payload, non-secure payload, signature), then jumps to the secure
app link origin 0x0C014000, and drives commit/revert.

The boot DECISION is a state machine (`decision.rs`) over the persistent
state (running bank, pending record, NVCNT, image health). It is proven
exhaustively on the host, including a power-cut census at every persistent
mutation boundary. The silicon glue (the real flash driver port, the register
reads, the secure-to-secure jump) is thin and target-only (`entry.rs`, `real.rs`).
The anti-rollback NVCNT bump is done last and is mutually exclusive with a revert.

The FLASH origin/length here MUST agree with the layout table:

pages 0-1 0x0C000000 16K boot metadata (physical Bank 1 only)
pages 2-8 0x0C004000 56K boot stage (this crate, IMMUTABLE)
page 9 0x0C012000 8K image descriptor (header [0:24], signature [24:88])
pages 10-19 0x0C014000 80K secure app + NSC veneer
pages 20-31 0x08028000 96K non-secure app
34 changes: 34 additions & 0 deletions crates/boot-stage/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Linker wiring for the immutable boot-stage binary.
//!
//! For the embedded target only, emit `memory.x` (the boot-stage FLASH / RAM
//! layout, pages 2-8 at 0x0C004000) onto the linker search path so cortex-m-rt's
//! `link.x` composes with it. On the host the bin is an empty stub, so this is a
//! no-op there.

use std::env;
use std::error::Error;
use std::fs;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn Error>>
{
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rerun-if-changed=build.rs");

let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "none"
{
return Ok(());
}

let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::write(out_dir.join("memory.x"), include_bytes!("memory.x"))?;
println!("cargo:rustc-link-search={}", out_dir.display());
// Disable section page-alignment so the linker emits no header-carrying
// segment below FLASH ORIGIN. Without it rust-lld aligns the first segment
// down to the 64 KB page (0x0C000000), placing a phantom ELF-header LOAD in
// the metadata band (pages 0-1). --nmagic keeps every loadable byte inside the
// boot-stage band [0x0C004000, 0x0C012000).
println!("cargo:rustc-link-arg=--nmagic");
Ok(())
}
26 changes: 26 additions & 0 deletions crates/boot-stage/memory.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Immutable boot-stage memory layout for cortex-m-rt's link.x.
*
* FLASH is pages 2-8 of a 256 KB bank at the low secure alias 0x0C00_4000,
* LENGTH 56 KB (7 pages x 8 KB). This band is IMMUTABLE: it is OUTSIDE the A/B
* image band (pages 9-31), so an update can neither program it (the updater MPU
* regions start at page 9) nor erase it (WRP guards the erase, the only op the
* MPU cannot see). Its vector base is SECBOOTADD0 = 0x0C00_4000, written once at
* provisioning and selected on every reset. SWAP_BANK remaps which physical bank
* sits at this low alias, so the boot stage runs from whichever bank booted.
*
* Pages 0-1 (boot metadata, 0x0C00_0000, 16 KB) sit BELOW this origin and are
* pinned to physical Bank 1, so they are never linked into the boot stage.
*
* RAM is the lower 128 KB of SRAM1 at 0x2000_0000, the secure RAM half, matching
* the secure app crate and the SAU region 2 / MPCBB1 split in platform map.rs.
*
* The boot-stage crate consumes this script (bank choice, commit/revert, image
* health, anti-rollback) and is fully built. This layout is fixed so the address
* map stays stable across the A/B work. RM0456 sec 7.5.8 (identical layout per
* bank) and Table 26 (SECBOOTADD0).
*/
MEMORY
{
FLASH (rx) : ORIGIN = 0x0C004000, LENGTH = 56K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
2 changes: 2 additions & 0 deletions crates/boot-stage/product_root_key.sec1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
…¤=V©rSÓÔàóÍ"žËkß c‚^©Þ]èH¯O7¼Ë›W¢Y€z _ƒ«4„Ý1ˆ–LÃÉM3ò¸
Expand Down
Loading
Loading