diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 93164d37..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,20 +0,0 @@ -[target.thumbv8m.main-none-eabihf] -# runner = 'probe-rs run --chip-description-path .vscode/rt633s.yaml --chip MIMXRT633SFFOB' -runner = 'probe-rs run --chip MIMXRT685SFVKB' - -rustflags = [ - "-C", - "linker=flip-link", - # "-C", - # "link-arg=-Tdefmt.x", - # This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x - # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 - "-C", - "link-arg=--nmagic", -] - -[build] -target = "thumbv8m.main-none-eabihf" # Cortex-M33 - -[env] -DEFMT_LOG = "trace" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4a038115..eacd00e5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,7 +21,6 @@ concurrency: cancel-in-progress: true name: check jobs: - fmt: runs-on: ubuntu-latest name: nightly / fmt @@ -39,8 +38,12 @@ jobs: uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - - name: cargo fmt --check + - name: cargo fmt --check (libs) + run: cargo fmt --check + working-directory: "./libs" + - name: cargo fmt --check (examples) run: cargo fmt --check + working-directory: "./examples/rt685s" clippy: runs-on: ubuntu-latest @@ -65,12 +68,20 @@ jobs: components: clippy - name: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }} - - name: cargo clippy + - name: cargo clippy (libs) + uses: giraffate/clippy-action@v1 + with: + reporter: "github-pr-check" + clippy_flags: -- -F clippy::suspicious -D clippy::correctness -F clippy::perf -F clippy::style + github_token: ${{ secrets.GITHUB_TOKEN }} + workdir: "./libs" + - name: cargo clippy (examples) uses: giraffate/clippy-action@v1 with: - reporter: 'github-pr-check' - clippy_flags: -- -F clippy::suspicious -F clippy::correctness -F clippy::perf -F clippy::style + reporter: "github-pr-check" + clippy_flags: -- -F clippy::suspicious -D clippy::correctness -F clippy::perf -F clippy::style github_token: ${{ secrets.GITHUB_TOKEN }} + workdir: "./examples/rt685s" # Enable once we have a released crate # semver: @@ -107,8 +118,14 @@ jobs: uses: dtolnay/rust-toolchain@nightly - name: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }} - - name: cargo doc + - name: cargo doc (libs) + run: cargo doc --no-deps --features mimxrt685s + working-directory: "./libs" + env: + RUSTDOCFLAGS: --cfg docsrs + - name: cargo doc (examples) run: cargo doc --no-deps + working-directory: "./examples/rt685s" env: RUSTDOCFLAGS: --cfg docsrs @@ -125,15 +142,23 @@ jobs: - uses: actions/checkout@v4 with: submodules: true + - name: Install stable uses: dtolnay/rust-toolchain@stable - - name: rustup target add ${{ matrix.target }} - run: rustup target add ${{ matrix.target }} - - name: cargo install cargo-hack - uses: taiki-e/install-action@cargo-hack - # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 - - name: cargo hack - run: cargo hack --each-feature --exclude-all-features check + + - name: rustup target add thumbv8m.main-none-eabihf + run: rustup target add thumbv8m.main-none-eabihf + + - name: cargo install cargo-batch + run: cargo install --git https://github.com/embassy-rs/cargo-batch cargo --bin cargo-batch --locked + + - name: cargo manual hack (libs) + run: ./ci.sh + working-directory: "./libs" + + - name: cargo manual hack (examples/rt685s) + run: ./ci.sh + working-directory: "./examples/rt685s" deny: # cargo-deny checks licenses, advisories, sources, and bans for @@ -152,11 +177,17 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }} - - name: cargo install cargo-deny + - name: Cargo deny (libs) + uses: EmbarkStudios/cargo-deny-action@v2 + with: + log-level: warn + manifest-path: ./libs/Cargo.toml + command: check + - name: Cargo deny (examples) uses: EmbarkStudios/cargo-deny-action@v2 with: log-level: warn - manifest-path: ./Cargo.toml + manifest-path: ./examples/rt685s/Cargo.toml command: check msrv: @@ -167,19 +198,9 @@ jobs: strategy: fail-fast: false matrix: - msrv: ["1.79"] # We're relying on namespaced-features, which - # was released in 1.60 - # - # We also depend on `fixed' which requires rust - # 1.71 - # - # Additionally, we depend on embedded-hal-async - # which requires 1.75 - # - # embassy-time requires 1.79 due to - # collapse_debuginfo + msrv: ["1.90"] # We are depending on embassy-imxrt. target: [thumbv8m.main-none-eabihf] - name: ubuntu / ${{ matrix.msrv }} + name: ubuntu / MSRV ${{ matrix.msrv }} steps: - uses: actions/checkout@v4 with: @@ -187,8 +208,12 @@ jobs: - name: Install ${{ matrix.msrv }} uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.msrv }} + toolchain: ${{ matrix.msrv }} - name: rustup target add ${{ matrix.target }} run: rustup target add ${{ matrix.target }} - - name: cargo +${{ matrix.msrv }} check + - name: cargo +${{ matrix.msrv }} check (libs) + run: cargo check --features mimxrt685s + working-directory: "./libs" + - name: cargo +${{ matrix.msrv }} check (examples) run: cargo check + working-directory: "./examples/rt685s" diff --git a/.github/workflows/nostd.yml b/.github/workflows/nostd.yml deleted file mode 100644 index c67c9f7a..00000000 --- a/.github/workflows/nostd.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This workflow checks whether the library is able to run without the std library (e.g., embedded). -# This entire file should be removed if this crate does not support no-std. See check.yml for -# information about how the concurrency cancellation and workflow triggering works -permissions: - contents: read -on: - push: - branches: [main] - pull_request: -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true -name: no-std -jobs: - nostd: - runs-on: ubuntu-latest - name: ${{ matrix.target }} - strategy: - matrix: - target: [thumbv8m.main-none-eabihf] - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Install stable - uses: dtolnay/rust-toolchain@stable - - name: rustup target add ${{ matrix.target }} - run: rustup target add ${{ matrix.target }} - - name: cargo check - run: cargo check --target ${{ matrix.target }} --no-default-features diff --git a/.vscode/launch.json b/.vscode/launch.json index c4cf72a1..ddde0dd6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ }, "coreConfigs": [ { - "programBinary": "target/thumbv8m.main-none-eabihf/debug/ec-slimloader", + "programBinary": "examples/rt685s/target/thumbv8m.main-none-eabihf/release/example-bootloader", "svdFile": ".vscode/MIMXRT685S_cm33.svd", "rttEnabled": true, } diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 18971a9e..00000000 --- a/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "ec-slimloader" -version = "0.1.0" -edition = "2021" - - -[profile.release] -lto = true # better optimizations -opt-level = "z" - -[features] -# Special board support -mimxrt685s-evk = ["imxrt-fcb-rt685evk", "mimxrt685s"] - -# FCB support -imxrt-fcb-rt685evk = ["mimxrt685s"] -imxrt-fcb-1spi-a1-nor = ["imxrt-fcb-1spi-nor"] -imxrt-fcb-1spi-b1-nor = ["imxrt-fcb-1spi-nor"] -imxrt-fcb-1spi-nor = ["imxrt"] - -# chip variants -mimxrt685s = ["imxrt"] -mimxrt633s = ["imxrt"] - -# common imxrt features -imxrt = ["dep:mimxrt600-fcb"] - -# optional debug logging -defmt = ["dep:defmt", "dep:defmt-rtt"] - -[dependencies] -ec-slimloader-descriptors = { git = "https://github.com/OpenDevicePartnership/ec-slimloader-descriptors" } - -crc = "3.2.1" - -# Optionally pull in defmt for trace debugging scenarios -defmt = { version = "0.3.6", optional = true } -defmt-rtt = { version = "0.4.0", optional = true } - -cortex-m = { version = "0.7.7", features = [ - "inline-asm", - "critical-section-single-core", -] } -cortex-m-rt = "0.7.3" -panic-probe = "*" - -mimxrt600-fcb = { version = "0.2.0", optional = true } diff --git a/README.md b/README.md index 5b8ef348..4d6311dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,92 @@ -# ec-slimloader +# ec-slimloader **OUTDATED** -A light-weight stage-one bootloader for loading an app image as configured by ec-slimloader-descriptors +A light-weight stage-two bootloader written in Rust for loading an app image as configured by ec-slimloader-descriptors. 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. + +Currently this bootloader can only be used on the IMXRT600 series of chipsets from NXP. + +## Organisation + +This repository is split up into four parts: +* ec-slimloader: the binary project which forms the second stage bootloader +* ec-slimloader-descriptors: the library crate containing a descriptor of where each image slot exists, as well as a persistent fail-safe state journal for recording the A/B bootloading state. +* bootloader-tool: a command-line utility using 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. +* example: an example application image that uses the state-journal to select alternative images to execute. + +## Memory layout +This repository has default configuration files detailing the used memory layout. This layout will probably will need to be adapted for your specific usecase. + +## Quick guide +This guide details how to use this repository on the NXP MIMXRT685S-EVK. First step is compiling the bootloader and application: + +```bash +pushd ec-slimloader +cargo build --release --features defmt +popd +pushd examples/rt685s-application +cargo build --release +popd +``` + +In general, the bootloader-tool is a `clap` supported CLI application with for each subcommand a full `--help`: +``` +cargo run -- --help + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s + Running `target/debug/bootloader-tool --help` +Usage: bootloader-tool [OPTIONS] [COMMAND] + +Commands: + generate Generate keys and certificates + sign Sign binaries for flashing or OTA + download Download binaries to the device + run Run binaries, setting the shadow registers, by going through the bootloader chain for testing purposes + fuse Burn fuse registers with key material and settings + help Print this message or the help of the given subcommand(s) + +Options: + -c, --config Configuration file path [default: ./config.toml] + -h, --help Print help + -V, --version Print version +``` + +For now we need to prepare our testing setup by generating the key material: +```bash +cd bootloader-tool +cargo run -- generate certificates +cargo run -- generate otp +``` + +This key material is only used for testing right now, and everything is put in the `./artifacts` directory. This can be configured in the `./config.toml` file. +We are working on a setup to also support external HSM integration. + +Now we have everything ready to start flashing. +We can use run `run` command to immediately flash and `attach` in the same way you are familiar with from `probe-rs`. However, we need the bootloader to start up the application, and we need the FCB (we call everything in 0x0 to 0x1000 the 'prelude') to start the bootloader. We can extract the FCB from the `ec-slimloader` as it is built with the appropriate feature flags to include a FCB in the ELF file. Extraction happens as a side-product of signing: + +```bash +cargo run -- sign bootloader -i ../target/thumbv8m.main-none-eabihf/release/ec-slimloader +``` + +We can now flash the FCB: + +```bash +cargo run -- download prelude --prelude-path ../target/thumbv8m.main-none-eabihf/release/ec-slimloader.prelude.elf +``` + +And we can flash the application into *both slots*: +```bash +cargo run -- download application -i ../examples/rt685s-application/target/thumbv8m.main-none-eabihf/release/example-application --slot 0 +cargo run -- download application -i ../examples/rt685s-application/target/thumbv8m.main-none-eabihf/release/example-application --slot 1 +``` + +To flash & attach to the bootloader now run, whilst setting the OTP shadow registers: +```bash +cargo run -- download bootloader -i ../target/thumbv8m.main-none-eabihf/release/ec-slimloader +``` + +To flash & attach to the application (TODO it now is not resetting the state journal so take care), assuming you have a and FCB bootloader already flashed: +```bash +cargo run -- run application -i ../examples/rt685s-application/target/thumbv8m.main-none-eabihf/release/example-application +``` + +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`. diff --git a/bootloader-tool/.cargo/config.toml b/bootloader-tool/.cargo/config.toml new file mode 100644 index 00000000..86875880 --- /dev/null +++ b/bootloader-tool/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUST_LOG = "none,bootloader_tool=debug" \ No newline at end of file diff --git a/bootloader-tool/.gitignore b/bootloader-tool/.gitignore new file mode 100644 index 00000000..93a34f72 --- /dev/null +++ b/bootloader-tool/.gitignore @@ -0,0 +1,8 @@ +/target + +# Software package +/elftosb* + +/binaries.tar.xz +/gpio-blinky +/*.bin diff --git a/bootloader-tool/Cargo.lock b/bootloader-tool/Cargo.lock new file mode 100644 index 00000000..0edd081b --- /dev/null +++ b/bootloader-tool/Cargo.lock @@ -0,0 +1,3031 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.1", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitfield" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a3a774b2fcac1b726922b921ebba5e9fe36ad37659c822cf8ff2c1e0819892" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52511b09931f7d5fe3a14f23adefbc23e5725b184013e96c8419febb61f14734" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bootloader-tool" +version = "0.1.0" +dependencies = [ + "aes", + "anyhow", + "clap", + "hmac", + "indicatif", + "itertools", + "log", + "object", + "pretty_env_logger", + "probe-rs", + "rand 0.9.2", + "rsa", + "serde", + "serde_yml", + "sha2", + "tempfile", + "tokio", + "toml", + "x509-parser", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cobs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea6d1b751c55bd9c0dda7d4ff752074e98f4765ae969664648bd193bb326d15" +dependencies = [ + "thiserror", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deku" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9711031e209dc1306d66985363b4397d4c7b911597580340b93c9729b55f6eb" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cb0719583cbe4e81fb40434ace2f0d22ccc3e39a74bb3796c22b451b4f139d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "docsplay" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c64169ab69178537cce74424c550583a84f58bc62593ea6023c58886be69b4" +dependencies = [ + "docsplay-macros", +] + +[[package]] +name = "docsplay-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4673f83edb6dfabfbc26704bd89ee95f4b164cd5db5fe8c88efda48fb0fca8d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.1", +] + +[[package]] +name = "esp-idf-part" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ebc2381d030e4e89183554c3fcd4ad44dc5ab34961ab09e09b4adbe4f94b61" +dependencies = [ + "bitflags 2.9.4", + "csv", + "deku", + "md-5", + "parse_int", + "regex", + "serde", + "serde_plain", + "strum", + "thiserror", +] + +[[package]] +name = "espflash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c63d954132db04edb660af5a554a46f909f7b172d7601b4af3fea994b926821" +dependencies = [ + "base64", + "bitflags 2.9.4", + "bytemuck", + "esp-idf-part", + "flate2", + "libc", + "log", + "md-5", + "miette", + "nix 0.30.1", + "object", + "serde", + "sha2", + "strum", + "thiserror", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "nix 0.27.1", + "pkg-config", + "udev", + "windows-sys 0.48.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ihex" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.1", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jep106" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1354c92c91fd5595fd4cc46694b6914749cc90ea437246549c26b6ff0ec6d1" +dependencies = [ + "serde", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "nusb" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f861541f15de120eae5982923d073bfc0c1a65466561988c82d6e197734c19e" +dependencies = [ + "atomic-waker", + "core-foundation 0.9.4", + "core-foundation-sys", + "futures-core", + "io-kit-sys", + "libc", + "log", + "once_cell", + "rustix 0.38.44", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.15.5", + "indexmap", + "memchr", + "ruzstd", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parse_int" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31" +dependencies = [ + "num-traits", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.1", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "probe-rs" +version = "0.29.0" +source = "git+https://github.com/probe-rs/probe-rs.git?rev=6b0c108#6b0c108e067cc70d5e000e664b549eb23f6a7e96" +dependencies = [ + "anyhow", + "async-io", + "async-trait", + "bincode", + "bitfield", + "bitvec", + "cobs", + "docsplay", + "dunce", + "espflash", + "flate2", + "futures-lite", + "hidapi", + "ihex", + "itertools", + "jep106", + "nusb", + "object", + "parking_lot", + "probe-rs-target", + "rmp-serde", + "scroll", + "serde", + "serde_yaml", + "serialport", + "thiserror", + "tracing", + "uf2-decode", + "zerocopy", +] + +[[package]] +name = "probe-rs-target" +version = "0.29.0" +source = "git+https://github.com/probe-rs/probe-rs.git?rev=6b0c108#6b0c108e067cc70d5e000e664b549eb23f6a7e96" +dependencies = [ + "base64", + "indexmap", + "jep106", + "serde", + "serde_with", + "url", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.1", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruzstd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "time", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "serialport" +version = "4.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acaf3f973e8616d7ceac415f53fc60e190b2a686fbcf8d27d0256c741c5007b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "core-foundation 0.10.1", + "core-foundation-sys", + "io-kit-sys", + "mach2", + "nix 0.26.4", + "scopeguard", + "unescaper", + "winapi", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.1", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "udev" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50051c6e22be28ee6f217d50014f3bc29e81c20dc66ff7ca0d5c5226e1dcc5a1" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + +[[package]] +name = "uf2-decode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca77d41ab27e3fa45df42043f96c79b80c6d8632eed906b54681d8d47ab00623" + +[[package]] +name = "unescaper" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" +dependencies = [ + "thiserror", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/bootloader-tool/Cargo.toml b/bootloader-tool/Cargo.toml new file mode 100644 index 00000000..8d60dca5 --- /dev/null +++ b/bootloader-tool/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "bootloader-tool" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } + +indicatif = "0.17.11" +object = { version = "0.37.3", features = ["build"] } +probe-rs = { git = "https://github.com/probe-rs/probe-rs.git", rev = "6b0c108" } + +tokio = { version = "1.47.1", features = ["macros", "rt", "rt-multi-thread"] } + +anyhow = "1.0" +itertools = "0.14.0" + +rand = "0.9" + +serde = { version = "1.0", features = ["derive"] } +toml = { version = "0.9", features = ["serde"] } +serde_yml = "0.0" + +pretty_env_logger = "0.5" +log = "0.4" + +hmac = "0.12" +sha2 = "0.10" +aes = "0.8" +rsa = { version = "0.9.8", features = ["sha2"] } +x509-parser = { version = "0.17.0", features = ["verify"] } + +tempfile = "3.20.0" diff --git a/bootloader-tool/README.md b/bootloader-tool/README.md new file mode 100644 index 00000000..7c5b0d13 --- /dev/null +++ b/bootloader-tool/README.md @@ -0,0 +1,89 @@ +# RT6xx Bootloader signing tool **EXPERIMENTAL** + +This binary provides support for signing, downloading and running bootloaders via the secure pathway on the NXP RT6xx family of chips. It automates the signing process, as well as setting up the chip temporarily for running signed binaries. Note: It does not blow any fuses, unless using the `fuse` command, and any boot configuration changes made are erased on a power cycle. + +**Note**: the RT6xx chipset enters a FAULT condition when image verification fails. You might need to periodically powercycle your board until the complete chain runs successfully. (TODO why?) + +## Installation + +This binary assumes that probe-rs and the nxp spsdk are available in the system path. Probe-rs has [installation instructions online](https://probe.rs/docs/getting-started/installation/). Spsdk can be installed on Linux platforms using: + +``` +pip3 install spsdk +``` + +| Package | Version tested | +| ------- | -------------- | +| probe-rs | 29.1 | +| spsdk | 3.1.0 | + +## Usage + +For creating the signed binary, certificates and the devices root symmetric key are needed. The details of the bootloader and target device memory mapping are controlled via the `config.toml` provided in this source tree. + +You can view the general instructions on how to invoke the bootloader tool as follows: +``` +cargo run -- --help +``` + +In general you first need to get the key material to be used. You can use this tool to generate it, or use some other tooling like secure hardware certificate tools to get your key material. This tool does not overwrite any pre-existing key material. You can generate it using: + +``` +cargo run -- generate certificates +cargo run -- generate otp +``` + +Then, assuming you have a bootloader and application ready (see the example folder to quickly build something that runs on the RT685S EVK), you can use the following to flash an application to slot 0: + +``` +cargo run -- run application --input-path ./example/application/target/thumbv8m.main-none-eabihf/release/example-application +``` + +And then flash the bootloader to test that it works: +``` +cargo run -- run bootloader --input-path ./example/bootloader/target/thumbv8m.main-none-eabihf/release/example-bootloader +``` + +**Note**: initially flashing the application causes the target to lock up, and you might need to powercycle before +running the bootloader. + +### Signing an image using an HSM + +```bash +# Copy in your image +mkdir sign_me +cp example/bootloader/target/thumbv8m.main-none-eabihf/release/example-bootloader sign_me/ + +# Prepare image for signing +cargo run -- sign bootloader --input-path sign_me/example-bootloader --dont-sign + +# This will generate example-bootloader.mbi-proto.bin which you can pass to your HSM +openssl dgst -sign artifacts/cert-img1-user-key.pem -sha256 -out sign_me/signature.bin -binary sign_me/example-bootloader.mbi-proto.bin + +# Lastly merge the signature into the image (this also verifies that the signature is correct) +cargo run -- sign bootloader --input-path sign_me/example-bootloader --signature-path sign_me/signature.bin + +# The final signed image for flashing is then in sign_me/example-bootloader.signed.bin +``` + +## Binary layout + +Binaries for flashing with this tool should be designed to be loaded into RAM. They should not be linked to have sections loaded into flash, as flash layout is changed somewhat by the signing process. No additional sections like keyblobs or keystores should be present. When using `cortex-m-rt`, the `example` folder can be investigated for a suggestion of the memory layout for respectively the bootloader and application corresponding to the in-tree `config.toml`. + +## Method of operation +This tool takes an input ELF image and: +1. extracts all relevant sections from the given ELF +2. performs checks, such as that the sections are consecutive, not too large, and that the vector table exists on the expected memory address +3. converts the ELF sections into a consecutive binary image +4. calls the SPSDK tooling to package the image as a Master Boot Image in signed and encrypted mode, set to be loaded into RAM +5. checks the integrity of this image +6. (optionally) loads the relevant shadow registers for RTKH (certificate hashes) and OTP (decryption) +7. (optionally) uploads the signed binary to external NOR flash on the address that the 1st stage ROM bootloader +8. (optionally TODO) loads the other sections required like the FCB into external NOR flash +9. (optionally) resets the target device, causing the 2nd stage bootloader to be executed + +### Other sections +The following sections need to also be set (TODO support) before the bootloader can be run: +* OTFAD: KeyBlob used for external NOR flash encryption/decryption. +* FCB: Flash Configuration Block: used to configure access to external NOR flash, is read using 1-bit mode to bootstrap better modes. +* BIV: Boot Image Version, used for ping-pong boot. We are not using this at all. \ No newline at end of file diff --git a/bootloader-tool/artifacts/.gitignore b/bootloader-tool/artifacts/.gitignore new file mode 100644 index 00000000..c94a563d --- /dev/null +++ b/bootloader-tool/artifacts/.gitignore @@ -0,0 +1,11 @@ +*.bin +*.pem +*.der +*.pub +*.sb +*.txt + + +mbi +sb21 +sbkek \ No newline at end of file diff --git a/bootloader-tool/artifacts/README.md b/bootloader-tool/artifacts/README.md new file mode 100644 index 00000000..e01f009f --- /dev/null +++ b/bootloader-tool/artifacts/README.md @@ -0,0 +1,5 @@ +# Artifacts folder +This directory contains configuration assets used by SPSDK to generate key materials and signed master boot images. +Anything generated by SPSDK should be included in the `.gitignore` file and never committed to git. + +In the future even the configuration files might also be auto-generated by the `ms-bootloader-tool`. diff --git a/bootloader-tool/artifacts/mbi-application.yaml b/bootloader-tool/artifacts/mbi-application.yaml new file mode 100644 index 00000000..1a781840 --- /dev/null +++ b/bootloader-tool/artifacts/mbi-application.yaml @@ -0,0 +1,83 @@ +# =========================== mbi Configuration template for mimxrt685s, Revision: latest. =========================== + +# ====================================================================================================================== +# == General Options == +# ====================================================================================================================== +# -------------------------------------===== The chip family name [Required] =====-------------------------------------- +# Description: NXP chip family identifier. +family: mimxrt685s +# -----------------------------------------===== MCU revision [Optional] =====------------------------------------------ +# Description: Revision of silicon. The 'latest' name, means most current revision. +# Possible options: +revision: latest +# ====================================================================================================================== +# == Basic Settings == +# ====================================================================================================================== +# --------------------------------------===== Application target [Required] =====--------------------------------------- +# Description: Definition if application is Execute in Place(XiP) or loaded to RAM during reset sequence. +# Possible options: +outputImageExecutionTarget: xip +# -------------------------------===== Type of boot image authentication [Required] =====------------------------------- +# Description: Specification of final master boot image authentication. +# Possible options: +outputImageAuthenticationType: signed +# ---------------------------------------===== Output Image name [Required] =====--------------------------------------- +# Description: The path for result binary file. +masterBootOutputFile: placeholder # filled in by tooling +# ------------------------------------===== Plain application image [Required] =====------------------------------------ +# Description: The input application image to by modified to Master Boot Image. +inputImageFile: placeholder # filled in by tooling +# --------------------------------===== The list of additional binaries [Optional] =====-------------------------------- +# Description: This is software feature of RTxxx family that NXP SDK startup code (not ROM) could load additional +# images. +# applicationTable: +# - # ----------------------------------------===== Binary file [Required] =====---------------------------------------- +# # Description: The binary file to be added to final application. +# binary: my_additional_binary.bin +# # ------------------------------------===== Destination address [Required] =====------------------------------------ +# # Description: Destination address in RAM of additional binary. +# destAddress: 536870912 +# # ----------------------------------------===== Enable load [Required] =====---------------------------------------- +# # Description: Enabler to load/use the image. +# load: true +# --------------------------------===== Loading address of application [Optional] =====--------------------------------- +# Description: Application loading address in RAM if not XiP, otherwise address of load in XiP. +outputImageExecutionAddress: 0 # filled in by tooling +# ----------------------------------===== Enable User HW key sharing [Required] =====----------------------------------- +# Description: Controlling secure hardware key bus. If enabled(1), then it is possible to access keys on hardware secure +# bus from non-secure application, else non-secure application will read zeros. +enableHwUserModeKeys: false +# ====================================================================================================================== +# == Trust Zone Settings == +# ====================================================================================================================== +# ------------------------------------===== TrustZone enable option [Optional] =====------------------------------------ +# Description: If not specified, the Trust zone is disabled. +enableTrustZone: true +# ---------------------------------===== TrustZone Customization file [Optional] =====---------------------------------- +# Description: If not specified, but TrustZone is enabled(enableTrustZone) the default values are used. +#trustZonePresetFile: my_tz_custom.yaml +# ====================================================================================================================== +# == Certificate Block V1 == +# ====================================================================================================================== +# -----------------------------===== Certificate Block binary/config file [Required] =====------------------------------ +# Description: Path to certificate block binary or config file. +certBlock: placeholder # filled in by tooling +# ====================================================================================================================== +# == Image Signing Settings == +# ====================================================================================================================== +# -------------------------------------===== Signer configuration [Required] =====-------------------------------------- +# Description: Signature provider configuration in format 'type=;=;=' or path to a +# private key. +signer: type=file;file_path=cert-img2-user-key.pem +# ====================================================================================================================== +# == Encryption Settings == +# ====================================================================================================================== +# -----------------------------------===== OTP Master key (HMAC Key) [Required] =====----------------------------------- +# Description: The OTP Master key that is used to compute HMAC encryption key. Could be defined as hex number and also +# as hex/binary file.\n Used algorithm by tool the get HMAC Key; AES_ENCRYPT (OTP_MASTER_KEK, +# 0x00000000000000000000000000000000) +# outputImageEncryptionKeyFile: otp_master_key.txt +# ------------------------------------===== The Key store data file [Optional] =====------------------------------------ +# Description: Optional KeyStore data file for included keystore in LoadToRam images. If defined the KeyStore is added +# into MBI. +# keyStoreFile: my_key_store_data.bin diff --git a/bootloader-tool/artifacts/mbi-bootloader.yaml b/bootloader-tool/artifacts/mbi-bootloader.yaml new file mode 100644 index 00000000..dff24a25 --- /dev/null +++ b/bootloader-tool/artifacts/mbi-bootloader.yaml @@ -0,0 +1,83 @@ +# =========================== mbi Configuration template for mimxrt685s, Revision: latest. =========================== + +# ====================================================================================================================== +# == General Options == +# ====================================================================================================================== +# -------------------------------------===== The chip family name [Required] =====-------------------------------------- +# Description: NXP chip family identifier. +family: mimxrt685s +# -----------------------------------------===== MCU revision [Optional] =====------------------------------------------ +# Description: Revision of silicon. The 'latest' name, means most current revision. +# Possible options: +revision: latest +# ====================================================================================================================== +# == Basic Settings == +# ====================================================================================================================== +# --------------------------------------===== Application target [Required] =====--------------------------------------- +# Description: Definition if application is Execute in Place(XiP) or loaded to RAM during reset sequence. +# Possible options: +outputImageExecutionTarget: xip +# -------------------------------===== Type of boot image authentication [Required] =====------------------------------- +# Description: Specification of final master boot image authentication. +# Possible options: +outputImageAuthenticationType: signed +# ---------------------------------------===== Output Image name [Required] =====--------------------------------------- +# Description: The path for result binary file. +masterBootOutputFile: placeholder # filled in by tooling +# ------------------------------------===== Plain application image [Required] =====------------------------------------ +# Description: The input application image to by modified to Master Boot Image. +inputImageFile: placeholder # filled in by tooling +# --------------------------------===== The list of additional binaries [Optional] =====-------------------------------- +# Description: This is software feature of RTxxx family that NXP SDK startup code (not ROM) could load additional +# images. +# applicationTable: +# - # ----------------------------------------===== Binary file [Required] =====---------------------------------------- +# # Description: The binary file to be added to final application. +# binary: my_additional_binary.bin +# # ------------------------------------===== Destination address [Required] =====------------------------------------ +# # Description: Destination address in RAM of additional binary. +# destAddress: 536870912 +# # ----------------------------------------===== Enable load [Required] =====---------------------------------------- +# # Description: Enabler to load/use the image. +# load: true +# --------------------------------===== Loading address of application [Optional] =====--------------------------------- +# Description: Application loading address in RAM if not XiP, otherwise address of load in XiP. +outputImageExecutionAddress: 0 # filled in by tooling +# ----------------------------------===== Enable User HW key sharing [Required] =====----------------------------------- +# Description: Controlling secure hardware key bus. If enabled(1), then it is possible to access keys on hardware secure +# bus from non-secure application, else non-secure application will read zeros. +enableHwUserModeKeys: false +# ====================================================================================================================== +# == Trust Zone Settings == +# ====================================================================================================================== +# ------------------------------------===== TrustZone enable option [Optional] =====------------------------------------ +# Description: If not specified, the Trust zone is disabled. +enableTrustZone: true +# ---------------------------------===== TrustZone Customization file [Optional] =====---------------------------------- +# Description: If not specified, but TrustZone is enabled(enableTrustZone) the default values are used. +#trustZonePresetFile: my_tz_custom.yaml +# ====================================================================================================================== +# == Certificate Block V1 == +# ====================================================================================================================== +# -----------------------------===== Certificate Block binary/config file [Required] =====------------------------------ +# Description: Path to certificate block binary or config file. +certBlock: placeholder # filled in by tooling +# ====================================================================================================================== +# == Image Signing Settings == +# ====================================================================================================================== +# -------------------------------------===== Signer configuration [Required] =====-------------------------------------- +# Description: Signature provider configuration in format 'type=;=;=' or path to a +# private key. +signer: type=file;file_path=cert-img1-user-key.pem +# ====================================================================================================================== +# == Encryption Settings == +# ====================================================================================================================== +# -----------------------------------===== OTP Master key (HMAC Key) [Required] =====----------------------------------- +# Description: The OTP Master key that is used to compute HMAC encryption key. Could be defined as hex number and also +# as hex/binary file.\n Used algorithm by tool the get HMAC Key; AES_ENCRYPT (OTP_MASTER_KEK, +# 0x00000000000000000000000000000000) +# outputImageEncryptionKeyFile: otp_master_key.txt +# ------------------------------------===== The Key store data file [Optional] =====------------------------------------ +# Description: Optional KeyStore data file for included keystore in LoadToRam images. If defined the KeyStore is added +# into MBI. +# keyStoreFile: my_key_store_data.bin diff --git a/bootloader-tool/config.toml b/bootloader-tool/config.toml new file mode 100644 index 00000000..808a29ef --- /dev/null +++ b/bootloader-tool/config.toml @@ -0,0 +1,28 @@ +artifacts_path = "./artifacts" + +key_type = "rsa3072" + +rkth_path = "./artifacts/rkth.txt" +otp_path = "./artifacts/otp_master_key.txt" + +certificates = [ + [ + { path = "./artifacts/cert-rot1.pem", prototype = { key_path = "./artifacts/cert-rot1-ca-key.pem", key_type = "rsa3072" } }, + { path = "./artifacts/cert-img1.pem", prototype = { key_path = "./artifacts/cert-img1-user-key.pem", key_type = "rsa3072" } }, + ], + [ + { path = "./artifacts/cert-rot2.pem", prototype = { key_path = "./artifacts/cert-rot2-ca-key.pem", key_type = "rsa3072" } }, + { path = "./artifacts/cert-img2.pem", prototype = { key_path = "./artifacts/cert-img2-user-key.pem", key_type = "rsa3072" } }, + ], +] + +[bootloader] +flash_start = 0x08001000 +run_start = 0x10170000 +max_size = 0x8000 +state = { start = 0x0800B000, size = 0x2000 } + +[application] +slot_starts = [0x800D000, 0x80F9000] +run_start = 0x10020000 +slot_size = 0xEC000 # 944K diff --git a/bootloader-tool/src/commands/download.rs b/bootloader-tool/src/commands/download.rs new file mode 100644 index 00000000..ea6d6dc4 --- /dev/null +++ b/bootloader-tool/src/commands/download.rs @@ -0,0 +1,111 @@ +use crate::commands::sign::SignOutput; +use crate::config::Config; +use crate::processors::certificates::Rkth; +use crate::processors::probe; +use crate::{DownloadCommands, ProbeArgs, RunCommands, SignCommands}; +use DownloadCommands::Other; +use anyhow::Context; +use probe_rs::flashing::ElfOptions; +use probe_rs::{Session, flashing}; +use std::path::Path; + +pub async fn process(config: &Config, command: DownloadCommands) -> anyhow::Result<()> { + match command { + DownloadCommands::Prelude { + prelude_path, + probe_args, + } => { + download_prelude(&prelude_path, &probe_args).await?; + } + Other(args) => { + process_other(config, args).await?; + } + }; + Ok(()) +} + +pub struct DownloadOutput { + pub session: Session, + pub rkth: Rkth, +} + +pub async fn process_other(config: &Config, command: RunCommands) -> anyhow::Result { + let (run_args, is_bootloader, flash_start) = match command { + RunCommands::Bootloader(run_args) => { + if let Some(bootloader) = &config.bootloader { + (run_args, true, bootloader.flash_start) + } else { + return Err(anyhow::anyhow!("Bootloader not defined in configuration file")); + } + } + RunCommands::Application { run_args, slot } => { + if let Some(application) = &config.application { + let flash_start = *application + .slot_starts + .get(slot as usize) + .ok_or_else(|| anyhow::anyhow!(format!("Slot {} not defined in configuration file", slot)))?; + (run_args, false, flash_start) + } else { + return Err(anyhow::anyhow!("Bootloader not defined in configuration file")); + } + } + }; + + log::debug!("Preparing for download by calling sign..."); + + let sign_command = if is_bootloader { + SignCommands::Bootloader(run_args.sign_args.clone()) + } else { + SignCommands::Application(run_args.sign_args.clone()) + }; + + let SignOutput { output_path, rkth } = super::sign::process(config, sign_command).await?; + + let Some(output_path) = output_path else { + return Err(anyhow::anyhow!("Image was not signed so nothing to run")); + }; + + log::debug!("Starting probe session..."); + let mut session = probe::start_session(&run_args.probe_args.chip, run_args.probe_args.probe.clone()).await?; + + // NOTE: First flash then set secure boot configuration! Doing it the other way around causes + // flashing to almost always fail. + + log::info!( + "Flashing {} to target at address 0x{:02x}", + output_path.display(), + flash_start + ); + + let options = flashing::DownloadOptions::default(); + flashing::download_file_with_options( + &mut session, + output_path, + flashing::Format::Bin(flashing::BinOptions { + base_address: Some(flash_start), + skip: 0, + }), + options, + ) + .context("Failed to flash binary")?; + + Ok(DownloadOutput { session, rkth }) +} + +async fn download_prelude(path: &Path, probe_args: &ProbeArgs) -> anyhow::Result { + log::debug!("Starting probe session..."); + let mut session = probe::start_session(&probe_args.chip, probe_args.probe.clone()).await?; + + log::info!("Flashing {} to target", path.display(),); + + let options = flashing::DownloadOptions::default(); + flashing::download_file_with_options( + &mut session, + path, + flashing::Format::Elf(ElfOptions::default()), + options, + ) + .context("Failed to flash binary")?; + + Ok(session) +} diff --git a/bootloader-tool/src/commands/generate.rs b/bootloader-tool/src/commands/generate.rs new file mode 100644 index 00000000..8ba400c8 --- /dev/null +++ b/bootloader-tool/src/commands/generate.rs @@ -0,0 +1,11 @@ +use crate::{GenerateCommands, config::Config, processors}; + +pub async fn process(config: &Config, command: GenerateCommands) -> anyhow::Result<()> { + match command { + GenerateCommands::Certificates(args) => processors::certificates::generate(args, config), + GenerateCommands::Otp => { + let _ = processors::otp::generate(config)?; + Ok(()) + } + } +} diff --git a/bootloader-tool/src/commands/mod.rs b/bootloader-tool/src/commands/mod.rs new file mode 100644 index 00000000..5adb3ba9 --- /dev/null +++ b/bootloader-tool/src/commands/mod.rs @@ -0,0 +1,22 @@ +mod download; +mod generate; +mod run; +mod sign; + +use crate::{Commands, config::Config}; + +pub async fn process(config: &Config, command: Commands) -> anyhow::Result<()> { + match command { + Commands::Generate { subcommand } => generate::process(config, subcommand).await, + Commands::Sign { subcommand } => { + let _ = sign::process(config, subcommand).await?; + Ok(()) + } + Commands::Download { subcommand } => { + download::process(config, subcommand).await?; + Ok(()) + } + Commands::Run { subcommand } => run::process(config, subcommand).await, + Commands::Fuse => todo!(), + } +} diff --git a/bootloader-tool/src/commands/run.rs b/bootloader-tool/src/commands/run.rs new file mode 100644 index 00000000..d821012f --- /dev/null +++ b/bootloader-tool/src/commands/run.rs @@ -0,0 +1,62 @@ +use probe_rs::MemoryInterface; + +use crate::{RunCommands, commands::download::DownloadOutput, config::Config, processors::otp}; + +pub async fn process(config: &Config, command: RunCommands) -> anyhow::Result<()> { + let otp = otp::get_otp(config)?; + + log::debug!("Preparing for run by calling download..."); + let DownloadOutput { mut session, rkth } = super::download::process_other(config, command.clone()).await?; + + let mut core = session.core(0)?; + + log::info!("Setting shadow registers on target"); + core.write_32(0x401301E0, &rkth.as_u32_le())?; + core.write_32(0x401301C0, &otp.as_reversed_u32_be())?; + + // Enable secure boot, skip DICE + let mut boot0 = 0u32; + boot0 |= 0b0101; // Use QSPI B + boot0 |= 0b111 << 4; // Completely disable ISP mode + boot0 |= 0b10 << 13; // Force Trust-Zone mode + boot0 |= 0b01 << 20; // Enable secure boot + boot0 |= 0b1 << 23; // Skip DICE + boot0 |= 0b101 << 24; // Configure boot_fail_pin port 5 + boot0 |= 0b00111 << 27; // Configure boot_fail_pin pin 7 + + core.write_32(0x40130180, &[boot0])?; + + let mut boot1 = 0u32; + boot1 |= 1 << 14; // Reset pin enable. + boot1 |= 2 << 15; // Reset pin port 2. + boot1 |= 12 << 18; // Reset pin number 12. + + core.write_32(0x40130184, &[boot1])?; + + let mut buf = [0u32; 1]; + core.read_32(0x40130194, &mut buf)?; + + // buf[0] |= 0b1111; // Revoke root cert 2. + buf[0] &= !(1 << 7); // Set USE_PUF to 0 + + core.write_32(0x40130194, &buf)?; + + core.reset().unwrap(); + drop(core); + drop(session); + + log::info!("Target configured and reset, attaching..."); + + let (RunCommands::Bootloader(run_args) | RunCommands::Application { run_args, .. }) = command; + + let mut command = std::process::Command::new(&run_args.probe_rs_path); + command.args(["attach", "--chip", &run_args.probe_args.chip]); + + if let Some(probe) = run_args.probe_args.probe.as_ref() { + command.args(["--probe", probe]); + } + + command.arg(run_args.sign_args.input_path).status().unwrap(); + + Ok(()) +} diff --git a/bootloader-tool/src/commands/sign.rs b/bootloader-tool/src/commands/sign.rs new file mode 100644 index 00000000..238b2fff --- /dev/null +++ b/bootloader-tool/src/commands/sign.rs @@ -0,0 +1,130 @@ +use crate::SignCommands; +use crate::config::Config; +use crate::processors::certificates::Rkth; +use crate::processors::mbi::cert_block; +use crate::processors::otp::get_otp; +use crate::processors::{mbi, objcopy}; +use anyhow::Context; +use object::read::elf::ElfFile32; +use std::path::PathBuf; + +pub struct SignOutput { + pub output_path: Option, + pub rkth: Rkth, +} + +pub async fn process(config: &Config, command: SignCommands) -> anyhow::Result { + let (is_bootloader, args) = match command { + SignCommands::Bootloader(sign_arguments) => (true, sign_arguments), + SignCommands::Application(sign_arguments) => (false, sign_arguments), + }; + + let input_data = std::fs::read(&args.input_path)?; + + log::info!("Reading ELF from {}", args.input_path.display()); + let file = ElfFile32::parse(&input_data[..]).context("Could not parse ELF file")?; + + if is_bootloader { + log::info!("Extracting prelude"); + let out = objcopy::remove_non_prelude(&input_data)?; + std::fs::write(args.prelude_path_with_default(), &out).context("Could not write prelude elf file")?; + } + + log::info!("Generating image for {}", args.input_path.display()); + let (image, base_addr) = objcopy::objcopy(&file)?; + + if is_bootloader { + if let Some(bootloader) = &config.bootloader + && bootloader.run_start != base_addr as u64 + { + return Err(anyhow::anyhow!( + "Bootloader image will be run from unexpected address 0x{:x}, should be 0x{:x}", + base_addr, + bootloader.run_start + )); + } + } else if let Some(application) = &config.application + && application.run_start != base_addr as u64 + { + return Err(anyhow::anyhow!( + "Application image will be run from unexpected address 0x{:x}, should be 0x{:x}", + base_addr, + application.run_start + )); + } + + let output_unsigned_path = args.output_unsigned_path_with_default(); + log::debug!("Wrote unsigned bare binary image to {}", output_unsigned_path.display()); + std::fs::write(&output_unsigned_path, &image)?; + + let otp = get_otp(config)?; + + let output_prestage_path = args.output_prestage_path_with_default(); + log::info!( + "Generating prestage MBI using pure Rust in {}", + output_prestage_path.display() + ); + + let cert_block = cert_block::generate(&args.nxpimage_path, config, args.certificate)?; + + mbi::prepare_to_sign( + &output_unsigned_path, + base_addr, + &output_prestage_path, + is_bootloader, + cert_block.clone(), + ) + .context("Could not generate prestage MBI")?; + + let mut signature_path = args.signature_path.clone(); + + if !args.dont_sign && signature_path.is_none() { + log::info!("Signing image {}", args.input_path.display()); + + let Some(cert_chain) = config.certificates.get(args.certificate) else { + return Err(anyhow::anyhow!("Certificate chain {} does not exist", args.certificate)); + }; + + let Some(cert) = cert_chain.0.last() else { + return Err(anyhow::anyhow!("Empty certificate chain")); + }; + + let Some(cert_proto) = &cert.prototype else { + return Err(anyhow::anyhow!( + "No prototype configured for leaf of chain {}", + args.certificate + )); + }; + + let default_path = args.input_path.clone().with_extension("signature.bin"); + mbi::sign(&default_path, &output_prestage_path, &cert_proto.key_path).context("Could not sign image")?; + signature_path = Some(default_path); + } + + let rkth = cert_block.rkth(); + + if let Some(signature_path) = signature_path { + let output_path = args.output_path_with_default(); + log::info!("Merging signature into image"); + mbi::merge_with_signature( + &output_unsigned_path, + base_addr, + signature_path, + &output_path, + is_bootloader, + Some(otp), + cert_block, + ) + .context("Could not merge image with signature")?; + log::info!("Written merged image to {}", output_path.display()); + Ok(SignOutput { + output_path: Some(output_path), + rkth, + }) + } else { + Ok(SignOutput { + output_path: None, + rkth, + }) + } +} diff --git a/bootloader-tool/src/config.rs b/bootloader-tool/src/config.rs new file mode 100644 index 00000000..27be7b98 --- /dev/null +++ b/bootloader-tool/src/config.rs @@ -0,0 +1,101 @@ +#![allow(unused)] + +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +#[derive(Deserialize, Debug)] +pub struct Config { + /// Path of the directory where artifacts are put and can be found. + pub artifacts_path: PathBuf, + + /// Path of the file containing the OTP Master Key, used to encrypt the bootloader image. + pub otp_path: PathBuf, + + /// Certificate chains as used by this project. + pub certificates: Vec, + + /// Arguments related to the setup of the bootloader. + pub bootloader: Option, + + /// Arguments related to application images. + pub application: Option, +} + +#[derive(Deserialize, Debug)] +pub struct CertificateChain(pub Vec); + +#[derive(Deserialize, Debug, Clone)] +pub struct Certificate { + /// Path of the file containing the public facing certificate. + pub path: PathBuf, + + /// When set, the certificate can be generated and the private key can be directly used to generate signatures for binaries. + pub prototype: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct CertificatePrototype { + /// Key type to generate. + pub key_type: KeyType, + + /// Path of the file containing the private key, used to generate signatures for binaries. + pub key_path: PathBuf, +} + +#[derive(Deserialize, Debug)] +pub struct MemoryRange { + pub start: u64, + pub size: u64, +} + +#[derive(Deserialize, Debug)] +pub struct BootloaderArgs { + /// Location in external NOR flash in which the bootloader should live. (must be 0x08001000) + pub flash_start: u64, + /// Location in RAM which the bootloader should run from. (can be anything in RAM) + pub run_start: u64, + /// Maximum binary size of the image. (including certificates, hashes and encryption key) + pub max_size: u64, + /// Memory location of bootloader state. + /// + /// Used to set a new state when ordering to start a specific application image slot. + pub state: MemoryRange, +} + +#[derive(Deserialize, Debug)] +pub struct ApplicationArgs { + /// Starting addresses in external NOR flash for each slot. + pub slot_starts: Vec, + /// Starting RAM address for all images. + /// + /// This address is hard-coded and checked in the bootloader. + pub run_start: u64, + /// Exactly the slot size, which is also the maximum size of the binary image. (including certificates and hashes) + /// + /// This size is hard-coded and checked in the bootloader. + pub slot_size: u64, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum KeyType { + Rsa2048, + Rsa3072, + Rsa4096, +} + +impl KeyType { + pub fn as_str(&self) -> &'static str { + match self { + KeyType::Rsa2048 => "rsa2048", + KeyType::Rsa3072 => "rsa3072", + KeyType::Rsa4096 => "rsa4096", + } + } +} + +impl Config { + pub fn read(path: impl AsRef) -> anyhow::Result { + Ok(toml::from_str::(&std::fs::read_to_string(path)?)?) + } +} diff --git a/bootloader-tool/src/lib.rs b/bootloader-tool/src/lib.rs new file mode 100644 index 00000000..5e1b10e8 --- /dev/null +++ b/bootloader-tool/src/lib.rs @@ -0,0 +1,192 @@ +pub use crate::config::Config; +use clap::{Args, Parser, Subcommand}; +use std::path::PathBuf; + +pub mod commands; +mod config; +pub mod processors; +mod util; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Cli { + /// Configuration file path + #[arg(short, long, value_name = "FILE", default_value = "./config.toml")] + pub config: PathBuf, + + #[command(subcommand)] + pub commands: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Generate keys and certificates + Generate { + #[command(subcommand)] + subcommand: GenerateCommands, + }, + /// Sign binaries for flashing or OTA + Sign { + #[command(subcommand)] + subcommand: SignCommands, + }, + /// Download binaries to the device + Download { + #[command(subcommand)] + subcommand: DownloadCommands, + }, + /// Run binaries by going through the bootloader chain for testing purposes + Run { + #[command(subcommand)] + subcommand: RunCommands, + }, + /// Burn fuse registers with key material and settings + Fuse, +} + +#[derive(Args, Debug, Clone)] +pub struct GenerateCertificatesArguments { + /// Where the nxpcrypto binary can be found. May be on PATH + #[arg(long, default_value = "nxpcrypto")] + nxpcrypto_path: PathBuf, + + /// Where the nxpimage binary can be found. May be on PATH + #[arg(long, default_value = "nxpimage")] + nxpimage_path: PathBuf, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum GenerateCommands { + /// Generate the certificates, certificate block and RKTH + Certificates(GenerateCertificatesArguments), + /// Generate an OTP encryption master key (used for header integrity validation) + Otp, +} + +#[derive(Args, Debug, Clone)] +pub struct SignArguments { + /// Input file path (ELF) + #[arg(short, long, value_name = "INPUT_FILE")] + input_path: PathBuf, + /// Signature file + /// + /// If present, will be checked against image and merged into output path + #[arg(short, long, value_name = "SIGNATURE_FILE")] + signature_path: Option, + /// Output file path of unsigned application (BIN) [default: .unsigned.bin] + #[arg(long, value_name = "OUTPUT_UNSIGNED_FILE")] + output_unsigned_path: Option, + /// Output file path of unsigned Master Boot Image (BIN, without signature) [default: .mbi-proto.bin] + #[arg(long, value_name = "OUTPUT_PRESTAGE_FILE")] + output_prestage_path: Option, + /// Output file path (BIN) [default: .signed.bin] + #[arg(short, long, value_name = "OUTPUT_FILE")] + output_path: Option, + /// Do not actually sign the image only export the prestage for external signing by HSM + #[arg(long)] + dont_sign: bool, + /// Index of the certificate intended to sign the image with + /// + /// Used to generate the appropriate certificate block for this image + /// + /// When this tool is used to generate a signature, the private key also needs to be configured + #[arg(long, value_name = "CERTIFICATE_IDX", default_value = "0")] + certificate: usize, + /// Prelude output file path (BIN) [default: .prelude.bin] + #[arg(long)] + prelude_path: Option, + /// Where the nxpimage binary can be found. May be on PATH + #[arg(long, default_value = "nxpimage")] + nxpimage_path: PathBuf, +} + +impl SignArguments { + pub fn output_unsigned_path_with_default(&self) -> PathBuf { + self.output_unsigned_path + .clone() + .unwrap_or_else(|| self.input_path.clone().with_extension("unsigned.bin")) + } + + pub fn output_prestage_path_with_default(&self) -> PathBuf { + self.output_prestage_path + .clone() + .unwrap_or_else(|| self.input_path.clone().with_extension("mbi-proto.bin")) + } + + pub fn output_path_with_default(&self) -> PathBuf { + self.output_path + .clone() + .unwrap_or_else(|| self.input_path.clone().with_extension("signed.bin")) + } + + pub fn prelude_path_with_default(&self) -> PathBuf { + self.prelude_path + .clone() + .unwrap_or_else(|| self.input_path.clone().with_extension("prelude.elf")) + } +} + +#[derive(Subcommand, Debug, Clone)] +pub enum SignCommands { + /// Sign a bootloader image + Bootloader(SignArguments), + /// Sign an application image + Application(SignArguments), +} + +#[derive(Args, Debug, Clone)] +pub struct RunArguments { + #[command(flatten)] + sign_args: SignArguments, + + #[command(flatten)] + probe_args: ProbeArgs, + + /// Where the probe-rs binary can be found. May be on PATH + #[arg(long, default_value = "probe-rs")] + probe_rs_path: PathBuf, +} + +#[derive(Args, Debug, Clone)] +pub struct ProbeArgs { + /// Which probe to use (passed to probe-rs) + #[arg(short, long, value_name = "PROBE")] + probe: Option, + + /// Type of chip to be programmed (passed to probe-rs) + #[arg(short, long, value_name = "CHIP", default_value = "MIMXRT685SFVKB")] + chip: String, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum RunCommands { + /// Sign a bootloader image + Bootloader(RunArguments), + /// Run an application image in a preferred slot + /// + /// Will also set the appropriate 2nd stage bootloader state to start up the image + Application { + #[command(flatten)] + run_args: RunArguments, + + /// Image slot to which to upload the binary to + #[arg(long, default_value_t = 0)] + slot: u8, + }, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum DownloadCommands { + /// Download the flash prelude containing OTFAD, FCB, etc. + Prelude { + /// Path to the ELF file containing the prelude + #[arg(long)] + prelude_path: PathBuf, + + #[command(flatten)] + probe_args: ProbeArgs, + }, + + #[command(flatten)] + Other(RunCommands), +} diff --git a/bootloader-tool/src/main.rs b/bootloader-tool/src/main.rs new file mode 100644 index 00000000..ae0b5caf --- /dev/null +++ b/bootloader-tool/src/main.rs @@ -0,0 +1,23 @@ +extern crate log; +extern crate pretty_env_logger; + +use anyhow::Context; +use bootloader_tool::{Cli, Config, commands}; +use clap::Parser; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + pretty_env_logger::init(); + + let cli = Cli::parse(); + + let config = Config::read(&cli.config) + .with_context(|| format!("Tried to open --config {}", cli.config.display()))?; + + if let Some(command) = cli.commands { + commands::process(&config, command).await + } else { + eprintln!("Done nothing"); + Ok(()) + } +} diff --git a/bootloader-tool/src/processors/certificates.rs b/bootloader-tool/src/processors/certificates.rs new file mode 100644 index 00000000..56a63f04 --- /dev/null +++ b/bootloader-tool/src/processors/certificates.rs @@ -0,0 +1,192 @@ +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::Context; +use serde::Serialize; + +use crate::{ + GenerateCertificatesArguments, + config::{Certificate, CertificatePrototype, Config}, + util::{bytes_to_u32_le, generate_hex, parse_hex}, +}; + +#[derive(Serialize)] +struct BasicConstraints { + ca: bool, +} + +#[derive(Serialize)] +struct CertificateExtensions { + #[serde(rename = "BASIC_CONSTRAINTS")] + basic_constraints: BasicConstraints, +} + +#[derive(Serialize)] +struct CertificateConfig { + issuer_private_key: PathBuf, + subject_public_key: PathBuf, + extensions: CertificateExtensions, +} + +fn generate_private_key(nxpcrypto: impl AsRef, prototype: &CertificatePrototype) -> anyhow::Result<()> { + // Note: apparently the field name refers to a public key, but it is for the private key. + let output_path = &prototype.key_path; + if std::fs::exists(output_path)? { + log::warn!("Private key {} already generated, skipping...", output_path.display()); + return Ok(()); + } + + log::info!("Generating private key {}", output_path.display()); + + let mut command = Command::new(nxpcrypto.as_ref()); + + command.args(["key", "generate", "-k", prototype.key_type.as_str(), "-e", "PEM", "-o"]); + command.arg(output_path); + + let output = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .with_context(failed_exec(nxpcrypto))?; + + if !output.status.success() { + Err( + anyhow::anyhow!(format!("Failed to generate private key {}", output_path.display())) + .context(String::from_utf8(output.stdout)?), + ) + } else { + Ok(()) + } +} + +fn generate_certificate( + nxpcrypto: impl AsRef, + certificate: &Certificate, + parent_certificate: &Option, + is_leaf: bool, +) -> anyhow::Result<()> { + let Some(prototype) = &certificate.prototype else { + return Err(anyhow::anyhow!( + "Request generation of private key for {}, but no prototype has been defined", + certificate.path.display() + )); + }; + + let issuer_private_key = if let Some(parent_certificate) = &parent_certificate { + let Some(parent_prototype) = &parent_certificate.prototype else { + return Err(anyhow::anyhow!( + "Request generation of private key for {}, but no prototype has been defined", + certificate.path.display() + )); + }; + + parent_prototype.key_path.clone() + } else { + // This certificate is the root certificate, and hence self-signed. + prototype.key_path.clone() + }; + + let input = CertificateConfig { + issuer_private_key, + subject_public_key: prototype.key_path.clone(), + extensions: CertificateExtensions { + basic_constraints: BasicConstraints { ca: !is_leaf }, + }, + }; + + let mut input_file = tempfile::NamedTempFile::new()?; + serde_yml::to_writer(&mut input_file, &input)?; + + let output_path = &certificate.path; + if std::fs::exists(output_path)? { + log::warn!("Certificate {} already generated, skipping...", output_path.display()); + return Ok(()); + } + + log::info!("Generating certificate {}", output_path.display()); + + let mut command = Command::new(nxpcrypto.as_ref()); + + command.args(["cert", "generate", "-e", "PEM", "-c"]); + command.arg(input_file.path()); + command.arg("-o"); + command.arg(output_path); + + let output = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .with_context(failed_exec(nxpcrypto))?; + + if !output.status.success() { + Err(anyhow::anyhow!(format!( + "Failed to build certificate from {:?} (parent: {:?})", + certificate, parent_certificate + )) + .context(String::from_utf8(output.stdout)?)) + } else { + Ok(()) + } +} + +fn generate_single( + nxpcrypto: impl AsRef, + certificate: &Certificate, + parent_certificate: &Option, + is_leaf: bool, +) -> anyhow::Result<()> { + let Some(prototype) = &certificate.prototype else { + return Err(anyhow::anyhow!( + "Request generation of private key for {}, but no prototype has been defined", + certificate.path.display() + )); + }; + + generate_private_key(&nxpcrypto, prototype)?; + generate_certificate(&nxpcrypto, certificate, parent_certificate, is_leaf)?; + + Ok(()) +} + +#[derive(PartialEq, Clone, Debug)] +pub struct Rkth(pub [u8; 32]); + +impl Rkth { + pub fn as_hex(&self) -> String { + generate_hex(&self.0) + } + + pub fn from_hex(str: &str) -> anyhow::Result { + Ok(Self( + parse_hex(str)? + .try_into() + .map_err(|_| anyhow::anyhow!("Input not appropriate size"))?, + )) + } + + pub fn as_u32_le(&self) -> Vec { + bytes_to_u32_le(&self.0) + } +} + +pub fn generate(args: GenerateCertificatesArguments, config: &Config) -> anyhow::Result<()> { + for chain in &config.certificates { + let mut parent = None; + let mut iter = chain.0.iter().peekable(); + while let Some(certificate) = iter.next() { + let is_leaf = iter.peek().is_none(); + generate_single(&args.nxpcrypto_path, certificate, &parent, is_leaf)?; + parent = Some(certificate.clone()); + } + } + + Ok(()) +} + +fn failed_exec<'a>(tool: impl AsRef + 'a) -> impl Fn() -> String + 'a { + move || format!("Could not execute `{}`, is it installed?", tool.as_ref().display()) +} diff --git a/bootloader-tool/src/processors/mbi/cert_block.rs b/bootloader-tool/src/processors/mbi/cert_block.rs new file mode 100644 index 00000000..935de090 --- /dev/null +++ b/bootloader-tool/src/processors/mbi/cert_block.rs @@ -0,0 +1,312 @@ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::Context; +use rsa::{RsaPublicKey, pkcs1v15::VerifyingKey, pkcs8::DecodePublicKey, traits::PublicKeyParts}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use x509_parser::public_key::PublicKey; + +use crate::{ + Config, + processors::{certificates::Rkth, mbi::parse_x509_cert}, +}; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CertBlockConfig { + pub family: String, + pub revision: String, + + #[serde(flatten)] + pub certificates: BTreeMap, + + pub main_root_cert_id: usize, + pub container_output_file: Option, +} + +/// Best-effort attempt to canonicalize the path, if it exists. +fn canonicalize_or_leave(path: impl AsRef) -> PathBuf { + match path.as_ref().canonicalize() { + Ok(path) => path, + Err(_) => path.as_ref().to_owned(), + } +} + +pub fn generate_config( + config: &Config, + certificate_idx: usize, + output_file: Option>, +) -> CertBlockConfig { + let mut certificates = BTreeMap::default(); + for (chain_i, chain) in config.certificates.iter().enumerate() { + for (cert_i, cert) in chain.0.iter().enumerate() { + let name = if cert_i == 0 { + format!("rootCertificate{}File", chain_i) + } else { + format!("chainCertificate{}File{}", chain_i, cert_i - 1) + }; + certificates.insert(name, canonicalize_or_leave(&cert.path)); + } + } + + CertBlockConfig { + family: "mimxrt685s".to_owned(), + revision: "latest".to_owned(), + certificates, + main_root_cert_id: certificate_idx, + container_output_file: output_file.map(|output_file| output_file.as_ref().to_owned()), + } +} + +pub fn generate(nxpimage: impl AsRef, config: &Config, certificate_idx: usize) -> anyhow::Result { + // nxpimage cert-block export -c ./cert-block.yaml + + let mut input_file = tempfile::NamedTempFile::new()?; + let output_file = tempfile::NamedTempFile::new()?; + serde_yml::to_writer( + &mut input_file, + &generate_config(config, certificate_idx, Some(output_file.path())), + )?; + + let mut command = Command::new(nxpimage.as_ref()); + + command.args(["cert-block", "export", "-c"]); + command.arg(input_file.path()); + + let output = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .with_context(move || format!("Could not execute `{}`, is it installed?", nxpimage.as_ref().display()))?; + + if !output.status.success() { + return Err(anyhow::anyhow!(format!( + "Failed to build certificate block from {}", + input_file.as_ref().display() + )) + .context(String::from_utf8(output.stdout)?)); + } + + let rkth_str = String::from_utf8(output.stdout)? + .lines() + .find(|line| line.contains("RKTH")) + .map(|line| line.trim().trim_start_matches("RKTH: ")) + .ok_or_else(|| anyhow::anyhow!("nxpimage output does not contain RKTH"))? + .to_owned(); + let rkth = Rkth::from_hex(&rkth_str)?; + let rkth_str = rkth.as_hex(); // Canonicalize + + log::info!("RKTH: {rkth_str}"); + + CertBlock::from_file(output_file.path(), Some(&rkth)) +} + +/// Cert block as described in UM11147 Fig 239 +#[derive(Debug, Clone)] +pub struct CertBlock { + data: Vec, +} + +impl CertBlock { + /// Read cert block from file that was generated with `nxpimage cert-block export -c ./cert-block.yaml` + /// + /// That file contains padding data, so we read out the lengths from the header in that file to determine the correct length. + pub fn from_file(filename: impl AsRef, rkth: Option<&Rkth>) -> anyhow::Result { + let data = std::fs::read(filename)?; + let mut me = Self { data }; + + // Strip padding + let cert_block_len = me.header_len() + me.cert_table_len() + 4 * Sha256::output_size() as u32; + assert!(me.data.len() >= cert_block_len as usize); + me.data.truncate(cert_block_len as usize); + + // Ensure the cert block is valid + me.verify(rkth)?; + + Ok(me) + } + + /// Get the raw bytes of the cert block + pub fn raw(&self) -> &[u8] { + &self.data + } + + fn cert_count(&self) -> u32 { + u32::from_le_bytes(self.data[0x18..0x1c].try_into().unwrap()) + } + + fn cert_table_len(&self) -> u32 { + u32::from_le_bytes(self.data[0x1c..0x20].try_into().unwrap()) + } + + fn header_len(&self) -> u32 { + let len = u32::from_le_bytes(self.data[0x08..0x0c].try_into().unwrap()); + + if len != 0x20 { + log::warn!("Header length mismatch, expected {:x?}, got {:x?}", 0x20, len); + } + + len + } + + /// Sets the total length of signed bytes, this needs to be updated before signing + pub fn set_total_image_length_in_bytes(&mut self, total_image_length_in_bytes: usize) { + let total_image_length_in_bytes = total_image_length_in_bytes.try_into().unwrap(); + self.data[0x14..0x18].copy_from_slice(&u32::to_le_bytes(total_image_length_in_bytes)); + } + + fn root_key_hashes(&self) -> [[u8; 256 / 8]; 4] { + let rkh_start = (self.header_len() + self.cert_table_len()) as usize; + let data = &self.data[rkh_start..]; + assert_eq!(data.len(), 4 * Sha256::output_size()); + + data.chunks_exact(Sha256::output_size()) + .map(|chunk| chunk.try_into().unwrap()) + .collect::>() + .try_into() + .unwrap() + } + + /// Calculate the RKTH of the contained root key hashes + pub fn rkth(&self) -> Rkth { + let mut hash = Sha256::default(); + for rkh in self.root_key_hashes() { + hash.update(rkh); + } + Rkth(hash.finalize().into()) + } + + fn certificates(&self) -> anyhow::Result>> { + let mut out = vec![]; + let mut next_cert = self.header_len() as usize; + + for i in 0..self.cert_count() { + let cert_len = u32::from_le_bytes(self.data[next_cert..next_cert + 4].try_into().unwrap()) as usize; + if !cert_len.is_multiple_of(4) { + return Err(anyhow::anyhow!("Certificate of cert {i} length is not divisible by 4")); + } + next_cert += 4; + out.push(self.data[next_cert..next_cert + cert_len].to_vec()); + next_cert += cert_len; + } + + assert_eq!(next_cert, (self.header_len() + self.cert_table_len()) as usize); + + Ok(out) + } + + /// Check if this CertBlock is consistent with itself and the Rkth + fn verify(&self, rkth: Option<&Rkth>) -> anyhow::Result<()> { + log::info!("Checking certificate block is consistent"); + + // Ensure the rkth is correct if the user has one + match rkth { + Some(rkth) if &self.rkth() != rkth => { + return Err(anyhow::anyhow!( + "CertBlock RKTH does not match provided Rkth, in block: {:x?}, to check: {rkth:x?}", + self.rkth() + )); + } + _ => {} + } + + // Unpack the certificates + let certs = self.certificates().context("Could not parse certificate list")?; + + log::info!("Got {} certificates in certificate block", certs.len()); + + // Check root cert is in root key hashes + let Some(raw_root_cert) = certs.first() else { + return Err(anyhow::anyhow!("Certificate block contains no certificates")); + }; + + // Get the root public key + let root_cert = parse_x509_cert(raw_root_cert)?; + let root_public_key = root_cert + .public_key() + .parsed() + .context("could not parse root public key")?; + let PublicKey::RSA(root_public_key) = root_public_key else { + return Err(anyhow::anyhow!( + "Invalid public key type: {root_public_key:?} Must be RSA" + )); + }; + + // Hash modulus || exponent, but ensure to strip leading zero bytes from the modulus + let mut rkh = Sha256::default(); + rkh.update( + root_public_key + .modulus + .strip_prefix(&[0]) + .unwrap_or(root_public_key.modulus), + ); + rkh.update(root_public_key.exponent); + let rkh: [u8; 32] = rkh.finalize().into(); + + // Walk the root key hashes to find the right slot + match self.root_key_hashes().iter().position(|&slot| slot == rkh) { + Some(slot) => log::info!("Found key hash in RKT slot {slot}"), + None => { + return Err(anyhow::anyhow!( + "Root cert is not in root key hashes! Cert hash: {rkh:x?}, Hash table: {:x?}", + self.root_key_hashes() + )); + } + } + + // Check root_cert is a CA cert + if !root_cert.is_ca() { + return Err(anyhow::anyhow!("Root cert is not marked as CA")); + } + + // Walk the certificate chain + let mut signing_cert = root_cert; + for (i, cert) in certs.iter().skip(1).enumerate() { + // Check that all intermediate certificates are CA certs + if !signing_cert.is_ca() { + return Err(anyhow::anyhow!("Root cert is not marked as CA")); + } + + let cert = parse_x509_cert(cert)?; + cert.verify_signature(Some(signing_cert.public_key())) + .with_context(|| format!("Could not verify cert number {i}"))?; + + signing_cert = cert; + } + + // Check that the last cert is NOT a CA cert, except for if it is the root cert + if signing_cert.is_ca() && certs.len() > 1 { + return Err(anyhow::anyhow!("Signing cert is marked as CA cert")); + } + + // Check that the last public key can be parsed + RsaPublicKey::from_public_key_der(signing_cert.public_key().raw) + .context("Could not parse image signing key")?; + + Ok(()) + } + + /// Get the public key of the certificates that is used to sign the image + pub fn verifying_key(&self) -> VerifyingKey { + let certs = self.certificates().expect("Ensured in verify"); + let signing_cert = certs.last().expect("Ensured in verify").as_slice(); + let signing_cert = parse_x509_cert(signing_cert).expect("Ensured in verify"); + + // Parse the final public key into something the rsa crate can work with + let image_signing_key = RsaPublicKey::from_public_key_der(signing_cert.public_key().raw) + .expect("Ensured by verify being called in from file"); + + VerifyingKey::new(image_signing_key) + } + + /// Get the length of the signature for the image + pub fn signature_len(&self) -> usize { + let key: RsaPublicKey = self.verifying_key().into(); + key.size() + } +} diff --git a/bootloader-tool/src/processors/mbi/mod.rs b/bootloader-tool/src/processors/mbi/mod.rs new file mode 100644 index 00000000..2b2cbed2 --- /dev/null +++ b/bootloader-tool/src/processors/mbi/mod.rs @@ -0,0 +1,509 @@ +#![allow(clippy::len_without_is_empty)] + +pub mod cert_block; + +use sha2::Digest; +use std::collections::BTreeMap; +use std::path::Path; +use std::process::{Command, Stdio}; +use tempfile::NamedTempFile; + +use crate::processors::certificates::Rkth; +use crate::processors::mbi::cert_block::{CertBlock, CertBlockConfig}; +use crate::processors::otp::Otp; +use anyhow::{Context, anyhow, bail}; +use hmac::{Hmac, Mac}; +use rsa::RsaPrivateKey; +use rsa::pkcs1v15::{Signature, SigningKey}; +use rsa::pkcs8::DecodePrivateKey; +use rsa::signature::{SignatureEncoding, SignerMut, Verifier}; +use sha2::Sha256; +use x509_parser::asn1_rs::FromDer; +use x509_parser::certificate::X509Certificate; +use x509_parser::oid_registry::Oid; + +type HmacSha256 = Hmac; + +/// Image header, this is a vector table with some fields modified +#[derive(Debug, Clone)] +pub struct ImageHeader { + ivt: Vec, +} + +impl ImageHeader { + /// Take a vector table and merge in the header information + pub fn new(mut ivt: Vec, image_type: ImageType, header_offset: u32, load_addr: u32) -> Self { + assert_eq!(ivt.len(), 0x40); + + ivt[0x24..0x28].copy_from_slice(&u32::to_le_bytes(image_type.as_u32())); + ivt[0x28..0x2C].copy_from_slice(&u32::to_le_bytes(header_offset)); + ivt[0x34..0x38].copy_from_slice(&u32::to_le_bytes(load_addr)); + + Self { ivt } + } + + /// Set the image length, this includes the header, data and cert block + pub fn set_image_length(&mut self, image_length: usize) { + let image_length = image_length.try_into().unwrap(); + self.ivt[0x20..0x24].copy_from_slice(&u32::to_le_bytes(image_length)); + } + + /// Get the raw data + pub fn raw(&self) -> &[u8] { + &self.ivt + } + + /// Get the [ImageKind] + pub fn image_kind(&self) -> ImageKind { + ImageKind::from_u8(self.ivt[0x24]).expect("image kind was set in new") + } +} + +/// Bitset for the image type header field +#[derive(Debug, Copy, Clone)] +pub struct ImageType { + pub key_store_included: bool, + pub tz_m_image_type: TrustZone, + pub tz_m_preset: TrustZonePreset, + pub enable_hw_user_mode_keys: bool, + pub image_kind: ImageKind, +} + +impl ImageType { + pub fn new(image_kind: ImageKind) -> Self { + Self { + key_store_included: false, + tz_m_image_type: TrustZone::Enabled, + tz_m_preset: TrustZonePreset::NotIncluded, + enable_hw_user_mode_keys: false, + image_kind, + } + } + + fn as_u32(&self) -> u32 { + let mut out = 0; + let Self { + key_store_included, + tz_m_image_type, + tz_m_preset, + enable_hw_user_mode_keys, + image_kind: image_type, + } = *self; + + out |= (key_store_included as u32 & 1) << 15; + out |= (tz_m_image_type as u32 & 1) << 14; + out |= (tz_m_preset as u32 & 1) << 13; + out |= (enable_hw_user_mode_keys as u32 & 1) << 12; + out |= image_type as u32; + + out + } +} + +#[derive(Debug, Copy, Clone)] +pub enum ImageKind { + Plain = 0, + /// Note: This uses the Encrypted image layout, but omits the Enc. Image Header? + PlainSigned = 1, + PlainWithCrc = 2, + EncryptedSigned = 3, + XipPlainSigned = 4, + XipPlainWithCrc = 5, +} + +impl ImageKind { + fn from_u8(value: u8) -> Option { + Some(match value { + 0 => Self::Plain, + 1 => Self::PlainSigned, + 2 => Self::PlainWithCrc, + 3 => Self::EncryptedSigned, + 4 => Self::XipPlainSigned, + 5 => Self::XipPlainWithCrc, + _ => return None, + }) + } +} + +impl ImageKind { + pub fn has_hmac(&self) -> bool { + match self { + ImageKind::Plain | ImageKind::PlainWithCrc | ImageKind::XipPlainSigned | ImageKind::XipPlainWithCrc => { + false + } + ImageKind::PlainSigned | ImageKind::EncryptedSigned => true, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum TrustZone { + Enabled = 0, + Disabled = 1, +} + +#[derive(Debug, Copy, Clone)] +pub enum TrustZonePreset { + NotIncluded = 0, + Included = 1, +} + +fn parse_x509_cert(bytes: &[u8]) -> anyhow::Result> { + let (rem, cert) = X509Certificate::from_der(bytes)?; + if rem.len() >= 4 { + bail!( + "parsed certificate has too much padding: {} expected at most 3", + rem.len() + ); + } + + const ALGO: Oid<'static> = x509_parser::oid_registry::OID_PKCS1_SHA256WITHRSA; + if cert.signature_algorithm.algorithm != ALGO { + bail!( + "Wrong signature algorithm, got: {:?}, expected {:?}", + cert.signature_algorithm, + ALGO + ); + } + + Ok(cert) +} + +/// All data that is contained in an MBI +#[derive(Debug, Clone)] +pub struct Image { + header: ImageHeader, + data: Vec, + cert_block: CertBlock, +} + +impl Image { + const DATA_ALIGN: usize = 4; + + pub fn new(image: Vec, base_addr: u32, image_type: ImageType, mut cert_block: CertBlock) -> Self { + let (ivt, data) = image.split_at(0x40); + + // Make sure we do not use the original image without padding by accident + let ivt = ivt.to_vec(); + let mut data = data.to_vec(); + drop(image); + + // Pad out data so the cert block is aligned correctly + while data.len() % Self::DATA_ALIGN != 0 { + data.push(0); + } + + let image_len = ivt.len() + data.len(); + + let mut header = ImageHeader::new(ivt, image_type, image_len as u32, base_addr); + + let signed_len = image_len + cert_block.raw().len(); + cert_block.set_total_image_length_in_bytes(signed_len); + + let hmac_len = if image_type.image_kind.has_hmac() { + Sha256::output_size() + } else { + 0 + }; + header.set_image_length(signed_len + hmac_len + cert_block.signature_len()); + + Self { + header, + data, + cert_block, + } + } + + fn hmac(&self, otp: Otp) -> anyhow::Result> { + let hmac_key = otp.hmac_key()?; + + let mut mac = HmacSha256::new_from_slice(&hmac_key.0)?; + + mac.update(self.header.raw()); + let result = mac.finalize(); + + let hmac = &result.into_bytes()[..]; + + Ok(Vec::from(hmac)) + } + + /// Get the binary as it should be signed + /// + /// This does not include the HMAC of the header since that is not included in the signature. + pub fn sign_me(&self) -> Vec { + let mut out = vec![]; + out.extend(self.header.raw()); + out.extend(&self.data); + out.extend(self.cert_block.raw()); + + out + } + + /// Check if the signature matches this image and RKTH + pub fn check(&self, signature: &[u8], rkth: &Rkth) -> anyhow::Result<()> { + log::info!("Checking if signature matches image"); + + if signature.len() != self.cert_block.signature_len() { + bail!( + "signature length mismatch, expected {} got {}", + self.cert_block.signature_len(), + signature.len() + ); + } + + if &self.cert_block.rkth() != rkth { + bail!( + "CertBlock RKTH does not match expected. CertBlock: {:x?}, Expected: {rkth:x?}", + self.cert_block.rkth() + ); + } + + let signature = Signature::try_from(signature)?; + self.cert_block + .verifying_key() + .verify(&self.sign_me(), &signature) + .context("Could not verify signature with the current image")?; + + log::info!("OK - Signature matches image"); + + Ok(()) + } + + /// Combine the signature with this image + /// + /// This also inserts the HMAC of the header if it is needed for this image type. + pub fn merge(&self, signature: &[u8], otp: Option) -> anyhow::Result> { + // Ensure the signature matches what we have + self.check(signature, &self.cert_block.rkth())?; + + let mut out = vec![]; + out.extend(self.header.raw()); + + if self.header.image_kind().has_hmac() { + let otp = otp.ok_or_else(|| anyhow::anyhow!("Expected OTP to be passed for signing the bootloader"))?; + out.extend(self.hmac(otp)?) + } + + out.extend(&self.data); + out.extend(self.cert_block.raw()); + out.extend(signature); + + Ok(out) + } +} + +fn load_image( + input_path: &impl AsRef, + base_addr: u32, + is_bootloader: bool, + cert_block: CertBlock, +) -> anyhow::Result { + let plain_image = std::fs::read(input_path)?; + + let image_type = if is_bootloader { + // Note: ROM bootloader only accepts xip plain signed images when secure_boot_en bit is unset. + ImageKind::XipPlainSigned + } else { + // Note: ec-slimloader loads the application to RAM, but skboot_authenticate requires the image to be marked for XIP. + ImageKind::XipPlainSigned + }; + + let image = Image::new(plain_image, base_addr, ImageType::new(image_type), cert_block); + Ok(image) +} + +/// Produce an image that can be signed by an external source +pub fn prepare_to_sign( + input_path: impl AsRef, + base_addr: u32, + output_path: impl AsRef, + is_bootloader: bool, + cert_block: CertBlock, +) -> anyhow::Result<()> { + let image = load_image(&input_path, base_addr, is_bootloader, cert_block)?; + let signing_body = image.sign_me(); + + std::fs::write(output_path, &signing_body).context("Could not write prepared image")?; + + Ok(()) +} + +/// Merge an image with a signature +pub fn merge_with_signature( + input_path: impl AsRef, + base_addr: u32, + signature_path: impl AsRef, + output_path: impl AsRef, + is_bootloader: bool, + otp: Option, + cert_block: CertBlock, +) -> anyhow::Result<()> { + let image = load_image(&input_path, base_addr, is_bootloader, cert_block)?; + let signature = std::fs::read(&signature_path)?; + + std::fs::write(output_path.as_ref(), image.merge(&signature, otp)?)?; + + Ok(()) +} + +/// Sign the image (useful for testing without HSM) +pub fn sign( + signature_path: impl AsRef, + prepared_path: impl AsRef, + private_key_path: impl AsRef, +) -> anyhow::Result<()> { + let private_key = std::fs::read_to_string(private_key_path)?; + let private_key = RsaPrivateKey::from_pkcs8_pem(&private_key)?; + let mut signing_key = SigningKey::::new(private_key); + + let signing_body = std::fs::read(&prepared_path)?; + let signature = signing_key.sign(&signing_body); + + std::fs::write(&signature_path, signature.to_bytes()).context("Could not write signature")?; + Ok(()) +} + +/// Generate a MBI using our pure Rust implementation. +pub fn generate_pure( + input_path: impl AsRef, + base_addr: u32, + output_path: impl AsRef, + is_bootloader: bool, + otp: Option, + cert_block: CertBlock, + private_key_path: impl AsRef, +) -> anyhow::Result<()> { + let mut prepared_path = output_path.as_ref().to_path_buf(); + prepared_path.set_extension(".prepared.bin"); + prepare_to_sign( + &input_path, + base_addr, + &prepared_path, + is_bootloader, + cert_block.clone(), + )?; + + let mut signature_path = output_path.as_ref().to_path_buf(); + signature_path.set_extension(".signature.bin"); + sign(&signature_path, &prepared_path, private_key_path)?; + + merge_with_signature( + input_path, + base_addr, + signature_path, + output_path, + is_bootloader, + otp, + cert_block, + )?; + + Ok(()) +} + +/// Generate a MBI using the original NXP SPSDK tooling. +pub fn generate_nxp( + nxpimage: impl AsRef, + input_path: impl AsRef, + base_addr: u32, + output_path: impl AsRef, + is_bootloader: bool, + cert_block: CertBlockConfig, +) -> anyhow::Result<()> { + let mut config: BTreeMap = BTreeMap::default(); + + let mut cert_block_file = NamedTempFile::new()?; + serde_yml::to_writer(&mut cert_block_file, &cert_block)?; + + config.insert( + "certBlock".to_owned(), + cert_block_file + .path() + .to_str() + .ok_or_else(|| anyhow!("Path not a string"))? + .to_owned(), + ); + + config.insert("outputImageExecutionAddress".to_owned(), format!("{base_addr:#x}")); + + config.insert( + "inputImageFile".to_owned(), + input_path + .as_ref() + .to_str() + .ok_or_else(|| anyhow!("Path not a string"))? + .into(), + ); + + let output_path_abs = std::env::current_dir()?.join(output_path.as_ref()); + config.insert( + "masterBootOutputFile".to_owned(), + output_path_abs + .to_str() + .ok_or_else(|| anyhow!("Path not a string"))? + .into(), + ); + + log::debug!("Config: {config:#?}"); + + let mut command = Command::new(nxpimage.as_ref()); + command.current_dir("./artifacts"); + + let mbi_conf_path = if is_bootloader { + "mbi-bootloader.yaml" + } else { + "mbi-application.yaml" + }; + + command.args(["mbi", "export", "-c", mbi_conf_path]); + + for (k, v) in config { + command.args(["-oc", &format!("{k}={v}")]); + } + + eprintln!("{:?}", command); + + let output = command + .stdin(Stdio::inherit()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .with_context(|| format!("Could not execute command {command:?}"))?; + + if !output.status.success() { + return Err( + anyhow::anyhow!(format!("Failed to build MBI image from {}", mbi_conf_path)) + .context(String::from_utf8(output.stdout)?), + ); + } + + let input = std::fs::read(&input_path)?; + let output = std::fs::read(&output_path)?; + let diff_len = output.len() - input.len(); + + log::debug!("Output len: 0x{:x}", output.len()); + log::debug!("Added len: 0x{diff_len:x}"); + + if diff_len > 0xC47 { + return Err(anyhow::anyhow!( + "Added more than expected to output image when signing: {diff_len}" + )); + } + + // Performing checks on output image + #[allow(clippy::if_same_then_else)] + let expected_image_type = if is_bootloader { 0x0004u32 } else { 0x0004u32 }; + + let image_type = u32::from_le_bytes((&output[0x24..0x28]).try_into().unwrap()); + if image_type != expected_image_type { + return Err(anyhow::anyhow!( + "Failed to generate expected image type 0x{:x}, got 0x{:x}", + expected_image_type, + image_type + )); + } + log::debug!("Got expected image type 0x{expected_image_type:x}"); + + // TODO more checks + + Ok(()) +} diff --git a/bootloader-tool/src/processors/mod.rs b/bootloader-tool/src/processors/mod.rs new file mode 100644 index 00000000..c2d96517 --- /dev/null +++ b/bootloader-tool/src/processors/mod.rs @@ -0,0 +1,5 @@ +pub mod certificates; +pub mod mbi; +pub mod objcopy; +pub mod otp; +pub mod probe; diff --git a/bootloader-tool/src/processors/objcopy.rs b/bootloader-tool/src/processors/objcopy.rs new file mode 100644 index 00000000..06471338 --- /dev/null +++ b/bootloader-tool/src/processors/objcopy.rs @@ -0,0 +1,111 @@ +use anyhow::Context; +use object::elf::{SHT_NOBITS, SHT_PROGBITS}; +use object::read::elf::{ElfFile32, ProgramHeader}; +use object::{Object, ObjectSegment}; +use std::ops::Range; + +const PRELUDE_ADDRESS_RANGE: Range = 0x08000000..0x08001000; + +pub fn objcopy(file: &ElfFile32) -> anyhow::Result<(Vec, u32)> { + // Sanity checks + let mut last_paddr = 0; + let mut segments = vec![]; + for segment in file.segments() { + let filesz = segment.elf_program_header().p_filesz(file.endianness()); + let memsz = segment.elf_program_header().p_memsz(file.endianness()); + + if filesz == 0 { + // Skip bss to reduce size of image to flash. The bss will be cleared during startup anyway. + continue; + } + + if filesz > memsz { + return Err(anyhow::anyhow!("p_filesz larger than p_memsz")); + } + if memsz > filesz { + return Err(anyhow::anyhow!("Segment only partially a bss segment")); + } + + let paddr = segment.elf_program_header().p_paddr(file.endianness()); + if PRELUDE_ADDRESS_RANGE.contains(&paddr) { + continue; + } + if paddr < last_paddr { + return Err(anyhow::anyhow!( + "Segments not in order of physical address or overlapping segments" + )); + } + last_paddr = paddr + segment.elf_program_header().p_memsz(file.endianness()); + + segments.push(segment); + } + + let base_addr = segments + .iter() + .map(|segment| segment.elf_program_header().p_paddr.get(file.endianness())) + .min() + .unwrap(); + let top_addr = segments + .iter() + .map(|segment| { + segment.elf_program_header().p_paddr(file.endianness()) + + segment.elf_program_header().p_filesz(file.endianness()) + }) + .max() + .unwrap(); + let output_size = top_addr - base_addr; + + log::debug!("Image base address: 0x{base_addr:0x}"); + log::debug!("Image entry address: 0x{:0x}", file.entry()); + log::debug!("Image output size: 0x{output_size:0x}"); + + // The bootrom will start executing at offset 0x130 of the final image. Add 1 for thumb mode. + let expected_entry = (base_addr + 0x131) as u64; + if file.entry() != expected_entry { + return Err(anyhow::anyhow!(format!( + "Image entrypoint 0x{:0x} not at expected address 0x{:0x}", + file.entry(), + expected_entry + ))); + } + + // TODO check VTOR + // TODO check image size + // TODO check execution address + + // Assemble BIN image by copying all segments directly + let mut image = vec![0; output_size as usize]; + for segment in segments { + let paddr = segment.elf_program_header().p_paddr(file.endianness()); + + image[paddr as usize - base_addr as usize + ..paddr as usize - base_addr as usize + segment.size() as usize] + .copy_from_slice(segment.data().unwrap()); + } + + Ok((image, base_addr)) +} + +pub fn remove_non_prelude(data: &[u8]) -> anyhow::Result> { + let mut builder = object::build::elf::Builder::read32(data).context("Could not parse ELF")?; + + for section in builder.sections.iter_mut() { + if PRELUDE_ADDRESS_RANGE.contains(&(section.sh_addr as u32)) { + // This segment is part of the prelude + continue; + } + + if section.sh_type != SHT_PROGBITS && section.sh_type != SHT_NOBITS { + // This is not a data section so keep it + continue; + } + + section.delete = true; + } + + builder.delete_orphans(); + + let mut out = vec![]; + builder.write(&mut out)?; + Ok(out) +} diff --git a/bootloader-tool/src/processors/otp.rs b/bootloader-tool/src/processors/otp.rs new file mode 100644 index 00000000..5d895e44 --- /dev/null +++ b/bootloader-tool/src/processors/otp.rs @@ -0,0 +1,72 @@ +use aes::{ + Aes256, + cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray}, +}; +use anyhow::Context; + +use crate::{ + config::Config, + util::{bytes_to_u32_be, generate_hex, parse_hex}, +}; + +#[derive(Clone)] +pub struct Otp(pub [u8; 32]); + +impl Otp { + pub fn generate() -> Self { + let mut buf = [0u8; 32]; + rand::fill(&mut buf); + Self(buf) + } + + pub fn as_hex(&self) -> String { + generate_hex(&self.0) + } + + pub fn from_hex(str: &str) -> anyhow::Result { + Ok(Self(parse_hex(str)?.try_into().map_err(|_| { + anyhow::anyhow!("Input not appropriate size") + })?)) + } + + pub fn as_reversed_u32_be(&self) -> Vec { + let mut result = bytes_to_u32_be(&self.0); + result.reverse(); + result + } + + pub fn hmac_key(&self) -> anyhow::Result { + // See UM11147 page 1246, 43.2.3.1 HMAC_KEY + let aes = Aes256::new_from_slice(&self.0)?; + let mut block = GenericArray::from([0u8; 16]); + aes.encrypt_block(&mut block); + + Ok(HmacKey(block.as_slice().try_into().unwrap())) + } +} + +pub struct HmacKey(pub [u8; 16]); + +pub fn generate(config: &Config) -> anyhow::Result { + if std::fs::exists(&config.otp_path)? { + log::warn!( + "OTP file {} already generated, skipping...", + &config.otp_path.display() + ); + return get_otp(config); + } + + let otp = Otp::generate(); + std::fs::write(&config.otp_path, otp.as_hex())?; + + log::info!("Generated and wrote OTP key"); + Ok(otp) +} + +pub fn get_otp(config: &Config) -> anyhow::Result { + let path = &config.otp_path; + let otp_hex_str = std::fs::read_to_string(&config.otp_path) + .with_context(|| format!("Failed to open OTP file {}", path.display()))?; + + Otp::from_hex(&otp_hex_str) +} diff --git a/bootloader-tool/src/processors/probe.rs b/bootloader-tool/src/processors/probe.rs new file mode 100644 index 00000000..8450d62d --- /dev/null +++ b/bootloader-tool/src/processors/probe.rs @@ -0,0 +1,31 @@ +use probe_rs::{ + Permissions, Session, + probe::{DebugProbeSelector, list::Lister}, +}; + +pub async fn start_session(chip: &str, probe_selector: Option) -> anyhow::Result { + let session = if let Some(ref probe) = probe_selector { + Lister::new() + .open(DebugProbeSelector::try_from(&**probe)?) + .await? + } else { + let probes = Lister::new().list_all().await; + let probe = match probes.len() { + 0 => return Err(anyhow::anyhow!("No probe found")), + 1 => probes.first().unwrap(), + _ => { + eprintln!("Use --probe to select one of the following available Probes:"); + for (i, probe_info) in probes.iter().enumerate() { + eprintln!("{i}: {probe_info}"); + } + + std::process::exit(1); + } + }; + + probe.open().unwrap() + } + .attach(chip, Permissions::default())?; + + Ok(session) +} diff --git a/bootloader-tool/src/util/mod.rs b/bootloader-tool/src/util/mod.rs new file mode 100644 index 00000000..13b3341f --- /dev/null +++ b/bootloader-tool/src/util/mod.rs @@ -0,0 +1,30 @@ +use anyhow::Context; +use itertools::Itertools; + +pub fn parse_hex(s: &str) -> anyhow::Result> { + s.as_bytes() + .chunks_exact(2) + .map(|chunk| u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16)) + .try_collect::<_, Vec<_>, _>() + .context("Input not hexidecimal") +} + +pub fn generate_hex(buf: &[u8]) -> String { + let mut result = String::new(); + for b in buf { + result.push_str(&format!("{b:02X}")); + } + result +} + +pub fn bytes_to_u32_le(b: &[u8]) -> Vec { + b.chunks_exact(4) + .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap())) + .collect::>() +} + +pub fn bytes_to_u32_be(b: &[u8]) -> Vec { + b.chunks_exact(4) + .map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap())) + .collect::>() +} diff --git a/bootloader-tool/src/util/progress.rs b/bootloader-tool/src/util/progress.rs new file mode 100644 index 00000000..59f2d2aa --- /dev/null +++ b/bootloader-tool/src/util/progress.rs @@ -0,0 +1,193 @@ +// Adapted from probe-rs-tools/src/bin/probe-rs/util/flash.rs of probe-rs v0.27.0 + +use std::cell::RefCell; +use std::time::Duration; + +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use probe_rs::flashing::{FlashLayout, FlashProgress, ProgressEvent}; + +pub fn flash_progress() -> FlashProgress<'static> { + let multi_progress = MultiProgress::new(); + let progress_bars = RefCell::new(ProgressBars { + erase: ProgressBarGroup::new(" Erasing"), + fill: ProgressBarGroup::new(" Reading flash"), + program: ProgressBarGroup::new(" Programming"), + }); + + FlashProgress::new(move |event| { + let mut progress_bars = progress_bars.borrow_mut(); + + match event { + ProgressEvent::Initialized { + chip_erase, + phases, + restore_unwritten, + } => { + // Build progress bars. + if chip_erase { + progress_bars + .erase + .add(multi_progress.add(ProgressBar::new(0))); + } + + if phases.len() > 1 { + progress_bars.erase.append_phase(); + progress_bars.program.append_phase(); + progress_bars.fill.append_phase(); + } + + let mut flash_layout = FlashLayout::default(); + for phase_layout in phases { + if restore_unwritten { + let fill_size = phase_layout.fills().iter().map(|s| s.size()).sum::(); + progress_bars + .fill + .add(multi_progress.add(ProgressBar::new(fill_size))); + } + + if !chip_erase { + let sector_size = + phase_layout.sectors().iter().map(|s| s.size()).sum::(); + progress_bars + .erase + .add(multi_progress.add(ProgressBar::new(sector_size))); + } + + progress_bars + .program + .add(multi_progress.add(ProgressBar::new(0))); + + flash_layout.merge_from(phase_layout); + } + + // TODO: progress bar for verifying? + } + ProgressEvent::StartedProgramming { length } => { + progress_bars.program.mark_start_now(); + progress_bars.program.set_length(length); + } + ProgressEvent::StartedErasing => { + progress_bars.erase.mark_start_now(); + } + ProgressEvent::StartedFilling => { + progress_bars.fill.mark_start_now(); + } + ProgressEvent::PageProgrammed { size, .. } => { + progress_bars.program.inc(size as u64); + } + ProgressEvent::SectorErased { size, .. } => progress_bars.erase.inc(size), + ProgressEvent::PageFilled { size, .. } => progress_bars.fill.inc(size), + ProgressEvent::FailedErasing => { + progress_bars.erase.abandon(); + progress_bars.program.abandon(); + } + ProgressEvent::FinishedErasing => progress_bars.erase.finish(), + ProgressEvent::FailedProgramming => progress_bars.program.abandon(), + ProgressEvent::FinishedProgramming => progress_bars.program.finish(), + ProgressEvent::FailedFilling => progress_bars.fill.abandon(), + ProgressEvent::FinishedFilling => progress_bars.fill.finish(), + ProgressEvent::DiagnosticMessage { .. } => {} + } + }) +} + +struct ProgressBars { + erase: ProgressBarGroup, + fill: ProgressBarGroup, + program: ProgressBarGroup, +} + +pub struct ProgressBarGroup { + message: String, + bars: Vec, + selected: usize, + append_phase: bool, +} + +impl ProgressBarGroup { + pub fn new(message: &str) -> Self { + Self { + message: message.to_string(), + bars: vec![], + selected: 0, + append_phase: false, + } + } + + fn idle() -> ProgressStyle { + ProgressStyle::with_template("{msg:.green.bold} {spinner} {percent:>3}% [{bar:20}]") + .unwrap() + .tick_chars("⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈✔") + .progress_chars("--") + } + + fn active() -> ProgressStyle { + ProgressStyle::with_template("{msg:.green.bold} {spinner} {percent:>3}% [{bar:20}] {bytes:>10} @ {bytes_per_sec:>12} (ETA {eta})") + .unwrap() + .tick_chars("⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈✔") + .progress_chars("##-") + } + + fn finished() -> ProgressStyle { + ProgressStyle::with_template("{msg:.green.bold} {spinner} {percent:>3}% [{bar:20}] {bytes:>10} @ {bytes_per_sec:>12} (took {elapsed})") + .unwrap() + .tick_chars("⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈✔") + .progress_chars("##") + } + + pub fn add(&mut self, bar: ProgressBar) { + if self.append_phase { + bar.set_message(format!("{} {}", self.message, self.bars.len() + 1)); + } else { + bar.set_message(self.message.clone()); + } + bar.set_style(Self::idle()); + bar.enable_steady_tick(Duration::from_millis(100)); + bar.reset_elapsed(); + + self.bars.push(bar); + } + + pub fn set_length(&mut self, length: u64) { + if let Some(bar) = self.bars.get(self.selected) { + bar.set_length(length); + } + } + + pub fn inc(&mut self, size: u64) { + if let Some(bar) = self.bars.get(self.selected) { + bar.set_style(Self::active()); + bar.inc(size); + } + } + + pub fn abandon(&mut self) { + if let Some(bar) = self.bars.get(self.selected) { + bar.abandon(); + } + self.next(); + } + + pub fn finish(&mut self) { + if let Some(bar) = self.bars.get(self.selected) { + bar.set_style(Self::finished()); + bar.finish(); + } + self.next(); + } + + pub fn next(&mut self) { + self.selected += 1; + } + + pub fn append_phase(&mut self) { + self.append_phase = true; + } + + pub fn mark_start_now(&mut self) { + if let Some(bar) = self.bars.get(self.selected) { + bar.reset_elapsed(); + bar.reset_eta(); + } + } +} diff --git a/bootloader-tool/tests/behavior_matches_nxpimage.rs b/bootloader-tool/tests/behavior_matches_nxpimage.rs new file mode 100644 index 00000000..be9368f0 --- /dev/null +++ b/bootloader-tool/tests/behavior_matches_nxpimage.rs @@ -0,0 +1,132 @@ +use std::path::PathBuf; + +use anyhow::Context; +use bootloader_tool::processors::{ + mbi::{self, cert_block}, + objcopy, + otp::Otp, +}; +use bootloader_tool::Config; +use object::read::elf::ElfFile32; + +fn get_private_key(config: &Config, certificate_idx: usize) -> PathBuf { + config.certificates[certificate_idx] + .0 + .last() + .as_ref() + .unwrap() + .prototype + .as_ref() + .unwrap() + .key_path + .clone() +} + +#[test] +fn test_app() { + const CERTIFICATE_IDX: usize = 1; + + let config = Config::read("config.toml").unwrap(); + let (data, base_addr) = read_example("application"); + assert_same(&data, base_addr, false, None, &config, CERTIFICATE_IDX); +} + +#[test] +fn test_bootloader() { + test_bootloader_padding(0); +} + +#[test] +fn test_bootloader_padding_1() { + test_bootloader_padding(1); +} + +#[test] +fn test_bootloader_padding_5() { + test_bootloader_padding(5); +} +#[test] +fn test_bootloader_padding_9() { + test_bootloader_padding(9); +} +#[test] +fn test_bootloader_padding_17() { + test_bootloader_padding(17); +} + +fn test_bootloader_padding(added_bytes: u8) { + const CERTIFICATE_IDX: usize = 0; + + let config = Config::read("config.toml").unwrap(); + + let (mut data, base_addr) = read_example("bootloader"); + for i in 0..added_bytes { + data.push(0x42 + i); + } + assert_same(&data, base_addr, true, None, &config, CERTIFICATE_IDX); +} + +fn assert_same( + input_data: &[u8], + base_addr: u32, + is_bootloader: bool, + otp: Option, + config: &Config, + certificate_idx: usize, +) { + let output_dir = tempfile::tempdir().unwrap(); + + let input_path = output_dir.path().join("input.bin"); + std::fs::write(&input_path, input_data).unwrap(); + + let pure_out = output_dir.path().join("pure.bin"); + let nxp_out = output_dir.path().join("nxp.bin"); + + let cert_block = cert_block::generate("nxpimage", &config, certificate_idx).unwrap(); + let private_key_path = get_private_key(&config, certificate_idx); + mbi::generate_pure( + &input_path, + base_addr, + &pure_out, + is_bootloader, + otp, + cert_block, + private_key_path, + ) + .unwrap(); + + let cert_block_config = cert_block::generate_config(&config, certificate_idx, None as Option); + mbi::generate_nxp( + "nxpimage", + &input_path, + base_addr, + &nxp_out, + is_bootloader, + cert_block_config, + ) + .unwrap(); + + let pure = std::fs::read(&pure_out).unwrap(); + let nxp = std::fs::read(&nxp_out).unwrap(); + + if pure != nxp { + let evidence = output_dir.keep(); + panic!("Outputs differ, see {} for generated files.", evidence.display()); + } +} + +fn read_example(app_or_boot: &str) -> (Vec, u32) { + let path = format!("../examples/rt685s/target/thumbv8m.main-none-eabihf/release/example-{app_or_boot}",); + let input = match std::fs::read(&path) { + Ok(input) => input, + Err(e) => { + panic!( + "Could not load example binary at '{path}'!\n -> Go to example/{app_or_boot} and run cargo build --release.\nError: {e}", + ); + } + }; + let file = ElfFile32::parse(&input[..]) + .context("Could not parse ELF file") + .unwrap(); + objcopy::objcopy(&file).unwrap() +} diff --git a/build.rs b/build.rs deleted file mode 100644 index 29976d77..00000000 --- a/build.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; - -fn main() { - // TODO: add check back when adding support for other chipsets - /*if env::var("CARGO_FEATURE_IMXRT").is_ok()*/ - { - // Put corresponding linker script 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("link-imxrt.x")) - .unwrap() - .write_all(include_bytes!("link-imxrt.x")) - .unwrap(); - println!("cargo:rustc-link-search={}", out.display()); - println!("cargo:rerun-if-changed=link-imxrt.x"); - println!("cargo:rustc-link-arg=-Tlink-imxrt.x"); - - // Inject crate version into the .biv section. - File::create(out.join("biv.rs")) - .unwrap() - .write_all( - format!( - r##" -#[link_section = ".biv"] -#[used] -static BOOT_IMAGE_VERSION: u32 = 0x{:02x}{:02x}{:02x}00; -"##, - env!("CARGO_PKG_VERSION_MAJOR") - .parse::() - .expect("should have major version"), - env!("CARGO_PKG_VERSION_MINOR") - .parse::() - .expect("should have minor version"), - env!("CARGO_PKG_VERSION_PATCH") - .parse::() - .expect("should have patch version"), - ) - .as_bytes(), - ) - .unwrap(); - } -} diff --git a/deny.toml b/deny.toml index 5ec7f8a7..fdcd4ee8 100644 --- a/deny.toml +++ b/deny.toml @@ -74,7 +74,7 @@ ignore = [ #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, - { id = "RUSTSEC-2024-0370", reason = "proc-macro-error is unmaintained, no safe upgrade available, need upstream dependencies to migrate away from it." }, + { id = "RUSTSEC-2024-0436", reason = "paste is unmaintained, no safe upgrade available, need upstream dependencies to migrate away from it." }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -102,23 +102,7 @@ allow = [ confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list -exceptions = [ - # Each entry is the crate and version constraint, and its specific allow - # list - { allow = [ - "Zlib", - ], crate = "constmuck" }, # used for computing CRC32 checksum at compile time - # below are dependencies of constmuck - { allow = [ - "Zlib", - ], crate = "constmuck_internal" }, - { allow = [ - "Zlib", - ], crate = "const_panic" }, - { allow = [ - "Zlib", - ], crate = "typewit" }, -] +exceptions = [] # Some crates don't have (easily) machine readable licensing information, # adding a clarification entry for it allows you to manually specify the @@ -214,6 +198,11 @@ deny = [ skip = [ #"ansi_term@0.11.0", #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, + "embedded-hal@0.2.7", + "nb@0.1.3", + "winnow@0.6.24", + "itertools@0.11.0", + "bitfield@0.13.2", ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive @@ -242,7 +231,7 @@ allow-git = [] [sources.allow-org] # github.com organizations to allow git sources for -github = ["embassy-rs"] +github = ["OpenDevicePartnership"] # gitlab.com organizations to allow git sources for gitlab = [] # bitbucket.org organizations to allow git sources for diff --git a/examples/rt685s/.cargo/config.toml b/examples/rt685s/.cargo/config.toml new file mode 100644 index 00000000..fe78a9ca --- /dev/null +++ b/examples/rt685s/.cargo/config.toml @@ -0,0 +1,14 @@ +[target.thumbv8m.main-none-eabihf] +runner = [ + 'probe-rs', + 'run', + '--chip', + 'MIMXRT685SFVKB' +] + +[build] +target = "thumbv8m.main-none-eabihf" # Cortex-M33 + +[env] +RUST_LOG = "info" +DEFMT_LOG = "info" \ No newline at end of file diff --git a/examples/rt685s/.gitignore b/examples/rt685s/.gitignore new file mode 100644 index 00000000..0397b4fe --- /dev/null +++ b/examples/rt685s/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/target +/target_ci diff --git a/examples/rt685s/Cargo.toml b/examples/rt685s/Cargo.toml new file mode 100644 index 00000000..932fe428 --- /dev/null +++ b/examples/rt685s/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] +resolver = "3" +members = [ + "application", + "bootloader", + "bsp", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/OpenDevicePartnership/ec-slimloader" + +[workspace.lints.rust] +warnings = "deny" + +[workspace.dependencies] +embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt.git", default-features = false } + +partition-manager = { git = "https://github.com/OpenDevicePartnership/embedded-services.git", default-features = false } +embassy-sync = "0.7.2" +embassy-executor = "0.9.1" + +defmt = "1.0.1" +defmt-or-log = { version = "0.2.2", default-features = false } +defmt-rtt = "0.4.0" + +[profile.release] +lto = true # better optimizations +debug = 2 +opt-level = "s" diff --git a/examples/rt685s/application/.gitignore b/examples/rt685s/application/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/examples/rt685s/application/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/examples/rt685s/application/Cargo.toml b/examples/rt685s/application/Cargo.toml new file mode 100644 index 00000000..dc20841b --- /dev/null +++ b/examples/rt685s/application/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "example-application" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[features] +defmt = [ + "dep:defmt", + "dep:defmt-rtt", + "defmt-or-log/defmt", + "panic-probe/defmt", + "embassy-imxrt/defmt", + "embassy-time/defmt", + "embassy-time/defmt-timestamp-uptime", + "ec-slimloader-state/defmt", +] +log = ["defmt-or-log/log"] + +[dependencies] +example-bsp = { path = "../bsp", features = ["application"] } + +ec-slimloader-state = { path = "../../../libs/ec-slimloader-state" } +imxrt-rom = { path = "../../../libs/imxrt-rom" } + +cortex-m = { version = "0.7.7", features = [ + "inline-asm", + "critical-section-single-core", +] } +cortex-m-rt = { version = "0.7.5" } + +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true } +defmt-rtt = { workspace = true, optional = true } +panic-probe = { version = "*" } + +embassy-imxrt = { workspace = true, features = [ + "rt", + "time-driver-os-timer", + "time", + "mimxrt685s", + "unstable-pac", +] } + +embassy-executor = { workspace = true, features = [ + "arch-cortex-m", + "executor-thread", +] } + +embassy-time = { version = "0.5" } +embassy-futures = "0.1.2" +embassy-embedded-hal = "0.5.0" + +embassy-sync = { workspace = true } +partition-manager = { workspace = true, features = ["esa", "macros"] } + +embedded-storage = "0.3.1" +embedded-storage-async = "0.4.1" diff --git a/examples/rt685s/application/README.md b/examples/rt685s/application/README.md new file mode 100644 index 00000000..5c43d38b --- /dev/null +++ b/examples/rt685s/application/README.md @@ -0,0 +1,6 @@ +# RT685S test application +* Flash to both slots +* Application will detect which slot it is running from and show the appropriate color in the LED. +* Application will reboot every 10 seconds. +* Press User 1 to confirm the current image. +* Press User 2 to request (with Initial) the other image. diff --git a/examples/rt685s/application/build.rs b/examples/rt685s/application/build.rs new file mode 100644 index 00000000..a282fe3b --- /dev/null +++ b/examples/rt685s/application/build.rs @@ -0,0 +1,19 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + 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()); + println!("cargo:rustc-link-arg=-Tlink.x"); + println!("cargo:rustc-link-arg=--nmagic"); + println!("cargo:rerun-if-changed=memory.x"); + + #[cfg(feature = "defmt")] + println!("cargo:rustc-link-arg=-Tdefmt.x"); +} diff --git a/examples/rt685s/application/memory.x b/examples/rt685s/application/memory.x new file mode 100644 index 00000000..0dabf287 --- /dev/null +++ b/examples/rt685s/application/memory.x @@ -0,0 +1,12 @@ +MEMORY { + FLASH : ORIGIN = 0x10020000, LENGTH = 1M + RAM : ORIGIN = 0x30120000, LENGTH = 32K + ROM_TABLE (r) : ORIGIN = 0x1303F000, LENGTH = 64 +} + +SECTIONS { + .rom_table ORIGIN(ROM_TABLE) (NOLOAD): { + API_TABLE = .; + . += LENGTH(ROM_TABLE); + } > ROM_TABLE +} INSERT AFTER .uninit; \ No newline at end of file diff --git a/examples/rt685s/application/src/main.rs b/examples/rt685s/application/src/main.rs new file mode 100644 index 00000000..a2493cc7 --- /dev/null +++ b/examples/rt685s/application/src/main.rs @@ -0,0 +1,239 @@ +#![no_std] +#![no_main] + +use cortex_m_rt::exception; +#[cfg(feature = "defmt")] +use defmt_rtt as _; +use ec_slimloader_state::flash::FlashJournal; +use ec_slimloader_state::state::{Slot, State, Status}; +use embassy_embedded_hal::adapter::BlockingAsync; +use embassy_executor::Spawner; +use embassy_imxrt::flexspi::embedded_storage::FlexSpiNorStorage; +use embassy_imxrt::flexspi::nor_flash::FlexSpiNorFlash; +use embassy_imxrt::gpio::{self, DriveMode, DriveStrength, Level, Output, SlewRate}; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_time::{Duration, Instant, Timer}; +use example_bsp::application::{ExternalStorageConfig, ExternalStorageMap}; +use imxrt_rom::registers::field_sets::{BootCfg0, Rkth}; +use imxrt_rom::registers::{OtpFuses, ShadowRegisters}; +use partition_manager::PartitionManager; + +#[allow(dead_code)] +struct Leds<'a> { + pub red: Output<'a>, + pub blue: Output<'a>, + pub green: Output<'a>, +} + +const JOURNAL_BUFFER_SIZE: usize = 1024; +const FUSE_DELAY: Duration = Duration::from_secs(5); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + defmt_or_log::info!("Example application"); + + const SYSTEM_CORE_CLOCK_HZ: u32 = 500_000_000; + let p = embassy_imxrt::init(Default::default()); + + let ext_flash = match unsafe { FlexSpiNorFlash::with_probed_config(p.FLEXSPI, 2, 2) } { + Ok(ext_flash) => ext_flash, + Err(e) => defmt_or_log::panic!("Failed to initialize FlexSPI peripheral: {:?}", e), + }; + + let ext_flash = match unsafe { FlexSpiNorStorage::<2, 2, 4096>::new(ext_flash) } { + Ok(ext_flash) => ext_flash, + Err(e) => defmt_or_log::panic!("Failed to wrap FlexSPI flash in embedded_storage adaptor: {:?}", e), + }; + + let mut ext_flash_manager = PartitionManager::<_, NoopRawMutex>::new(BlockingAsync::new(ext_flash)); + + let ExternalStorageMap { bl_state, .. } = ext_flash_manager.map(ExternalStorageConfig::new()); + + let mut journal = match FlashJournal::new::<{ crate::JOURNAL_BUFFER_SIZE }>(bl_state).await { + Ok(journal) => journal, + Err(e) => defmt_or_log::panic!("Failed to initialize the flash state journal: {:?}", e), + }; + + let slot_a = defmt_or_log::unwrap!(Slot::try_from(0)); + let slot_b = defmt_or_log::unwrap!(Slot::try_from(1)); + + let state = match journal.get() { + Some(state) => { + defmt_or_log::info!("Read state {}", state); + *state + } + None => { + defmt_or_log::info!("Initial state loaded"); + State::new(Status::Confirmed, slot_a, slot_b) + } + }; + + let (slot, is_confirmed, is_backup) = match state.status() { + Status::Initial => { + defmt_or_log::warn!( + "Booted into 'Initial' state, which should not be possible if the bootloader is flashed" + ); + (state.target(), false, false) + } + Status::Attempting => (state.target(), false, false), + Status::Failed => (state.backup(), false, true), + Status::Confirmed => (state.target(), true, false), + }; + + let other_slot = if slot == slot_a { slot_b } else { slot_a }; + + let mut leds = Leds { + // Blue: blink number indicates active slot + blue: Output::new( + p.PIO0_26, + Level::Low, + DriveMode::PushPull, + DriveStrength::Normal, + SlewRate::Standard, + ), + // Red: is_backup (blinking) + red: Output::new( + p.PIO0_31, + Level::Low, + DriveMode::PushPull, + DriveStrength::Normal, + SlewRate::Standard, + ), + // Green: is_confirmed + green: Output::new( + p.PIO0_14, + is_confirmed.into(), + DriveMode::PushPull, + DriveStrength::Normal, + SlewRate::Standard, + ), + }; + + // Maps to user1 and user2 buttons on EVK. + let mut button1 = gpio::Input::new(p.PIO1_1, gpio::Pull::None, gpio::Inverter::Disabled); + let mut button2 = gpio::Input::new(p.PIO0_10, gpio::Pull::None, gpio::Inverter::Disabled); + + // Task to repeatedly blink the blue LED, once for Slot(0) and twice for Slot(1). + let led_fut = async { + let slot = u8::from(slot) + 1; + loop { + for _ in 0..slot { + leds.blue.set_high(); + Timer::after_millis(200).await; + leds.blue.set_low(); + Timer::after_millis(200).await; + } + + Timer::after_millis(500).await; + } + }; + + // Task to blink the red LED if we are currently booted as the 'backup'. + let backup_led_fut = async { + if !is_backup { + return; + } + loop { + leds.red.toggle(); + Timer::after_millis(250).await; + } + }; + + // Task to handle writing the state if we want to either attempt the other slot, + // or want to confirm the current slot. + let button1_fut = async move { + // Potential new state used, but only if USER1 is pressed for a short period. + let new_state = if is_confirmed { + // Swap around + State::new(Status::Initial, other_slot, slot) + } else if is_backup { + // Try main again + state.with_status(Status::Initial) + } else { + // We were attempting so confirm + state.with_status(Status::Confirmed) + }; + + loop { + button1.wait_for_falling_edge().await; + let start = Instant::now(); + button1.wait_for_rising_edge().await; + defmt_or_log::info!("USER1"); + + if start.elapsed() > FUSE_DELAY { + let mut otp = imxrt_rom::otp::Otp::init(SYSTEM_CORE_CLOCK_HZ); + let mut fuses = OtpFuses::writable(&mut otp, false); + let mut shadow = ShadowRegisters::new(); + + { + let rkth_shadow = defmt_or_log::unwrap!(shadow.rkth().read()); + let rkth_otp = defmt_or_log::unwrap!(fuses.rkth().read()); + + if rkth_otp != rkth_shadow { + if rkth_shadow == Rkth::new_zero() { + defmt_or_log::error!("Requesting write of fuses, but RKTH is not set to something useful"); + } else { + let rkth_shadow_arr: [u8; 32] = rkth_shadow.into(); + defmt_or_log::info!("Writing RKTH fuses {:x}", rkth_shadow_arr); + + defmt_or_log::unwrap!(fuses.rkth().write(|w| *w = rkth_shadow)); + } + } + } + { + let boot0_otp = defmt_or_log::unwrap!(fuses.boot_cfg_0().read()); + if boot0_otp != BootCfg0::new_zero() { + defmt_or_log::error!("Requesting write of fuses, but Boot0 seems to already be set"); + } else { + defmt_or_log::info!("Writing boot0 fuse"); + + defmt_or_log::unwrap!(fuses.boot_cfg_0().write(|w| { + w.set_primary_boot_src(imxrt_rom::registers::BootSrc::QspiBBoot); + w.set_default_isp_mode(imxrt_rom::registers::DefaultIspMode::DisableIsp); + w.set_tzm_image_type(imxrt_rom::registers::TzmImageType::TzmEnable); + w.set_secure_boot_en(imxrt_rom::registers::SecureBoot::Enabled); + w.set_dice_skip(true); + w.set_boot_fail_pin_port(5); + w.set_boot_fail_pin_num(7); + })); + } + } + } else { + defmt_or_log::info!("Writing new state: {}", new_state); + defmt_or_log::unwrap!(journal.set::(&new_state).await); + } + } + }; + + // Task to reboot. + let button2_fut = async { + button2.wait_for_falling_edge().await; + defmt_or_log::info!("USER2"); + + Timer::after_millis(100).await; // Await for defmt. + cortex_m::peripheral::SCB::sys_reset() + }; + + embassy_futures::join::join4(led_fut, button1_fut, button2_fut, backup_led_fut).await; +} + +#[panic_handler] +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + core::hint::black_box(&info); + loop { + cortex_m::asm::wfe(); + } +} + +#[exception] +unsafe fn HardFault(frame: &cortex_m_rt::ExceptionFrame) -> ! { + let p = cortex_m::Peripherals::steal(); + let csfr = p.SCB.cfsr.read(); + let hfsr = p.SCB.hfsr.read(); + core::hint::black_box(&frame); + core::hint::black_box(&csfr); + core::hint::black_box(&hfsr); + loop { + cortex_m::asm::wfe(); + } +} diff --git a/examples/rt685s/bootloader/Cargo.toml b/examples/rt685s/bootloader/Cargo.toml new file mode 100644 index 00000000..123bf7c3 --- /dev/null +++ b/examples/rt685s/bootloader/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "example-bootloader" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[features] +defmt = [ + "dep:defmt", + "dep:defmt-rtt", + "defmt-or-log/defmt", + "ec-slimloader/defmt", + "ec-slimloader-imxrt/defmt", + "embassy-imxrt/defmt", + "embassy-executor/defmt", + "partition-manager/defmt", +] + +non-secure = ["ec-slimloader-imxrt/non-secure"] + +[dependencies] +ec-slimloader = { path = "../../../libs/ec-slimloader", default-features = false } +ec-slimloader-imxrt = { path = "../../../libs/ec-slimloader-imxrt", features = [ + "mimxrt685s-evk", +], default-features = false } + +example-bsp = { path = "../bsp", features = ["bootloader"] } + +heapless = "0.8.0" + +cortex-m = { version = "0.7.7", features = [ + "inline-asm", + "critical-section-single-core", +] } +cortex-m-rt = { version = "0.7.3" } + +embassy-imxrt = { workspace = true, features = ["mimxrt685s", "unstable-pac"] } + +embassy-executor = { workspace = true, features = [ + "arch-cortex-m", + "executor-thread", +] } + +embassy-sync = { workspace = true } +partition-manager = { workspace = true, features = ["esa", "macros"] } + +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true } +defmt-rtt = { workspace = true, optional = true } + +panic-probe = "*" diff --git a/examples/rt685s/bootloader/build.rs b/examples/rt685s/bootloader/build.rs new file mode 100644 index 00000000..9efb58b3 --- /dev/null +++ b/examples/rt685s/bootloader/build.rs @@ -0,0 +1,46 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put corresponding linker script 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()); + println!("cargo:rustc-link-arg=-Tlink.x"); + println!("cargo:rustc-link-arg=--nmagic"); + + #[cfg(feature = "defmt")] + println!("cargo:rustc-link-arg=-Tdefmt.x"); + + println!("cargo:rerun-if-changed=memory.x"); + + // Inject crate version into the .biv section. + File::create(out.join("biv.rs")) + .unwrap() + .write_all( + format!( + r##" +#[link_section = ".biv"] +#[used] +static BOOT_IMAGE_VERSION: u32 = 0x{:02x}{:02x}{:02x}00; +"##, + env!("CARGO_PKG_VERSION_MAJOR") + .parse::() + .expect("should have major version"), + env!("CARGO_PKG_VERSION_MINOR") + .parse::() + .expect("should have minor version"), + env!("CARGO_PKG_VERSION_PATCH") + .parse::() + .expect("should have patch version"), + ) + .as_bytes(), + ) + .unwrap(); +} diff --git a/examples/rt685s/bootloader/memory.x b/examples/rt685s/bootloader/memory.x new file mode 100644 index 00000000..c30eec0c --- /dev/null +++ b/examples/rt685s/bootloader/memory.x @@ -0,0 +1,34 @@ +MEMORY { + PRELUDE_OTFAD : ORIGIN = 0x08000000, LENGTH = 256 + PRELUDE_FCB : ORIGIN = 0x08000400, LENGTH = 512 + PRELUDE_BIV : ORIGIN = 0x08000600, LENGTH = 4 + + RAM : ORIGIN = 0x30176000, LENGTH = 20K + FLASH : ORIGIN = 0x10170000, LENGTH = 24K /* running in xip mode, in RAM */ + ROM_TABLE (r) : ORIGIN = 0x1303F000, LENGTH = 64 +} + +SECTIONS { + .otfad : { + . = ALIGN(4); + KEEP(* (.otfad)) + . = ALIGN(4); + } > PRELUDE_OTFAD + + .fcb : { + . = ALIGN(4); + KEEP(* (.fcb)) + . = ALIGN(4); + } > PRELUDE_FCB + + .biv : { + . = ALIGN(4); + KEEP(* (.biv)) + . = ALIGN(4); + } > PRELUDE_BIV + + .rom_table ORIGIN(ROM_TABLE) (NOLOAD): { + API_TABLE = .; + . += LENGTH(ROM_TABLE); + } > ROM_TABLE +} INSERT AFTER .uninit; diff --git a/examples/rt685s/bootloader/src/main.rs b/examples/rt685s/bootloader/src/main.rs new file mode 100644 index 00000000..e60d4f35 --- /dev/null +++ b/examples/rt685s/bootloader/src/main.rs @@ -0,0 +1,51 @@ +#![no_std] +#![no_main] + +#[cfg(feature = "defmt")] +use defmt_rtt as _; +use ec_slimloader_imxrt::{ExternalStorage, Partitions}; +use embassy_executor::Spawner; +use example_bsp::bootloader::{ExternalStorageConfig, ExternalStorageMap}; +use heapless::Vec; +use panic_probe as _; + +// auto-generated version information from Cargo.toml +include!(concat!(env!("OUT_DIR"), "/biv.rs")); + +struct Config; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct TooManySlots; + +const JOURNAL_BUFFER_SIZE: usize = 4096; + +impl ec_slimloader_imxrt::ImxrtConfig for Config { + const SLOT_SIZE_RANGE: core::ops::Range = 64..1024 * 1024; + const LOAD_RANGE: core::ops::Range<*mut u32> = (0x1002_0000 as *mut u32)..0x1018_0000 as *mut u32; + + fn partitions( + &self, + flash: &'static mut partition_manager::PartitionManager< + ExternalStorage, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >, + ) -> Partitions { + let ExternalStorageMap { + app_slot0, + app_slot1, + bl_state, + } = flash.map(ExternalStorageConfig::new()); + + let mut slots = Vec::new(); + defmt_or_log::unwrap!(slots.push(app_slot0).map_err(|_| TooManySlots)); + defmt_or_log::unwrap!(slots.push(app_slot1).map_err(|_| TooManySlots)); + + Partitions { state: bl_state, slots } + } +} + +#[embassy_executor::main] +async fn main(_spawner: Spawner) -> ! { + ec_slimloader::start::, JOURNAL_BUFFER_SIZE>(Config).await +} diff --git a/examples/rt685s/bsp/Cargo.toml b/examples/rt685s/bsp/Cargo.toml new file mode 100644 index 00000000..2a33d7c9 --- /dev/null +++ b/examples/rt685s/bsp/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-bsp" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +partition-manager = { workspace = true, features = ["esa", "macros"] } +embassy-sync = { workspace = true } + +[features] +application = [] +bootloader = [] diff --git a/examples/rt685s/bsp/src/ext-flash.toml b/examples/rt685s/bsp/src/ext-flash.toml new file mode 100644 index 00000000..6c64f07e --- /dev/null +++ b/examples/rt685s/bsp/src/ext-flash.toml @@ -0,0 +1,16 @@ +variants = ["bootloader", "application"] + +[disk] +size = 0x800000 +alignment = 0x1000 + +[partitions] +# prelude = { offset = 0x0, size = 0x1000 } +# bl_code = { offset = 0x1000, size = 0x8000 } +# bl_descriptors = { offset = 0x9000, size = 0x1000 } +# bl_mbi_suffix = { offset = 0xa000, size = 0x1000 } + +bl_state = { offset = 0xb000, size = 0x2000, access = { application = "rw", bootloader = "rw" } } + +app_slot0 = { offset = 0x0d000, size = 0xec000, access = { application = "rw", bootloader = "ro" } } +app_slot1 = { offset = 0xf9000, size = 0xec000, access = { application = "rw", bootloader = "ro" } } diff --git a/examples/rt685s/bsp/src/lib.rs b/examples/rt685s/bsp/src/lib.rs new file mode 100644 index 00000000..e81225e0 --- /dev/null +++ b/examples/rt685s/bsp/src/lib.rs @@ -0,0 +1,21 @@ +#![no_std] + +#[cfg(feature = "bootloader")] +pub mod bootloader { + partition_manager::macros::create_partition_map!( + name: ExternalStorageConfig, + map_name: ExternalStorageMap, + variant: "bootloader", + manifest: "src/ext-flash.toml" + ); +} + +#[cfg(feature = "application")] +pub mod application { + partition_manager::macros::create_partition_map!( + name: ExternalStorageConfig, + map_name: ExternalStorageMap, + variant: "application", + manifest: "src/ext-flash.toml" + ); +} diff --git a/examples/rt685s/ci.sh b/examples/rt685s/ci.sh new file mode 100755 index 00000000..a8948cbd --- /dev/null +++ b/examples/rt685s/ci.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -eo pipefail + +if ! command -v cargo-batch &> /dev/null; then + echo "cargo-batch could not be found. Install it with the following command:" + echo "" + echo " cargo install --git https://github.com/embassy-rs/cargo-batch cargo --bin cargo-batch --locked" + echo "" + exit 1 +fi + +export RUSTFLAGS=-Dwarnings +export DEFMT_LOG=trace +if [[ -z "${CARGO_TARGET_DIR}" ]]; then + export CARGO_TARGET_DIR=target_ci +fi + +TARGET="thumbv8m.main-none-eabihf" + +BUILD_EXTRA="" + +FEATURE_COMBINATIONS=( + "defmt" + "non-secure" +) +cargo batch \ + $(for features in "${FEATURE_COMBINATIONS[@]}"; do + echo "--- build --release --manifest-path Cargo.toml --target thumbv8m.main-none-eabihf " + echo "--- build --release --manifest-path Cargo.toml --target thumbv8m.main-none-eabihf --features $features " + done) $BUILD_EXTRA diff --git a/libs/.cargo/config.toml b/libs/.cargo/config.toml new file mode 100644 index 00000000..55dd5ea5 --- /dev/null +++ b/libs/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "thumbv8m.main-none-eabihf" # Cortex-M33 diff --git a/libs/.gitignore b/libs/.gitignore new file mode 100644 index 00000000..6974cc1e --- /dev/null +++ b/libs/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/target +/target_ci \ No newline at end of file diff --git a/libs/Cargo.toml b/libs/Cargo.toml new file mode 100644 index 00000000..350df9de --- /dev/null +++ b/libs/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +resolver = "3" +members = [ + "ec-slimloader", + "ec-slimloader-imxrt", + "ec-slimloader-state", + "imxrt-rom", +] + +[workspace.package] +version = "0.2.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/OpenDevicePartnership/ec-slimloader" + +[workspace.lints.clippy] +manual_let_else = "deny" + +[workspace.dependencies] +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" +log = "0.4" + +cortex-m = { version = "0.7.7" } diff --git a/libs/ci.sh b/libs/ci.sh new file mode 100755 index 00000000..86b429df --- /dev/null +++ b/libs/ci.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -eo pipefail + +if ! command -v cargo-batch &> /dev/null; then + echo "cargo-batch could not be found. Install it with the following command:" + echo "" + echo " cargo install --git https://github.com/embassy-rs/cargo-batch cargo --bin cargo-batch --locked" + echo "" + exit 1 +fi + +export RUSTFLAGS=-Dwarnings +export DEFMT_LOG=trace +if [[ -z "${CARGO_TARGET_DIR}" ]]; then + export CARGO_TARGET_DIR=target_ci +fi + +TARGET="thumbv8m.main-none-eabihf" + +BUILD_EXTRA="" + +FEATURE_COMBINATIONS=( + "mimxrt633s" + "mimxrt633s,defmt" + "mimxrt633s,non-secure" + "mimxrt685s" + "mimxrt685s,defmt" + "mimxrt685s,non-secure" +) +cargo batch \ + $(for features in "${FEATURE_COMBINATIONS[@]}"; do + echo "--- build --release --manifest-path Cargo.toml --target thumbv8m.main-none-eabihf --features $features " + done) $BUILD_EXTRA + +cargo test --locked --manifest-path Cargo.toml --target x86_64-unknown-linux-gnu --features "mimxrt633s" +cargo test --locked --manifest-path Cargo.toml --target x86_64-unknown-linux-gnu --features "mimxrt685s" diff --git a/.gitignore b/libs/ec-slimloader-imxrt/.gitignore similarity index 100% rename from .gitignore rename to libs/ec-slimloader-imxrt/.gitignore diff --git a/libs/ec-slimloader-imxrt/Cargo.toml b/libs/ec-slimloader-imxrt/Cargo.toml new file mode 100644 index 00000000..0141fcd9 --- /dev/null +++ b/libs/ec-slimloader-imxrt/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "ec-slimloader-imxrt" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[features] +# Skips all security and verification checks +non-secure = [] + +# Special board support +mimxrt685s-evk = ["imxrt-fcb-rt685evk", "mimxrt685s"] + +# Optional empty OTFAD definition +empty-otfad = [] + +# FCB support +fcb = [] + +# FCB modes +imxrt-fcb-1spi-nor = ["fcb"] +imxrt-fcb-rt685evk = ["fcb"] + +# FCB banks +imxrt-fcb-1spi-a1-nor = ["imxrt-fcb-1spi-nor"] +imxrt-fcb-1spi-b1-nor = ["imxrt-fcb-1spi-nor"] + +# Chip variants +mimxrt685s = ["imxrt", "embassy-imxrt/mimxrt685s"] +mimxrt633s = ["imxrt", "embassy-imxrt/mimxrt633s"] + +# Common imxrt features +imxrt = ["dep:mimxrt600-fcb"] + +# Optional debug logging +defmt = [ + "dep:defmt", + "defmt-or-log/defmt", + "ec-slimloader/defmt", + "ec-slimloader-state/defmt", + "imxrt-rom/defmt", + "embassy-imxrt/defmt", + "partition-manager/defmt", +] +log = ["defmt-or-log/log", "imxrt-rom/log"] + +default = [] + +[dependencies] +ec-slimloader = { path = "../ec-slimloader" } +ec-slimloader-state = { path = "../ec-slimloader-state", default-features = false } +imxrt-rom = { path = "../imxrt-rom", features = ["rt"] } + +cortex-m = { workspace = true } +embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt.git", default-features = false, features = [ + "unstable-pac", +] } + +mimxrt600-fcb = { version = "0.2.2", optional = true } + +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true } + +embassy-embedded-hal = "0.5.0" + +embassy-sync = { workspace = true } +partition-manager = { git = "https://github.com/OpenDevicePartnership/embedded-services.git", features = [ + "esa", + "macros", +], default-features = false } +embedded-storage = "0.3.1" +embedded-storage-async = { workspace = true } + +static_cell = "2.1.1" +heapless = "0.8.0" diff --git a/libs/ec-slimloader-imxrt/src/bootload.rs b/libs/ec-slimloader-imxrt/src/bootload.rs new file mode 100644 index 00000000..0fd955c3 --- /dev/null +++ b/libs/ec-slimloader-imxrt/src/bootload.rs @@ -0,0 +1,49 @@ +use defmt_or_log::info; + +/// Boot an application from memory. +/// +/// It should follow the standard ARM Cortex M image format: +/// initial stack pointer, vector table, program data. +/// +/// # SAFETY +/// The loaded application must be a valid firmware image for the platform, +/// and it must not return control to the caller. +pub unsafe fn boot_application(boot_address: *const u32) -> ! { + unsafe { + // Disable interrupts globally while we reset the NVIC. + cortex_m::interrupt::disable(); + + let nvic = &*cortex_m::peripheral::NVIC::PTR; + + // Disable all configurable interrupts. + for clear_enable in &nvic.icer { + clear_enable.write(u32::MAX); + } + + // Clear all interrupt-pending bits. + for clear_pending in &nvic.icpr { + clear_pending.write(u32::MAX); + } + + // Reset all interrupt priorities. + for priority in &nvic.ipr { + priority.write(0); + } + + // Re-enable interrupts globally to match boot-up environment. + cortex_m::interrupt::enable(); + + info!("Invalidating SCB icache, overwriting vector table and jumping to boot address"); + + let mut p = cortex_m::Peripherals::steal(); + p.SCB.invalidate_icache(); + p.SCB.vtor.write(boot_address as u32); + + // Ensure that all previous steps have been executed. + cortex_m::asm::dmb(); + cortex_m::asm::dsb(); + cortex_m::asm::isb(); + + cortex_m::asm::bootload(boot_address) + } +} diff --git a/src/imxrt/fcb.rs b/libs/ec-slimloader-imxrt/src/fcb.rs similarity index 70% rename from src/imxrt/fcb.rs rename to libs/ec-slimloader-imxrt/src/fcb.rs index 59f75533..a1767cb9 100644 --- a/src/imxrt/fcb.rs +++ b/libs/ec-slimloader-imxrt/src/fcb.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use mimxrt600_fcb::FlexSpiLutOpcode::{CMD_SDR, READ_SDR, STOP}; use mimxrt600_fcb::FlexSpiNumPads::Single; use mimxrt600_fcb::{flexspi_lut_seq, FlexSPIFlashConfigurationBlock}; @@ -20,85 +22,7 @@ use mimxrt600_fcb::{ControllerMiscOption, SFlashPadType, SerialClkFreq, SerialNO #[cfg(feature = "imxrt-fcb-rt685evk")] #[link_section = ".fcb"] #[used] -static FCB_685EVK: FlexSPIFlashConfigurationBlock = FlexSPIFlashConfigurationBlock::build().lookup_table([ - // Read - flexspi_lut_seq(CMD_DDR, Octal, 0xee, CMD_DDR, Octal, 0x11), - flexspi_lut_seq(RADDR_DDR, Octal, 0x20, DUMMY_DDR, Octal, 0x29), - flexspi_lut_seq(READ_DDR, Octal, 0x04, STOP, Single, 0x00), - 0, - // Read status SPI - flexspi_lut_seq(CMD_SDR, Single, 0x05, READ_SDR, Single, 0x04), - 0, - 0, - 0, - // Read status OPI - flexspi_lut_seq(CMD_DDR, Octal, 0x05, CMD_DDR, Octal, 0xFA), - flexspi_lut_seq(RADDR_DDR, Octal, 0x20, DUMMY_DDR, Octal, 0x14), - flexspi_lut_seq(READ_DDR, Octal, 0x04, STOP, Single, 0x00), - 0, - // Write enable - flexspi_lut_seq(CMD_SDR, Single, 0x06, STOP, Single, 0x00), - 0, - 0, - 0, - // Write enable - OPI - flexspi_lut_seq(CMD_DDR, Octal, 0x06, CMD_DDR, Octal, 0xF9), - 0, - 0, - 0, - // Erase Sector - flexspi_lut_seq(CMD_DDR, Octal, 0x21, CMD_DDR, Octal, 0xDE), - flexspi_lut_seq(RADDR_DDR, Octal, 0x20, STOP, Single, 0x00), - 0, - 0, - // Enable OPI DDR mode - flexspi_lut_seq(CMD_SDR, Single, 0x72, CMD_SDR, Single, 0x00), - flexspi_lut_seq(CMD_SDR, Single, 0x00, CMD_SDR, Single, 0x00), - flexspi_lut_seq(CMD_SDR, Single, 0x00, WRITE_SDR, Single, 0x01), - 0, - // Unused - 0, - 0, - 0, - 0, - // Erase block - flexspi_lut_seq(CMD_DDR, Octal, 0xDC, CMD_DDR, Octal, 0x23), - flexspi_lut_seq(RADDR_DDR, Octal, 0x20, STOP, Single, 0x00), - 0, - 0, - // Page program - flexspi_lut_seq(CMD_DDR, Octal, 0x12, CMD_DDR, Octal, 0xED), - flexspi_lut_seq(RADDR_DDR, Octal, 0x20, WRITE_DDR, Octal, 0x04), - 0, - 0, - // Unused - 0, - 0, - 0, - 0, - // Erase chip - flexspi_lut_seq(CMD_DDR, Octal, 0x60, CMD_DDR, Octal, 0x9F), - 0, - 0, - 0, - // Remainder is unused - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, -]); +static FCB_685EVK: FlexSPIFlashConfigurationBlock = FlexSPIFlashConfigurationBlock::build(); #[cfg(feature = "imxrt-fcb-1spi-a1-nor")] #[link_section = ".fcb"] diff --git a/libs/ec-slimloader-imxrt/src/lib.rs b/libs/ec-slimloader-imxrt/src/lib.rs new file mode 100644 index 00000000..7bb97f26 --- /dev/null +++ b/libs/ec-slimloader-imxrt/src/lib.rs @@ -0,0 +1,207 @@ +#![no_std] + +#[cfg(feature = "fcb")] +mod fcb; + +#[cfg(not(feature = "non-secure"))] +mod verification; + +#[cfg(feature = "empty-otfad")] +#[link_section = ".otfad"] +#[used] +static OTFAD: [u8; 256] = [0x00; 256]; + +mod bootload; +mod mbi; + +use core::ops::Range; + +use defmt_or_log::{error, info, panic}; +use ec_slimloader::{Board, BootError}; +use ec_slimloader_state::flash::FlashJournal; +use ec_slimloader_state::state::Slot; +use embassy_embedded_hal::adapter::BlockingAsync; +use embassy_imxrt::clocks::MainClkSrc; +use embassy_imxrt::flexspi::embedded_storage::FlexSpiNorStorage; +use embassy_imxrt::flexspi::nor_flash::FlexSpiNorFlash; +use embassy_imxrt::peripherals::HASHCRYPT; +use embassy_imxrt::Peri; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embedded_storage_async::nor_flash::{NorFlash, ReadNorFlash}; +use heapless::Vec; +use partition_manager::{Partition, PartitionManager, RO, RW}; +use static_cell::StaticCell; + +use crate::mbi::Ivt; + +const IMAGE_TYPE_TZ_XIP_SIGNED: u32 = 0x0004; +const READ_ALIGNMENT: u32 = 2; +const WRITE_ALIGNMENT: u32 = 2; +const ERASE_SIZE: u32 = 4096; +const MAX_SLOT_COUNT: usize = 7; + +pub type ExternalStorage = BlockingAsync>; + +pub struct Partitions { + pub state: Partition<'static, ExternalStorage, RW, NoopRawMutex>, + pub slots: Vec, MAX_SLOT_COUNT>, +} + +pub trait ImxrtConfig { + /// Minimum and maximum image size contained within a slot. + const SLOT_SIZE_RANGE: Range; + + /// The memory range an image is allowed to be copied to. + const LOAD_RANGE: Range<*mut u32>; + + fn partitions(&self, flash: &'static mut PartitionManager) -> Partitions; +} + +#[allow(dead_code)] +pub struct Imxrt { + journal: FlashJournal>, + slots: Vec, MAX_SLOT_COUNT>, + hashcrypt: Peri<'static, HASHCRYPT>, + _config: C, +} + +trait CheckImage { + fn check_image(&mut self, _ram_ivt: &Ivt) -> Result<(), BootError>; +} + +#[cfg(feature = "non-secure")] +impl CheckImage for Imxrt { + fn check_image(&mut self, _ram_ivt: &Ivt) -> Result<(), BootError> { + defmt_or_log::warn!("Skipped authentication because non-secure mode is set"); + Ok(()) + } +} + +impl Board for Imxrt { + type Config = C; + + async fn init(config: Self::Config) -> Self { + // Set clock to Pll but with a larger divider, otherwise + // we get nondeterministic behaviour from the ROM API. + let mut hal_config = embassy_imxrt::config::Config::default(); + hal_config.clocks.main_clk.src = MainClkSrc::PllMain; + hal_config.clocks.main_clk.div_int = 4.into(); + hal_config.clocks.main_pll_clk.pfd0 = 20; + let p = embassy_imxrt::init(hal_config); + + let ext_flash = match unsafe { FlexSpiNorFlash::with_probed_config(p.FLEXSPI, READ_ALIGNMENT, WRITE_ALIGNMENT) } + { + Ok(ext_flash) => ext_flash, + Err(e) => panic!("Failed to initialize FlexSPI peripheral: {:?}", e), + }; + + let ext_flash = + match unsafe { FlexSpiNorStorage::::new(ext_flash) } { + Ok(ext_flash) => ext_flash, + Err(e) => panic!("Failed to wrap FlexSPI flash in embedded_storage adaptor: {:?}", e), + }; + + static EXT_FLASH: StaticCell> = StaticCell::new(); + let ext_flash_manager = + EXT_FLASH.init_with(|| PartitionManager::<_, NoopRawMutex>::new(BlockingAsync::new(ext_flash))); + + let Partitions { state, slots } = config.partitions(ext_flash_manager); + + let journal = match FlashJournal::new::(state).await { + Ok(journal) => journal, + Err(e) => panic!("Failed to initialize the flash state journal: {:?}", e), + }; + + Self { + journal, + slots, + hashcrypt: p.HASHCRYPT, + _config: config, + } + } + + fn journal(&mut self) -> &mut FlashJournal { + &mut self.journal + } + + 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; + }; + + // Copy the image to RAM from flash, and ensure that everything from flash is no longer available. + let ram_ivt = { + let slot_size = slot_partition.capacity(); + + // Check if the image_len fits within the slot. + if slot_size >= C::SLOT_SIZE_RANGE.end { + return BootError::TooLarge; + } + + // Verify IVT fields. + let Ok(ivt) = mbi::Ivt::read(slot_partition).await else { + return BootError::IO; + }; + + // Note: skboot_authenticate only supports checking XIP_SIGNED, even though we are loading it to RAM here. + if ivt.image_type != IMAGE_TYPE_TZ_XIP_SIGNED { + return BootError::Markers; + } + if ivt.image_len > slot_size { + return BootError::TooLarge; + } + if ivt.image_len < C::SLOT_SIZE_RANGE.start { + return BootError::TooSmall; + } + + // Check if the target_ptr is within the allowed range. + // In MBI this is called the 'load_addr', which is located in 0x34 of IVT. + let Some(image_target_end_ptr) = ivt.target_end_ptr() else { + return BootError::TooLarge; + }; + + if !C::LOAD_RANGE.contains(&ivt.target_ptr) || !C::LOAD_RANGE.contains(&image_target_end_ptr) { + return BootError::MemoryRegion; + } + + info!("Starting copy"); + let target_slice = unsafe { core::slice::from_raw_parts_mut(ivt.target_ptr as *mut u8, ivt.image_len) }; + if let Err(_e) = slot_partition.read(0, target_slice).await { + return BootError::IO; + } + + // Invalidate icache as we are writing to Code RAM, which is cached. + unsafe { + let mut p = cortex_m::Peripherals::steal(); + p.SCB.invalidate_icache(); + } + info!("Copy done"); + + let Ok(ram_ivt) = mbi::Ivt::read_from_slice(target_slice) else { + return BootError::TooSmall; + }; + + if ivt != ram_ivt { + return BootError::ChangeAfterRead; + } + + ram_ivt + }; + + if let Err(e) = self.check_image(&ram_ivt) { + error!("Failed to boot image @ {}", slot); + return e; + } + + info!("Booting into application @ {:x}...", ram_ivt.target_ptr); + + // Boot to application, and we do not return from this function. + unsafe { bootload::boot_application(ram_ivt.target_ptr) } + } + + fn abort(&mut self) -> ! { + loop { + cortex_m::asm::wfi(); + } + } +} diff --git a/libs/ec-slimloader-imxrt/src/mbi.rs b/libs/ec-slimloader-imxrt/src/mbi.rs new file mode 100644 index 00000000..e3d85ca8 --- /dev/null +++ b/libs/ec-slimloader-imxrt/src/mbi.rs @@ -0,0 +1,66 @@ +#![allow(dead_code)] + +use embedded_storage_async::nor_flash::ReadNorFlash; + +#[derive(Debug, PartialEq)] +pub struct Ivt { + pub image_len: usize, + pub image_type: u32, + pub header_offset: u32, + pub target_ptr: *mut u32, +} + +pub struct BufferTooSmall; + +impl Ivt { + pub async fn read(slot: &mut F) -> Result { + let mut buf = [0u8; 64]; + slot.read(0, &mut buf).await?; + + // Note(unsafe): our buffer is 64 bytes large. + Ok(unsafe { Self::read_from_slice(&buf).unwrap_unchecked() }) + } + + pub fn read_from_slice(data: &[u8]) -> Result { + if data.len() < 64 { + return Err(BufferTooSmall); + } + + // Note(unsafe): we are taking byte slices 4 bytes long, so they should map perfectly to 4 byte arrays. + Ok(Self { + image_len: u32::from_le_bytes(unsafe { data[0x20..0x24].try_into().unwrap_unchecked() }) as usize, + image_type: u32::from_le_bytes(unsafe { data[0x24..0x28].try_into().unwrap_unchecked() }), + header_offset: u32::from_le_bytes(unsafe { data[0x28..0x2C].try_into().unwrap_unchecked() }), + target_ptr: u32::from_le_bytes(unsafe { data[0x34..0x38].try_into().unwrap_unchecked() }) as *mut u32, + }) + } + + pub fn target_end_ptr(&self) -> Option<*mut u32> { + (self.target_ptr as usize) + .checked_add(self.image_len) + .map(|ptr| ptr as *mut u32) + } +} + +#[repr(C)] +pub struct CertificateBlockHeader { + pub signature: u32, + pub header_major_version: u16, + pub header_minor_version: u16, + pub header_length: u32, + pub flags: u32, + pub build_number: u32, + pub total_image_length: u32, + pub certificate_count: u32, + pub certificate_table_length: u32, +} + +impl CertificateBlockHeader { + pub fn read_from_slice(data: &[u8]) -> Option { + if data.len() < core::mem::size_of::() { + return None; + } + + Some(unsafe { (data.as_ptr() as *const CertificateBlockHeader).read_unaligned() }) + } +} diff --git a/libs/ec-slimloader-imxrt/src/verification.rs b/libs/ec-slimloader-imxrt/src/verification.rs new file mode 100644 index 00000000..71ee9805 --- /dev/null +++ b/libs/ec-slimloader-imxrt/src/verification.rs @@ -0,0 +1,153 @@ +use defmt_or_log::{error, info, unwrap, warn}; +use ec_slimloader::BootError; +use embassy_imxrt::hashcrypt::Hashcrypt; +use embassy_imxrt::peripherals::HASHCRYPT; +use embassy_imxrt::Peri; +use imxrt_rom::otp::Otp; +use imxrt_rom::registers::field_sets::Rkth; +use imxrt_rom::registers::{OtpFuses, SecureBoot, ShadowRegisters}; + +use crate::mbi::Ivt; +use crate::{CheckImage, Imxrt, ImxrtConfig}; + +// TODO determine clock frequency from HAL. +const SYSTEM_CORE_CLOCK_HZ: u32 = (5 * 1000 * 1000) / 2; + +/// A Root Key Hash as lives in the Certificate Block at the end. +#[derive(PartialEq, Debug)] +#[repr(C)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Rkh(pub [u8; 32]); + +impl Rkh { + pub fn to_rkth(rkhs: &[Rkh; 4], hashcrypt: Peri) -> Rkth { + // Safety: Rkh's will be at least as aligned as u8's. + let rkhs = unsafe { + core::slice::from_raw_parts(rkhs.as_ptr() as *const u8, rkhs.len() * core::mem::size_of::()) + }; + let mut hashcrypt = Hashcrypt::new_blocking(hashcrypt); + + let mut result = [0u8; 32]; + hashcrypt.new_sha256().hash(rkhs, &mut result); + Rkth::from(result) + } + + pub fn read_all_from_slice(data: &[u8]) -> Option<[Rkh; 4]> { + if data.len() < core::mem::size_of::<[Rkh; 4]>() { + return None; + } + + Some(unsafe { (data.as_ptr() as *const [Rkh; 4]).read_unaligned() }) + } +} + +fn rkth(rkth: Rkth) -> [u8; 32] { + rkth.into() +} + +impl CheckImage for Imxrt { + fn check_image(&mut self, ram_ivt: &Ivt) -> Result<(), BootError> { + // Compute RKTH from image. + let image_rkth = { + // Safety: whilst we do not know if the image is valid by itself, + // this slice at least is what we just copied. (should be identical to target_slice) + + use crate::mbi::CertificateBlockHeader; + let ram_image_slice = + unsafe { core::slice::from_raw_parts(ram_ivt.target_ptr as *const u8, ram_ivt.image_len) }; + let cert_block_header_offset = ram_ivt.header_offset as usize; + + // Fetch certificate block + let Some(cert_block_header) = + CertificateBlockHeader::read_from_slice(&ram_image_slice[cert_block_header_offset..]) + else { + return Err(BootError::TooLarge); + }; + + if cert_block_header.header_length != 0x20 { + warn!("Certificate block header is not expected length"); + } + + let rkhs_offset = cert_block_header_offset + + cert_block_header.header_length as usize + + cert_block_header.certificate_table_length as usize; + + let Some(rkhs) = Rkh::read_all_from_slice(&ram_image_slice[rkhs_offset..]) else { + return Err(BootError::TooLarge); + }; + + Rkh::to_rkth(&rkhs, self.hashcrypt.reborrow()) + }; + + info!("RKTH (image) {:x}", rkth(image_rkth)); + + let mut shadow = ShadowRegisters::new(); + + { + info!("Boot0 (shadow) {}", unwrap!(shadow.boot_cfg_0().read())); + info!("Boot1 (shadow) {}", unwrap!(shadow.boot_cfg_1().read())); + info!("RKTH (shadow) {:x}", rkth(unwrap!(shadow.rkth().read()))); + } + + // Reload shadow registers. + { + let mut otp = Otp::init(SYSTEM_CORE_CLOCK_HZ); + { + let mut fuses = OtpFuses::readonly(&mut otp); + info!("Boot0 (fuse): {}", unwrap!(fuses.boot_cfg_0().read())); + info!("Boot1 (fuse): {}", unwrap!(fuses.boot_cfg_1().read())); + info!("RKTH (fuse): {:x}", rkth(unwrap!(fuses.rkth().read()))); + } + unwrap!(otp.reload_shadow()); + info!("Shadow registers reloaded from fuses"); + } + + // Fix for EVK without fuses. + #[cfg(feature = "mimxrt685s-evk")] + { + // Configure the EVK NOR flash @ port 2, pin 12 to be reset on a system reset. + unwrap!(shadow.boot_cfg_1().modify(|w| { + w.set_qspi_reset_pin_enable(true); + w.set_qspi_reset_pin_port(2); + w.set_qspi_reset_pin_num(12); + })); + } + + { + info!("Boot0 (shadow reloaded) {}", unwrap!(shadow.boot_cfg_0().read())); + info!("Boot1 (shadow reloaded) {}", unwrap!(shadow.boot_cfg_1().read())); + info!("RKTH (shadow reloaded) {:x}", rkth(unwrap!(shadow.rkth().read()))); + } + + // Whether the hardware is in 'development mode' is dependent on the secure_boot_en bit being asserted. + let dev_mode = unwrap!(shadow.boot_cfg_0().read()).secure_boot_en() == SecureBoot::Disabled; + + if image_rkth != unwrap!(shadow.rkth().read()) { + if dev_mode { + // If no SECURE_BOOT fuse set => overwrite shadow RKTH with image RKTH + warn!("Development mode detected, using new image RKTH {:x}", rkth(image_rkth)); + unwrap!(shadow.rkth().write(|w| *w = image_rkth)); + } else { + // If SECURE_BOOT fuse set => do nothing as skboot_authenticate should be annoyed (perhaps assert afterwards) + error!("Shadow and image RKTH do not concur, but we call skboot_authenticate in any case"); + } + } else { + info!("Shadow and image RKTH concur!") + } + + info!("Starting authenticate"); + // Call the ROM API to ensure that the image is signed and not broken or tampered with. + // Note: skboot_authenticate will show false-negatives if your clock jitter is too high. + // We noticed this with FFROdiv2 and MainClk > 475MHz. + match imxrt_rom::skboot::skboot_authenticate(ram_ivt.target_ptr, ram_ivt.image_len as u32, None) { + Ok(()) => { + info!("Authenticate succeeded!"); + Ok(()) + } + Err(e) => { + warn!("Failed to authenticate {:?}", e); + Err(BootError::Authenticate) + } + } + } +} diff --git a/libs/ec-slimloader-state/.gitignore b/libs/ec-slimloader-state/.gitignore new file mode 100644 index 00000000..196e176d --- /dev/null +++ b/libs/ec-slimloader-state/.gitignore @@ -0,0 +1,19 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + + +# Added by cargo + +/target diff --git a/libs/ec-slimloader-state/Cargo.toml b/libs/ec-slimloader-state/Cargo.toml new file mode 100644 index 00000000..375ab9c6 --- /dev/null +++ b/libs/ec-slimloader-state/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ec-slimloader-state" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +crc = "3.2.1" +num_enum = { version = "0.7.4", default-features = false } + +embedded-storage-async = { workspace = true } +defmt = { workspace = true, optional = true } + +[dev-dependencies] +embassy-futures = "0.1.1" + +[features] +defmt = ["dep:defmt"] diff --git a/libs/ec-slimloader-state/src/flash.rs b/libs/ec-slimloader-state/src/flash.rs new file mode 100644 index 00000000..c22c6d65 --- /dev/null +++ b/libs/ec-slimloader-state/src/flash.rs @@ -0,0 +1,281 @@ +#[cfg(test)] +mod mock; + +use core::ops::Range; + +use embedded_storage_async::nor_flash::NorFlash; + +use crate::state::{ParseResult, State}; + +/// Error describing that the Nvm should have at least two partitions. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// A storage medium has been passed that does not contain at least two pages. + NotEnoughPartitions, + + /// After writing the state a readback does not yield the same state. + /// + /// This indicates that something went wrong in the writing process, either on the bus + /// or in the storage medium itself. + ReadbackFailed, + + /// The underlying storage medium yielded an error. + Other(E), +} + +impl From for Error { + fn from(value: E) -> Self { + Error::Other(value) + } +} + +#[derive(Debug)] +struct StateWithAddr { + /// Actual value of the [State]. + state: State, + /// Address of the [State]. + address: usize, +} + +#[derive(Default)] +struct Cache { + /// A copy of the last valid [State] on-disk. + last_valid_state: Option, + + /// Address of last empty slot for a [State], containing only 0xff bytes. + first_empty_slot: Option, +} + +/// Bootloader [State] journal backed by Non-Volatile Memory. +pub struct FlashJournal { + /// Inner flash storage. + inner: T, + /// A in-ram cache of the state on disk and where to write the next state to. + cache: Cache, +} + +impl FlashJournal { + const PAGE_SIZE: usize = T::ERASE_SIZE; + + /// Construct the FlashJournal given a storage device (or a partition). + /// + /// Will yield [Error::NotEnoughPartitions] if the partition is not contain at least 2 pages. + pub async fn new(mut inner: T) -> Result> { + if Self::page_count(&inner) < 2 { + return Err(Error::NotEnoughPartitions); + } + + let cache = Self::compute_cache::(&mut inner).await?; + Ok(Self { inner, cache }) + } + + /// Number of pages in the backing storage medium. + fn page_count(inner: &T) -> usize { + inner.capacity().div_ceil(Self::PAGE_SIZE) + } + + /// Convert a memory address in the inner NVM to a page index number. + fn address_to_page_i(address: u32) -> usize { + address as usize / Self::PAGE_SIZE + } + + /// Walk through the entire NVM range, and find the last valid [State] entry + /// and find the first empty slot of a [State] entry, if any. + async fn compute_cache(inner: &mut T) -> Result { + let mut buf = [0u8; N]; + let block_count = inner.capacity().div_ceil(N); + + let mut result = Cache::default(); + for block_i in 0..block_count { + let block_start = block_i * N; + let block_end = (block_start + N).min(inner.capacity()); + + let slice = &mut buf[0..block_end - block_start]; + inner.read(block_start as u32, slice).await?; + + const CHUNK_SIZE: usize = 2; + 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; + match State::try_new(chunk) { + Ok(state) => { + result = Cache { + last_valid_state: Some(StateWithAddr { state, address }), + first_empty_slot: None, // Reset if any. + }; + } + Err(ParseResult::Unset) => { + // If not found an empty entry yet, we can record this one as the first one free. + if result.first_empty_slot.is_none() { + result.first_empty_slot = Some(address); + } + } + Err(ParseResult::Invalid) => {} // Broken. + } + } + } + Ok(result) + } + + /// Get the latest [State] contained in the [FlashJournal], if any. + pub fn get(&self) -> Option<&State> { + self.cache + .last_valid_state + .as_ref() + .map(|StateWithAddr { state, address: _ }| state) + } + + /// Erase a range of pages as a single erase instruction to [NorFlash]. + async fn erase_pages(&mut self, page_range: Range) -> Result<(), T::Error> { + let start = page_range.start * Self::PAGE_SIZE; + let end = (page_range.end * Self::PAGE_SIZE).min(self.inner.capacity()); + self.inner.erase(start as u32, end as u32).await + } + + /// Synchronize the latest [State] to the [FlashJournal]. + pub async fn set(&mut self, state: &State) -> Result<(), Error> { + // Check if the current state is identical. + if self.get() == Some(state) { + return Ok(()); + } + + // Write the new state somewhere. + if let Some(first_empty_slot) = self.cache.first_empty_slot { + // If detected first empty slot, we can write to it as we are [NorFlash] and the empty slot is all `0xff``. + self.inner.write(first_empty_slot as u32, &state.as_bytes()).await?; + } else if let Some(last_valid_state) = &self.cache.last_valid_state { + // If detected no empty slot, we can assume that all pages have been written, or we are in a partially valid state. + + let page_i = Self::address_to_page_i(last_valid_state.address as u32); + if page_i > 0 { + // Last valid state is not in the first page, so we can erase it freely. (typical happy flow) + self.erase_pages(0..1).await?; + + // Write state. + self.inner.write(0, &state.as_bytes()).await?; + + // Erase rest of pages, and the erasure of the final page will validate our just written state. + // If this gets interrupted, the last state will remain valid. + self.erase_pages(1..Self::page_count(&self.inner)).await?; + } else { + // Last valid state is in the first page, but the rest of the pages contain no free slot. + // This edge-case we need to deal with separately. + + // Erase the last pages, which is safe as our last state lives in the first page. + let second_page_i = 1; + self.erase_pages(second_page_i..Self::page_count(&self.inner)).await?; + + // Write the state to the first address in the second page, immediately becoming the newest valid state. + let state_address = second_page_i * Self::PAGE_SIZE; + self.inner.write(state_address as u32, &state.as_bytes()).await?; + } + } else { + // No state is stored anywhere, and there are no empty slots, clear everything, write. + self.inner.erase(0, self.inner.capacity() as u32).await?; + self.inner.write(0, &state.as_bytes()).await?; + } + + // Re-compute the cache to check if the journal is valid. + self.cache = Self::compute_cache::(&mut self.inner).await?; + + // Check if the readback is successful. + if self.get() == Some(state) { + Ok(()) + } else { + Err(Error::ReadbackFailed) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::flash::mock::MockFlashBase; + use crate::state::{Slot, Status}; + + async fn test_journal(nvm: impl NorFlash, assert_empty: bool) -> Option { + let mut journal = FlashJournal::new::<4>(nvm).await.unwrap(); + + if assert_empty { + assert!(journal.get().is_none()); + } + + { + let slot_a = Slot::try_from(1).unwrap(); + let slot_b = Slot::try_from(2).unwrap(); + + let state = State::new(Status::Initial, slot_b, slot_a); + journal.set::<4>(&state).await.unwrap(); + assert_eq!(journal.get(), Some(&state)); + + // Re-do the same operation. + journal.set::<4>(&state).await.unwrap(); + assert_eq!(journal.get(), Some(&state)); + } + + // Write all different kinds of states. + for status in [Status::Initial, Status::Attempting, Status::Confirmed, Status::Failed] { + for i in 0b0..0b111u8 { + let slot_a = Slot::try_from(i).unwrap(); + + for j in 0b0..0b111u8 { + let slot_b = Slot::try_from(j).unwrap(); + + let state = State::new(status, slot_b, slot_a); + journal.set::<4>(&state).await.unwrap(); + assert_eq!(journal.get(), Some(&state)); + } + } + } + + journal.cache.first_empty_slot + } + + #[test] + fn journal_normal() { + let mut mock: MockFlashBase<3, 2, 8> = MockFlashBase::new(None, true); + embassy_futures::block_on(test_journal(&mut mock, true)); + } + + #[test] + fn journal_garbage() { + let mut mock: MockFlashBase<3, 2, 8> = MockFlashBase::new(None, true); + embassy_futures::block_on(async { + // Write garbage to pages 1 and 2. + let bytes = [0xaa; 32]; + mock.write(16, &bytes).await.unwrap(); + let valid_address = test_journal(&mut mock, true).await; + + // Insert broken as we happen to have a valid address with this sequence. + mock.write(valid_address.unwrap() as u32, &[0xaa, 0xaa]).await.unwrap(); + + test_journal(&mut mock, false).await; + }); + } + + #[test] + fn journal_realistic() { + // Use a realistic page count and size. + let mut mock: MockFlashBase<2, 2, 2048> = MockFlashBase::new(None, true); + embassy_futures::block_on(async { + for status in [Status::Initial, Status::Attempting, Status::Confirmed, Status::Failed] { + for i in 0b0..0b111u8 { + let slot_a = Slot::try_from(i).unwrap(); + + for j in 0b0..0b111u8 { + let slot_b = Slot::try_from(j).unwrap(); + + let state = State::new(status, slot_b, slot_a); + + // Practice you re-init the journal every boot and application load. + let mut journal = FlashJournal::new::<256>(&mut mock).await.unwrap(); + journal.set::<4>(&state).await.unwrap(); + assert_eq!(journal.get(), Some(&state)); + } + } + } + }); + } +} diff --git a/libs/ec-slimloader-state/src/flash/mock.rs b/libs/ec-slimloader-state/src/flash/mock.rs new file mode 100644 index 00000000..842dee4b --- /dev/null +++ b/libs/ec-slimloader-state/src/flash/mock.rs @@ -0,0 +1,227 @@ +use core::fmt::Display; +use core::ops::Range; +use std::vec::Vec; + +use embedded_storage_async::nor_flash::{ + ErrorType, MultiwriteNorFlash, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, +}; + +/// State of a word in the flash. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Writable { + /// Once (can only convert 1 bits to 0 + O, + /// Never (must be cleared before being writable again) + N, +} + +use Writable::*; + +/// Base type for in memory flash that can be used for mocking. +#[derive(Debug, Clone)] +pub struct MockFlashBase { + writable: Vec, + data: Vec, + /// A countdown to shutoff. When some and 0, an early shutoff will happen. + pub bytes_until_shutoff: Option, + /// When true, write buffers have to be aligned + pub alignment_check: bool, +} + +impl Default + for MockFlashBase +{ + fn default() -> Self { + Self::new(None, true) + } +} + +impl + MockFlashBase +{ + const CAPACITY_WORDS: usize = PAGES * PAGE_WORDS; + const CAPACITY_BYTES: usize = Self::CAPACITY_WORDS * BYTES_PER_WORD; + + const PAGE_BYTES: usize = PAGE_WORDS * BYTES_PER_WORD; + + /// Create a new flash instance. + pub fn new(bytes_until_shutoff: Option, alignment_check: bool) -> Self { + Self { + writable: vec![O; Self::CAPACITY_WORDS], + data: vec![u8::MAX; Self::CAPACITY_BYTES], + bytes_until_shutoff, + alignment_check, + } + } + + /// Get a reference to the underlying data. + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Get a mutable reference to the underlying data. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + &mut self.data + } + + fn validate_operation(offset: u32, length: usize) -> Result, MockFlashError> { + let offset = offset as usize; + if (offset % Self::READ_SIZE) != 0 { + Err(MockFlashError::NotAligned) + } else if offset > Self::CAPACITY_BYTES || offset + length > Self::CAPACITY_BYTES { + Err(MockFlashError::OutOfBounds) + } else { + Ok(offset..(offset + length)) + } + } + + fn check_shutoff(&mut self, address: u32, operation: Operation) -> Result<(), MockFlashError> { + if let Some(bytes_until_shutoff) = self.bytes_until_shutoff.as_mut() { + if let Some(next) = bytes_until_shutoff.checked_sub(1) { + *bytes_until_shutoff = next; + Ok(()) + } else { + self.bytes_until_shutoff = None; + Err(MockFlashError::EarlyShutoff(address, operation)) + } + } else { + Ok(()) + } + } +} + +impl ErrorType + for MockFlashBase +{ + type Error = MockFlashError; +} + +impl ReadNorFlash + for MockFlashBase +{ + const READ_SIZE: usize = BYTES_PER_WORD; + + async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + if bytes.len() % Self::READ_SIZE != 0 { + panic!("any read must be a multiple of Self::READ_SIZE bytes"); + } + + let range = Self::validate_operation(offset, bytes.len())?; + + bytes.copy_from_slice(&self.as_bytes()[range]); + + Ok(()) + } + + fn capacity(&self) -> usize { + Self::CAPACITY_BYTES + } +} + +impl MultiwriteNorFlash + for MockFlashBase +{ +} + +impl NorFlash + for MockFlashBase +{ + const WRITE_SIZE: usize = BYTES_PER_WORD; + + const ERASE_SIZE: usize = Self::PAGE_BYTES; + + async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + let from = from as usize; + let to = to as usize; + + assert!(from <= to); + + if to > Self::CAPACITY_BYTES { + return Err(MockFlashError::OutOfBounds); + } + + if from % Self::PAGE_BYTES != 0 || to % Self::PAGE_BYTES != 0 { + return Err(MockFlashError::NotAligned); + } + + for index in from..to { + self.check_shutoff(index as u32, Operation::Erase)?; + self.as_bytes_mut()[index] = u8::MAX; + + if index % BYTES_PER_WORD == 0 { + self.writable[index / BYTES_PER_WORD] = O; + } + } + + Ok(()) + } + + async fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + let range = Self::validate_operation(offset, bytes.len())?; + + // Check alignment. Some flash types are strict about the alignment of the input buffer. This ensures + // that the mock flash is also strict to catch bugs and avoid regressions. + if self.alignment_check && bytes.as_ptr() as usize % 4 != 0 { + panic!("write buffer must be aligned to 4 bytes"); + } + + if bytes.len() % Self::WRITE_SIZE != 0 { + panic!("any write must be a multiple of Self::WRITE_SIZE bytes"); + } + + for (source_word, address) in bytes.chunks_exact(BYTES_PER_WORD).zip(range.step_by(BYTES_PER_WORD)) { + for (byte_index, byte) in source_word.iter().enumerate() { + self.check_shutoff((address + byte_index) as u32, Operation::Write)?; + + if byte_index == 0 { + let word_writable = &mut self.writable[address / BYTES_PER_WORD]; + *word_writable = match *word_writable { + Writable::O => Writable::N, + Writable::N => return Err(MockFlashError::NotWritable(address as u32)), + }; + } + + self.as_bytes_mut()[address + byte_index] &= byte; + } + } + + Ok(()) + } +} + +/// Errors reported by mock flash. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MockFlashError { + /// Operation out of bounds. + OutOfBounds, + /// Offset or data not aligned. + NotAligned, + /// Location not writeable. + NotWritable(u32), + /// We got a shutoff + EarlyShutoff(u32, Operation), +} + +impl Display for MockFlashError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{self:?}") + } +} + +impl NorFlashError for MockFlashError { + fn kind(&self) -> NorFlashErrorKind { + match self { + MockFlashError::OutOfBounds => NorFlashErrorKind::OutOfBounds, + MockFlashError::NotAligned => NorFlashErrorKind::NotAligned, + MockFlashError::NotWritable(_) => NorFlashErrorKind::Other, + MockFlashError::EarlyShutoff(_, _) => NorFlashErrorKind::Other, + } + } +} + +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Operation { + Write, + Erase, +} diff --git a/libs/ec-slimloader-state/src/lib.rs b/libs/ec-slimloader-state/src/lib.rs new file mode 100644 index 00000000..3d31c0ad --- /dev/null +++ b/libs/ec-slimloader-state/src/lib.rs @@ -0,0 +1,9 @@ +//! Journal for the EC Slimloader containing [state::State]. +#![no_std] + +#[cfg(test)] +#[macro_use] +extern crate std; + +pub mod flash; +pub mod state; diff --git a/libs/ec-slimloader-state/src/state.rs b/libs/ec-slimloader-state/src/state.rs new file mode 100644 index 00000000..34769b63 --- /dev/null +++ b/libs/ec-slimloader-state/src/state.rs @@ -0,0 +1,212 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +pub const MAX_SLOT_COUNT: usize = 0b111; + +const CRC: crc::Crc = crc::Crc::::new(&crc::CRC_8_OPENSAFETY); + +/// Image slot ID. +/// +/// Valid values from 0x00 to 0x06. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Slot(u8); + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TooManyBits; + +impl TryFrom for Slot { + type Error = TooManyBits; + + fn try_from(val: u8) -> Result { + if val >= MAX_SLOT_COUNT as u8 { + Err(TooManyBits) + } else { + Ok(Slot(val)) + } + } +} + +impl From for u8 { + fn from(val: Slot) -> Self { + val.0 + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseResult { + /// Nor flash entry yet to be written. + Unset, + /// State is an invalid value. + Invalid, +} + +/// Boot process status as stored in [State] as a 2-bit field. +/// +/// The enum values are assigned such that bits can be dropped for the happy flow, +/// ensuring minimal wear on the storage. +#[derive(Debug, PartialEq, Clone, Copy, TryFromPrimitive, IntoPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum Status { + /// Initial attempt at booting the target image. + Initial = 3, + /// Bootloader marks that the initial attempt at bootloading has started. + Attempting = 2, + /// Bootloader has encountered in `Attempting`, meaning that the application failed to `Confirm`. + /// + /// Or the application image did not pass verification and was never tried. + Failed = 1, + /// Application has marked the boot to be successful, and will boot it in the future. + Confirmed = 0, +} + +/// State record as stored in the State boot journal. +/// +/// Care must be taken that `0xffff` is an invalid value, +/// as that is the typical value used by an empty NOR flash cell. +/// +/// We ensure this by disallowing Slot value 0b111. +#[derive(PartialEq, Clone, Copy)] +pub struct State([u8; 2]); + +impl State { + pub const fn new(status: Status, target: Slot, backup: Slot) -> Self { + let mut data = 0u8; + data |= (status as u8) << 6; + data |= backup.0 << 3; + data |= target.0; + + let crc = CRC.checksum(&[data]); + + Self([data, crc]) + } + + pub fn try_new(data: [u8; 2]) -> Result { + if data == [0xff, 0xff] { + return Err(ParseResult::Unset); + } + + if Self::try_target(data[0]).is_none() || Self::try_backup(data[0]).is_none() { + return Err(ParseResult::Invalid); + } + + if !Self::check_crc(data[1], data[0]) { + return Err(ParseResult::Invalid); + } + + Ok(State(data)) + } + + pub fn as_bytes(&self) -> [u8; 2] { + self.0 + } + + fn check_crc(crc: u8, data: u8) -> bool { + crc == CRC.checksum(&[data]) + } + + pub fn status(&self) -> Status { + // Note(unsafe): we are sure that any 2-bit u8 is a valid Status. + unsafe { Status::try_from_primitive(self.0[0] >> 6).unwrap_unchecked() } + } + + pub fn with_status(&self, status: Status) -> Self { + Self::new(status, self.target(), self.backup()) + } + + fn try_target(val: u8) -> Option { + Slot::try_from(val & 0b111).ok() + } + + pub fn target(&self) -> Slot { + // If Self exists, Slot must be valid. + unsafe { State::try_target(self.0[0]).unwrap_unchecked() } + } + + fn try_backup(val: u8) -> Option { + Slot::try_from((val >> 3) & 0b111).ok() + } + + pub fn backup(&self) -> Slot { + // If Self exists, Slot must be valid. + unsafe { State::try_backup(self.0[0]).unwrap_unchecked() } + } +} + +impl core::fmt::Debug for State { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("State") + .field("status", &self.status()) + .field("target", &self.target()) + .field("backup", &self.backup()) + .finish() + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for State { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "State {{ status: {}, target: {}, backup: {} }}", + self.status(), + self.target(), + self.backup() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test whether we can construct only valid [Slot] values. + #[test] + fn slot_construction() { + for i in 0b0..0b111u8 { + assert!(Slot::try_from(i).is_ok()); + } + + for i in 0b111..=0xffu8 { + assert!(Slot::try_from(i).is_err()); + } + } + + /// Construct all possible [State] values and test whether we can get fields back out again. + #[test] + fn state_validity_content() { + // Test all possible states. + for status in [Status::Initial, Status::Attempting, Status::Confirmed, Status::Failed] { + for i in 0b0..0b111u8 { + let slot_a = Slot::try_from(i).unwrap(); + + for j in 0b0..0b111u8 { + let slot_b = Slot::try_from(j).unwrap(); + + let state = State::new(status, slot_b, slot_a); + assert_eq!(state.status(), status); + assert_eq!(state.target(), slot_b); + assert_eq!(state.backup(), slot_a); + } + } + } + } + + /// Try a few handpicked [State] values and assert Crc value. + #[test] + fn state_validity_crc() { + let slot_a = Slot::try_from(1).unwrap(); + let slot_b = Slot::try_from(2).unwrap(); + + let state = State::new(Status::Initial, slot_b, slot_a); + assert_eq!(state.0[1], 12); // Crc + let state = State::new(Status::Attempting, slot_b, slot_a); + assert_eq!(state.0[1], 234); // Crc + let state = State::new(Status::Confirmed, slot_b, slot_a); + assert_eq!(state.0[1], 9); // Crc + let state = State::new(Status::Failed, slot_b, slot_a); + assert_eq!(state.0[1], 239); // Crc + } +} diff --git a/libs/ec-slimloader/.gitignore b/libs/ec-slimloader/.gitignore new file mode 100644 index 00000000..196e176d --- /dev/null +++ b/libs/ec-slimloader/.gitignore @@ -0,0 +1,19 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + + +# Added by cargo + +/target diff --git a/libs/ec-slimloader/Cargo.toml b/libs/ec-slimloader/Cargo.toml new file mode 100644 index 00000000..37373428 --- /dev/null +++ b/libs/ec-slimloader/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ec-slimloader" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[features] +defmt = [ + "dep:defmt", + "defmt-or-log/defmt", + "ec-slimloader-state/defmt", +] +log = [ + "dep:log", + "defmt-or-log/log" +] + +default = [] + +[dependencies] +ec-slimloader-state = { path = "../ec-slimloader-state", default-features = false } + +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true } +embedded-storage-async = { workspace = true } +log = { workspace = true, optional = true } diff --git a/libs/ec-slimloader/src/lib.rs b/libs/ec-slimloader/src/lib.rs new file mode 100644 index 00000000..28b945a6 --- /dev/null +++ b/libs/ec-slimloader/src/lib.rs @@ -0,0 +1,142 @@ +#![no_std] + +use defmt_or_log::{debug, error, info, unwrap, warn}; +use ec_slimloader_state::flash::FlashJournal; +use ec_slimloader_state::state::{Slot, State, Status}; +use embedded_storage_async::nor_flash::NorFlash; + +/// A board that can boot an application image. +/// +/// Typically a board needs to support the intrinsics for some microcontroller and +/// contain non volatile memory that stores the multiple images and bootloading state. +#[allow(async_fn_in_trait)] +pub trait Board { + /// Type used to instantiate a [Board] implementation. + type Config; + + /// Initialize the [Board], can only be called once. + async fn init(config: Self::Config) -> Self; + + /// Give a mutable reference to the [FlashJournal]. + fn journal(&mut self) -> &mut FlashJournal; + + /// Check the application image for integrity, and try to boot. + /// + /// 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; + + /// Give up booting into an application. + /// + /// Either shut down the device or go into an infinite loop. + fn abort(&mut self) -> !; +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BootError { + /// Slot is not defined. + SlotUnknown, + /// Image is too large to fit. + TooLarge, + /// Image cannot not possible be this small. + TooSmall, + /// Image did not contain the correct markers, + Markers, + /// Image requested to be copied into a disallowed memory region. + MemoryRegion, + /// What we copied from the NVM seems to have changed after initial read. + /// + /// Indicates a possible Man-in-the-Middle attack on the NVM. + ChangeAfterRead, + /// Image failed to authenticate. + Authenticate, + /// The underlying NVM threw an error. + IO, +} + +/// Intent which denotes which [Slot] should be booted. +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum BootIntent { + Target, + Backup, +} + +/// Set a new valid [State] as the latest in the [FlashJournal]. +async fn set_status(board: &mut B, state: &mut State, status: Status) { + *state = state.with_status(status); + if let Err(_e) = board.journal().set::(state).await { + panic!("Failed to update state"); // TODO print e, but requirements for defmt are in the way. + } + + debug!("Stored new state in journal: {:?}", state); +} + +pub async fn start(config: B::Config) -> ! { + let mut board = B::init::(config).await; + + let state = board.journal().get(); + + // Fetch state or set initial state. + let mut state: State = match state { + Some(state) => { + info!("Latest state fetched from journal: {:?}", state); + *state + } + None => { + let slot = unwrap!(Slot::try_from(0)); + warn!( + "Initial bootup and no state was loaded into the journal, attempting {:?}", + slot + ); + State::new(Status::Initial, slot, slot) + } + }; + + // Determine our intended slot to boot. + let intent = match state.status() { + Status::Initial => { + // Mark the status to [Attempting], so that the app can mark the status to [Confirmed]. + set_status::<_, JOURNAL_BUFFER_SIZE>(&mut board, &mut state, Status::Attempting).await; + BootIntent::Target + } + Status::Attempting => { + // When the bootloader starts with the state [Attempting], + // it implies that an attempt was made to start the application in the slot, + // but the application failed to mark the slot as [Confirmed]. + set_status::<_, JOURNAL_BUFFER_SIZE>(&mut board, &mut state, Status::Failed).await; + BootIntent::Backup + } + Status::Failed => BootIntent::Backup, + Status::Confirmed => BootIntent::Target, + }; + + // Translate the abstract intention to a concrete slot. + let slot = match intent { + BootIntent::Target => state.target(), + BootIntent::Backup => state.backup(), + }; + + 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. + warn!("Failed to boot {:?} in {:?} because {:?}", intent, slot, error); + + // 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; + } + + if slot != state.backup() { + // There exists a separate backup slot. + // That implies that we were in either [Initial] or [Confirmed], and now are in [Failed]. + // 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. + warn!("Failed to boot backup in {:?} because {:?}", slot, error); + } + + error!("No candidates booted successfully, giving up..."); + board.abort() +} diff --git a/libs/imxrt-rom/Cargo.toml b/libs/imxrt-rom/Cargo.toml new file mode 100644 index 00000000..cb55e416 --- /dev/null +++ b/libs/imxrt-rom/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "imxrt-rom" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[features] +rt = [] + +log = ["defmt-or-log/log"] +defmt = [ + "dep:defmt", + "defmt-or-log/defmt", + "device-driver/defmt-03" +] + +mimxrt685s = ["embassy-imxrt/mimxrt685s"] +mimxrt633s = ["embassy-imxrt/mimxrt633s"] + +[dependencies] +defmt = { workspace = true, optional = true } +defmt-or-log = { workspace = true } + +cortex-m = { workspace = true } +embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt.git", default-features = false, features = [ + "unstable-pac", +] } + +device-driver = { version = "1.0", default-features = false, features = ["json"] } + +[lints] +workspace = true diff --git a/libs/imxrt-rom/build.rs b/libs/imxrt-rom/build.rs new file mode 100644 index 00000000..f4f2086d --- /dev/null +++ b/libs/imxrt-rom/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=registers.yaml"); +} diff --git a/libs/imxrt-rom/registers.json b/libs/imxrt-rom/registers.json new file mode 100644 index 00000000..a8528f91 --- /dev/null +++ b/libs/imxrt-rom/registers.json @@ -0,0 +1,326 @@ +{ + "config": { + "default_byte_order": "LE", + "register_address_type": "u32", + "defmt_feature": "defmt" + }, + "BOOT_CFG0": { + "type": "register", + "access": "RW", + "address": 96, + "size_bits": 32, + "fields": { + "primary_boot_src": { + "base": "uint", + "start": 0, + "end": 4, + "try_conversion": { + "name": "boot_src", + "description": "Primary boot Source. (a.k.a. Master boot source)", + "isp_pin_boot": { + "value": 0, + "description": "ISP pins will determine boot source." + }, + "qpsi_a_boot": { + "value": 1, + "description": "Boot from Octal/Quad SPI flash device using FlexSpi channel A interface pins." + }, + "sdhc0_boot": { + "value": 2, + "description": "Boot from eMMC device or SD card connected to SDHC0 port." + }, + "sdhc1_boot": { + "value": 3, + "description": "Boot from eMMC device or SD card connected to SDHC0 port." + }, + "spi_slv_boot": { + "value": 4, + "description": "Boot using SPI slave interface using master boot mode." + }, + "qspi_b_boot": { + "value": 5, + "description": "Boot from Octal/Quad SPI flash device using FlexSpi channel B interface pins. Only load-to-RAM image are supported in this mode." + }, + "uart_boot": { + "value": 6, + "description": "Boot using UART interface using master boot mode." + }, + "spi_fc_boot": { + "value": 7, + "description": "Boot from 1-bit SPI flash device from FlexCom interface pins selected by REDUNDANT_SPI_PORT field. Only load-to-RAM images are supported in this mode." + }, + "isp_mode": { + "value": 9, + "description": "Always enter ISP mode. DEFAULT_ISP_MODE field will determine the ISP interface." + }, + "qspi_b_rec_boot": { + "value": 11, + "description": "Boot from Octal/Quad SPI flash device using FlexSPI channel B interface pins. If image is not found check recovery boot using SPI-flash device through FlexComm." + }, + "qspi_a_rec_boot": { + "value": 12, + "description": "Boot from Octal/Quad SPI flash device using FlexSPI channel A interface pins. If image is not found check recovery boot using SPI-flash device through FlexComm." + }, + "sdhc0_rec_boot": { + "value": 13, + "description": "Boot from SDHC0 port device. If image is not found check recovery boot using SPI-flash device through FlexComm." + }, + "sdhc1_rec_boot": { + "value": 15, + "description": "Boot from SDHC1 port device. If image is not found check recovery boot using SPI-flash device through FlexComm." + } + } + }, + "default_isp_mode": { + "base": "uint", + "start": 4, + "end": 7, + "try_conversion": { + "name": "default_isp_mode", + "description": "When a valid image is not available to master boot, ROM switches to ISP mode for programming primary boot devices. This field determines the default ISP mode.", + "auto_isp": { + "value": 0, + "description": "Auto detect ISP mode. ROM monitors USB, UART, SPI and I2C interfaces for any activity." + }, + "usb_hid_isp": { + "value": 1, + "description": "Support ISP command interface using USB HID class only." + }, + "uart_isp": { + "value": 2, + "description": "Support ISP command interface on UART port only." + }, + "spi_isp": { + "value": 3, + "description": "Support ISP command interface on SPI port only." + }, + "i2c_isp": { + "value": 4, + "description": "Support ISP command interface on I2C port only." + }, + "disable_isp": { + "value": 7, + "description": "Disable ISP fall through when proper image is not found on primary boot device." + } + } + }, + "boot_clk_speed": { + "base": "uint", + "start": 7, + "end": 8, + "conversion": { + "name": "boot_clk_speed", + "description": "Defines clock speeds during boot.", + "normal_clk": { + "description": "Normal boot. All clocks are set to 48MHz using IRC48M, except USB block. USB block will use external XTAL clock." + }, + "hispeed_clk": { + "description": "High-speed boot.\n* Core clock is set to 198MHz using main_pll with IRC48M as input\n* UART, I2C : 48MHz (IRC48M)\n* SPI, SDHC: 198MHz (main_pll)\n* USB: external XTAL\n* OSPI: Set to differnet speed using aux0_pll. Speed of OSPI interface is obtained from Boot Configuration Block present on OSPI-flash device.\n - SDR: 30/50/60/72/80/90/100 MHz\n - DDR: 30/50/60/72/80 MHz\n" + } + } + }, + "rsa4k_en": { + "base": "bool", + "start": 8, + "description": "Use 4096 bit RSA keys only for certificate validations. By default the ROM assume 2048-bit keys." + }, + "tzm_image_type": { + "base": "uint", + "start": 13, + "end": 15, + "conversion": { + "name": "tzm_image_type", + "tzm_normal": { + "value": 0, + "description": "TrustZone-M mode is determined by the image header." + }, + "tzm_disable": { + "value": 1, + "description": "Disable TrustZone-M features. ROM will always boot to a non-secure code and all TZ-M features are disabled." + }, + "tzm_enable": { + "value": 2, + "description": "TrustZone-M features are enabled. ROM will always boot to secure code." + }, + "tzm_preset": { + "value": 3, + "description": "TrustZone-M features are enabled and setting are loaded from image header and locked before branching to user code." + } + } + }, + "psa_bstate_skip": { + "base": "bool", + "start": 15, + "description": "If set, ROM skips computation of boot state defined by PSA specification. As part of boot state computation ROM includes OTP words \n- Shadow register values of 95 to 104\n- Fuse values of words 128 to 147\n" + }, + "psa_bstate_inc_keys": { + "base": "bool", + "start": 16, + "description": "If set, boot state computation includes OTP shadow register values of words 106 to 127." + }, + "redundant_spi_port": { + "base": "uint", + "start": 17, + "end": 20, + "conversion": { + "name": "redundant_spi_port", + "description": "FlexComm port to use for redundant SPI flash boot.", + "fc0": { + "description": "Use FlexCom0 pins P0_0 (SCK), P0_1 (MISO), P0_2 (MOSI), P0_3 (SEL)" + }, + "fc1": { + "description": "Use FlexCom1 pins P0_7 (SCK), P0_8 (MISO), P0_9 (MOSI), P0_10 (SEL)" + }, + "fc2": { + "description": "Use FlexCom2 pins P0_14 (SCK), P0_15 (MISO), P0_16 (MOSI), P0_17 (SEL)" + }, + "fc3": { + "description": "Use FlexCom3 pins P0_21 (SCK), P0_22 (MISO), P0_23 (MOSI), P0_24 (SEL)" + }, + "fc4": { + "description": "Use FlexCom4 pins P0_28 (SCK), P0_29 (MISO), P0_30 (MOSI), P0_31 (SEL)" + }, + "fc5": { + "description": "Use FlexCom5 pins P1_3 (SCK), P1_4 (MISO), P1_5 (MOSI), P1_6 (SEL)" + }, + "fc6": { + "description": "Use FlexCom6 pins P3_25 (SCK), P3_26 (MISO), P3_27 (MOSI), P3_28 (SEL)" + }, + "fc7": { + "description": "Use FlexCom7 pins P4_0 (SCK), P4_1 (MISO), P4_2 (MOSI), P4_3 (SEL)" + } + } + }, + "secure_boot_en": { + "base": "uint", + "start": 20, + "end": 22, + "conversion": { + "name": "secure_boot", + "description": "Force secure image only.", + "disabled": 0, + "enabled": "default" + } + }, + "dice_inc_otp": { + "base": "bool", + "start": 22, + "description": "Include non-field updatable OTP Fields in DICE computation. OTP values in shadow registers are used in computation for words 95, 96, 98, 99, 104, 120 - 127." + }, + "dice_skip": { + "base": "bool", + "start": 23, + "description": "If set, ROM skips computation of Composite Device Identifier (CDI) defined in DICE specification. But ROM will continue to hide UDS source in OTP and PUF (index 15) before passing control to user code." + }, + "boot_fail_pin_port": { + "base": "uint", + "start": 24, + "end": 27, + "description": "GPIO port to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system.\n" + }, + "boot_fail_pin_num": { + "base": "uint", + "start": 27, + "end": 32, + "description": "GPIO pin number to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system.\n" + } + } + }, + "BOOT_CFG1": { + "type": "register", + "access": "RW", + "address": 97, + "size_bits": 32, + "fields": { + "qspi_reset_pin_enable": { + "base": "bool", + "start": 14, + "description": "Use QSPI_RESET_PIN to reset the flash device." + }, + "qspi_reset_pin_port": { + "base": "uint", + "start": 15, + "end": 18, + "description": "GPIO port to use for O/QSPI reset function." + }, + "qspi_reset_pin_num": { + "base": "uint", + "start": 18, + "end": 23, + "description": "GPIO pin number to use for O/QSPI reset function." + } + } + }, + "SEC_BOOT_CFG5": { + "type": "register", + "access": "RW", + "address": 101, + "size_bits": 16, + "fields": { + "revoke_rootkey": { + "base": "uint", + "start": 0, + "end": 4, + "description": "Revoke upto 4 root keys. When a bit is set corresponding root key is revoked." + }, + "fa_mode_en": { + "base": "bool", + "start": 4, + "end": 5, + "description": "Enable Fault Analysis mode.\n- When set ROM checks and erases customer sensitive assets (AES keys or key codes) stored in IFR/OTP.\n- Issues zeroized command to PUF (disables key decoding until POR).\n- Blocks all HW routed OTP keys and set lock bits on those registers.\n- Enables all debug ports and waits in loop for tester.\n" + }, + "enable_crc_check": { + "base": "uint", + "start": 5, + "end": 7, + "description": "Enable CRC checking of OTP words.", + "conversion": { + "name": "crc_check", + "disable": 0, + "enable": 1, + "nxp_only": 2, + "enable2": 3 + } + }, + "use_puf": { + "base": "uint", + "start": 7, + "end": 8, + "description": "Use PUF to store AES keys and UDS.", + "conversion": { + "name": "key_in", + "otp": 0, + "ouf": 1 + } + }, + "puf_block_enroll": { + "base": "uint", + "start": 8, + "end": 9, + "description": "Block further enrollement of PUF block. When this bit is set ROM blocks generation of new activation codes.", + "conversion": { + "name": "enroll", + "enable": 0, + "disable": 1 + } + }, + "puf_block_set_key": { + "base": "uint", + "start": 9, + "end": 10, + "description": "Block further enrollement of PUF block. When this bit is set ROM blocks generation of new key codes.", + "conversion": { + "name": "key_gen", + "enable": 0, + "disable": 1 + } + } + } + }, + "RKTH": { + "type": "register", + "access": "RW", + "address": 120, + "size_bits": 256 + } +} \ No newline at end of file diff --git a/libs/imxrt-rom/src/api.rs b/libs/imxrt-rom/src/api.rs new file mode 100644 index 00000000..525ef524 --- /dev/null +++ b/libs/imxrt-rom/src/api.rs @@ -0,0 +1,191 @@ +#[repr(C)] +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Version { + bugfix: u8, + minor: u8, + major: u8, + name: u8, +} + +#[repr(C)] +pub struct SKBoot { + pub authenticate: unsafe extern "C" fn(start_addr: *const u32, is_verified: *mut u32) -> u32, + pub hashcrypt_irq_handler: unsafe extern "C" fn() -> (), +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct KbAuthenticate { + pub profile: u32, + pub min_build_number: u32, + pub max_image_length: u32, + pub user_rhk: *const u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct KbRegion { + pub address: u32, + pub length: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct KbLoadSB { + pub profile: u32, + pub min_build_number: u32, + pub override_sbboot_section_id: u32, + pub user_sbkek: *const u32, + pub region_count: u32, + pub regions: *const KbRegion, +} + +#[repr(C)] +pub union KbSettings { + pub authenticate: KbAuthenticate, + pub load_sb: KbLoadSB, +} + +#[repr(C)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[allow(unused)] +pub enum KbOperation { + AuthenticateImage = 1, + LoadImage = 2, +} + +#[repr(C)] +pub struct KbOptions { + pub version: u32, + pub buffer: *mut u8, + pub buffer_length: u32, + pub op: KbOperation, + pub settings: KbSettings, +} + +#[repr(C)] +pub struct KbSessionRef { + pub context: KbOptions, + pub cau_3_initialized: bool, + pub memory_map: *const u8, +} + +#[repr(C)] +#[derive(PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[allow(unused)] +pub enum KbStatus { + Success = 0, + Fail = 1, + ReadOnly = 2, + OutOfRange = 3, + InvalidArgument = 4, + Timeout = 5, + NoTransferInProgress = 6, + /// Undocumented status code when passing insufficient memory. + UnknownInsufficientMemory = 10, + /// Incorrect SB2.1 loader signature. + Signature = 10101, + /// The SB state machine is waiting for more data. + DataUnderrun = 10109, + /// An image version rollback event has been detected. + RollbackBlocked = 10115, + Unknown, +} + +#[repr(C)] +pub struct IAPDriver { + pub init: unsafe extern "C" fn(*mut *mut KbSessionRef, *const KbOptions) -> u32, + pub deinit: unsafe extern "C" fn(*mut KbSessionRef) -> u32, + pub execute: unsafe extern "C" fn(*mut KbSessionRef, *const u8, u32) -> u32, +} + +#[repr(C)] +pub struct OTPDriver { + pub init: unsafe extern "C" fn(src_clk_freq: u32) -> u32, + pub deinit: unsafe extern "C" fn() -> u32, + pub fuse_read: unsafe extern "C" fn(addr: u32, *mut u8) -> u32, + pub fuse_program: unsafe extern "C" fn(addr: u32, data: u32, lock: bool) -> u32, + pub crc_calc: unsafe extern "C" fn(src: *const u32, number_of_worlds: u32, crc_checksum: *const u32) -> u32, + pub reload: unsafe extern "C" fn() -> u32, + pub crc_check: unsafe extern "C" fn(start_addr: u32, end_addr: u32, crc_addr: u32) -> u32, +} + +/// ROM API layout 42.9.3.1, RT6xx user manual UM11147. +#[repr(C)] +pub struct ApiTable { + bootloader_fn: unsafe extern "C" fn(*const u8), + pub version: Version, + pub copyright: &'static [u8; 0], + reserved: u32, + pub iap_driver: &'static IAPDriver, + reserved1: u32, + reserved2: u32, + flash_driver: &'static [u8; 0], // stubbed + pub otp_driver: &'static OTPDriver, + pub skboot: &'static SKBoot, +} + +extern "C" { + static API_TABLE: ApiTable; +} + +pub fn api_table() -> &'static ApiTable { + unsafe { &API_TABLE } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BootStatus { + Success, + Fail, + InvalidArgument, + KeyStoreMarkerInvalid, + HashcryptFinishedWithStatusSuccess, + HashcryptFinishedWithStatusFail, +} + +impl TryFrom for BootStatus { + type Error = (); + + fn try_from(value: u32) -> Result { + Ok(match value { + 0x5ac3c35a => BootStatus::Success, + 0xc35ac35a => BootStatus::Fail, + 0xc35a5ac3 => BootStatus::InvalidArgument, + 0xc3c35a5a => BootStatus::KeyStoreMarkerInvalid, + 0xc15a5ac3 => BootStatus::HashcryptFinishedWithStatusSuccess, + 0xc15a5acb => BootStatus::HashcryptFinishedWithStatusFail, + _ => return Err(()), + }) + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SecureBool { + True, + False, + CallProtectSecurityFlags, + CallProtectIsAppReady, + TrackerVerified, +} + +impl TryFrom for SecureBool { + type Error = (); + + fn try_from(value: u32) -> Result { + Ok(match value { + 0xc33cc33c => SecureBool::True, + 0x5aa55aa5 => SecureBool::False, + 0xc33c5aa5 => SecureBool::CallProtectSecurityFlags, + 0x5aa5c33c => SecureBool::CallProtectIsAppReady, + 0x55aacc33 => SecureBool::TrackerVerified, + _ => { + return Err(()); + } + }) + } +} diff --git a/libs/imxrt-rom/src/lib.rs b/libs/imxrt-rom/src/lib.rs new file mode 100644 index 00000000..9061e9c5 --- /dev/null +++ b/libs/imxrt-rom/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +#[cfg(test)] +#[macro_use] +extern crate std; + +pub(crate) mod api; + +pub mod otp; +pub mod registers; +pub mod skboot; diff --git a/libs/imxrt-rom/src/otp.rs b/libs/imxrt-rom/src/otp.rs new file mode 100644 index 00000000..aeed8492 --- /dev/null +++ b/libs/imxrt-rom/src/otp.rs @@ -0,0 +1,79 @@ +//! API to read and manipulate the OTP fuses. + +use core::sync::atomic::{AtomicBool, Ordering}; + +use crate::api::{api_table, KbStatus}; + +/// Whether the Otp driver has been initialized. +/// +/// If initialized without deinitializing, this results in a panic being thrown. +static INITIALIZED: AtomicBool = AtomicBool::new(false); + +pub struct Otp { + _private: (), +} + +#[derive(Debug)] +#[allow(dead_code)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Error(u32); + +impl Otp { + pub fn init(system_clock_frequency_hz: u32) -> Self { + if INITIALIZED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + defmt_or_log::panic!("Using OTP whilst it has already been initialized"); + }; + + unsafe { + (api_table().otp_driver.init)(system_clock_frequency_hz); + } + + Otp { _private: () } + } +} + +impl Otp { + /// Read the value directly from the fuse. + pub fn read_fuse(&mut self, addr: u32) -> Result { + let mut result = [0u8; 4]; + let status = unsafe { (api_table().otp_driver.fuse_read)(addr, result.as_mut_ptr()) }; + if status == KbStatus::Success as u32 { + Ok(u32::from_le_bytes(result)) + } else { + Err(Error(status)) + } + } + + pub fn write_fuse(&mut self, addr: u32, data: u32, lock: bool) -> Result<(), Error> { + let status = unsafe { (api_table().otp_driver.fuse_program)(addr, data, lock) }; + if status == KbStatus::Success as u32 { + Ok(()) + } else { + defmt_or_log::error!("OTP write failed with {:x}", status); + Err(Error(status)) + } + } + + /// Reload all shadow registers from what is stored in OTP fuses. + pub fn reload_shadow(&mut self) -> Result<(), Error> { + let status = unsafe { (api_table().otp_driver.reload)() }; + if status == KbStatus::Success as u32 { + Ok(()) + } else { + Err(Error(status)) + } + } +} + +impl Drop for Otp { + fn drop(&mut self) { + unsafe { + (api_table().otp_driver.deinit)(); + } + + INITIALIZED.store(false, Ordering::Release); + } +} diff --git a/libs/imxrt-rom/src/registers.rs b/libs/imxrt-rom/src/registers.rs new file mode 100644 index 00000000..ceac0099 --- /dev/null +++ b/libs/imxrt-rom/src/registers.rs @@ -0,0 +1,220 @@ +//! Registers that are available as OTP fuses and as shadow registers. +#![allow(dead_code)] + +mod device; + +use device_driver::RegisterInterface; + +use crate::otp::Otp; + +// Include the device driver definition. +// Can be regenerated by running `device-driver-cli -m registers.json -o src/registers/device.rs --device-name Device`. +// Will cause the GH workflow to SIGPIPE if included directly. +include!("registers/device.rs"); + +/// Interface to access the shadow registers. +pub struct ShadowInterface { + _private: (), +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct NotShadowRegister; + +/// Convert an OTP word index to offset in the shadow register block. +const fn otp_to_shadow_offset(otp_word_i: u32) -> Result { + let shadow_offset = match otp_word_i { + 8..=9 => (otp_word_i - 8) * 4 + 0x020, + 95..=104 => (otp_word_i - 95) * 4 + 0x17C, + 106..=127 => (otp_word_i - 106) * 4 + 0x1A8, + 492..=495 => (otp_word_i - 492) * 4 + 0x7B0, + _ => return Err(NotShadowRegister), + }; + + Ok(shadow_offset as usize) +} + +/// Convert an OTP word index to address in the shadow register block. +fn otp_to_shadow_addr(otp_word_i: u32) -> Result<*mut u32, NotShadowRegister> { + const OTP_SHADOW_BASE_ADDR: usize = 0x40130000; + Ok((OTP_SHADOW_BASE_ADDR + otp_to_shadow_offset(otp_word_i)?) as *mut u32) +} + +impl RegisterInterface for ShadowInterface { + type Error = NotShadowRegister; + type AddressType = u32; + + fn write_register( + &mut self, + otp_word_i: Self::AddressType, + _size_bits: u32, + data: &[u8], + ) -> Result<(), Self::Error> { + for (chunk_i, chunk) in data.chunks(4).enumerate() { + let otp_word_i = otp_word_i + chunk_i as u32; + let shadow_addr = otp_to_shadow_addr(otp_word_i)?; + + let word = if chunk.len() != 4 { + let mut buf = [0u8; 4]; + buf[..chunk.len()].copy_from_slice(chunk); + u32::from_le_bytes(buf) + } else { + // Safety: we have chunks of exactly 4 bytes, hence the conversion to [u8; 4] is safe. + u32::from_le_bytes(unsafe { chunk.try_into().unwrap_unchecked() }) + }; + + // Safety: we assume that the register yaml definition is correct, and that each register is aligned. + unsafe { shadow_addr.write_volatile(word) }; + } + Ok(()) + } + + fn read_register( + &mut self, + otp_word_i: Self::AddressType, + _size_bits: u32, + data: &mut [u8], + ) -> Result<(), Self::Error> { + // Safety: we assume that the register yaml definition is correct, no need for volatile memory access. + let shadow_addr = otp_to_shadow_addr(otp_word_i)? as *const u8; + let source = unsafe { core::slice::from_raw_parts(shadow_addr, data.len()) }; + data.copy_from_slice(source); + Ok(()) + } +} + +pub struct OtpInterface<'a> { + otp: &'a mut Otp, + allow_write: bool, + mode_locked: bool, +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OtpError { + WriteNotAllowed, + Inner(crate::otp::Error), +} + +impl From for OtpError { + fn from(value: crate::otp::Error) -> Self { + OtpError::Inner(value) + } +} + +impl RegisterInterface for OtpInterface<'_> { + type Error = OtpError; + type AddressType = u32; + + fn write_register(&mut self, address: Self::AddressType, _size_bits: u32, data: &[u8]) -> Result<(), Self::Error> { + if !self.allow_write { + return Err(OtpError::WriteNotAllowed); + } + + for (i, chunk) in data.chunks_exact(4).enumerate() { + // Safety: we have chunks of exactly 4 bytes, hence the conversion to [u8; 4] is safe. + let word = u32::from_le_bytes(unsafe { chunk.try_into().unwrap_unchecked() }); + + self.otp.write_fuse(address + i as u32, word, self.mode_locked)?; + } + Ok(()) + } + + fn read_register( + &mut self, + address: Self::AddressType, + _size_bits: u32, + data: &mut [u8], + ) -> Result<(), Self::Error> { + for (i, chunk) in data.chunks_exact_mut(4).enumerate() { + chunk.copy_from_slice(&self.otp.read_fuse(address + i as u32)?.to_le_bytes()); + } + Ok(()) + } +} + +pub struct ShadowRegisters { + device: Device, +} + +impl ShadowRegisters { + pub const fn new() -> Self { + Self { + device: Device::new(ShadowInterface { _private: () }), + } + } +} + +impl Default for ShadowRegisters { + fn default() -> Self { + Self::new() + } +} + +impl core::ops::Deref for ShadowRegisters { + type Target = Device; + + fn deref(&self) -> &Self::Target { + &self.device + } +} + +impl core::ops::DerefMut for ShadowRegisters { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.device + } +} + +pub struct OtpFuses<'a> { + device: Device>, +} + +impl<'a> OtpFuses<'a> { + pub fn readonly(otp: &'a mut Otp) -> Self { + Self { + device: Device::new(OtpInterface { + otp, + allow_write: false, + mode_locked: false, + }), + } + } + + pub fn writable(otp: &'a mut Otp, mode_locked: bool) -> Self { + Self { + device: Device::new(OtpInterface { + otp, + allow_write: true, + mode_locked, + }), + } + } +} + +impl<'a> core::ops::Deref for OtpFuses<'a> { + type Target = Device>; + + fn deref(&self) -> &Self::Target { + &self.device + } +} + +impl core::ops::DerefMut for OtpFuses<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.device + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mapping() { + assert_eq!(otp_to_shadow_offset(96), Ok(0x180)); // BOOT_CFG[0] + assert_eq!(otp_to_shadow_offset(97), Ok(0x184)); // BOOT_CFG[1] + assert_eq!(otp_to_shadow_offset(101), Ok(0x194)); // SEC_BOOT_CFG[5] + assert_eq!(otp_to_shadow_offset(120), Ok(0x1E0)); // RKTH[0] + assert_eq!(otp_to_shadow_offset(127), Ok(0x1FC)); // RKTH[7] + } +} diff --git a/libs/imxrt-rom/src/registers/device.rs b/libs/imxrt-rom/src/registers/device.rs new file mode 100644 index 00000000..42e4f5b9 --- /dev/null +++ b/libs/imxrt-rom/src/registers/device.rs @@ -0,0 +1,1784 @@ +/// Root block of the Device driver + +#[derive(Debug)] +pub struct Device { + pub(crate) interface: I, + + #[doc(hidden)] + base_address: u8, +} + +impl Device { + /// Create a new instance of the block based on device interface + pub const fn new(interface: I) -> Self { + Self { + interface, + base_address: 0, + } + } + + /// A reference to the interface used to communicate with the device + pub(crate) fn interface(&mut self) -> &mut I { + &mut self.interface + } + + /// Read all readable register values in this block from the device. + /// The callback is called for each of them. + /// Any registers in child blocks are not included. + /// + /// The callback has three arguments: + /// + /// - The address of the register + /// - The name of the register (with index for repeated registers) + /// - The read value from the register + /// + /// This is useful for e.g. debug printing all values. + /// The given [field_sets::FieldSetValue] has a Debug and Format implementation that forwards to the concrete type + /// the lies within so it can be printed without matching on it. + #[allow(unused_mut)] + #[allow(unused_variables)] + pub fn read_all_registers( + &mut self, + mut callback: impl FnMut(u32, &'static str, field_sets::FieldSetValue), + ) -> Result<(), I::Error> + where + I: ::device_driver::RegisterInterface, + { + let reg = self.boot_cfg_0().read()?; + + callback(96 + 0 * 0, "boot_cfg_0", reg.into()); + + let reg = self.boot_cfg_1().read()?; + + callback(97 + 0 * 0, "boot_cfg_1", reg.into()); + + let reg = self.sec_boot_cfg_5().read()?; + + callback(101 + 0 * 0, "sec_boot_cfg_5", reg.into()); + + let reg = self.rkth().read()?; + + callback(120 + 0 * 0, "rkth", reg.into()); + + Ok(()) + } + + /// Read all readable register values in this block from the device. + /// The callback is called for each of them. + /// Any registers in child blocks are not included. + /// + /// The callback has three arguments: + /// + /// - The address of the register + /// - The name of the register (with index for repeated registers) + /// - The read value from the register + /// + /// This is useful for e.g. debug printing all values. + /// The given [field_sets::FieldSetValue] has a Debug and Format implementation that forwards to the concrete type + /// the lies within so it can be printed without matching on it. + #[allow(unused_mut)] + #[allow(unused_variables)] + pub async fn read_all_registers_async( + &mut self, + mut callback: impl FnMut(u32, &'static str, field_sets::FieldSetValue), + ) -> Result<(), I::Error> + where + I: ::device_driver::AsyncRegisterInterface, + { + let reg = self.boot_cfg_0().read_async().await?; + + callback(96 + 0 * 0, "boot_cfg_0", reg.into()); + + let reg = self.boot_cfg_1().read_async().await?; + + callback(97 + 0 * 0, "boot_cfg_1", reg.into()); + + let reg = self.sec_boot_cfg_5().read_async().await?; + + callback(101 + 0 * 0, "sec_boot_cfg_5", reg.into()); + + let reg = self.rkth().read_async().await?; + + callback(120 + 0 * 0, "rkth", reg.into()); + + Ok(()) + } + + pub fn boot_cfg_0( + &mut self, + ) -> ::device_driver::RegisterOperation<'_, I, u32, field_sets::BootCfg0, ::device_driver::RW> { + let address = self.base_address + 96; + + ::device_driver::RegisterOperation::<'_, I, u32, field_sets::BootCfg0, ::device_driver::RW>::new( + self.interface(), + address as u32, + field_sets::BootCfg0::new, + ) + } + + pub fn boot_cfg_1( + &mut self, + ) -> ::device_driver::RegisterOperation<'_, I, u32, field_sets::BootCfg1, ::device_driver::RW> { + let address = self.base_address + 97; + + ::device_driver::RegisterOperation::<'_, I, u32, field_sets::BootCfg1, ::device_driver::RW>::new( + self.interface(), + address as u32, + field_sets::BootCfg1::new, + ) + } + + pub fn sec_boot_cfg_5( + &mut self, + ) -> ::device_driver::RegisterOperation<'_, I, u32, field_sets::SecBootCfg5, ::device_driver::RW> { + let address = self.base_address + 101; + + ::device_driver::RegisterOperation::<'_, I, u32, field_sets::SecBootCfg5, ::device_driver::RW>::new( + self.interface(), + address as u32, + field_sets::SecBootCfg5::new, + ) + } + + pub fn rkth(&mut self) -> ::device_driver::RegisterOperation<'_, I, u32, field_sets::Rkth, ::device_driver::RW> { + let address = self.base_address + 120; + + ::device_driver::RegisterOperation::<'_, I, u32, field_sets::Rkth, ::device_driver::RW>::new( + self.interface(), + address as u32, + field_sets::Rkth::new, + ) + } +} + +/// Module containing the generated fieldsets of the registers and commands +pub mod field_sets { + #[allow(unused_imports)] + use super::*; + + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct BootCfg0 { + /// The internal bits + bits: [u8; 4], + } + + impl ::device_driver::FieldSet for BootCfg0 { + const SIZE_BITS: u32 = 32; + fn new_with_zero() -> Self { + Self::new_zero() + } + fn get_inner_buffer(&self) -> &[u8] { + &self.bits + } + fn get_inner_buffer_mut(&mut self) -> &mut [u8] { + &mut self.bits + } + } + + impl BootCfg0 { + /// Create a new instance, loaded with the reset value (if any) + pub const fn new() -> Self { + Self { bits: [0, 0, 0, 0] } + } + /// Create a new instance, loaded with all zeroes + pub const fn new_zero() -> Self { + Self { bits: [0; 4] } + } + + ///Read the `primary_boot_src` field of the register. + /// + + pub fn primary_boot_src(&self) -> Result>::Error> { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 0, 4) }; + + raw.try_into() + } + + ///Read the `default_isp_mode` field of the register. + /// + + pub fn default_isp_mode(&self) -> Result>::Error> { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 4, 7) }; + + raw.try_into() + } + + ///Read the `boot_clk_speed` field of the register. + /// + + pub fn boot_clk_speed(&self) -> super::BootClkSpeed { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 7, 8) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `rsa_4_k_en` field of the register. + /// + /// Use 4096 bit RSA keys only for certificate validations. By default the ROM assume 2048-bit keys. + + pub fn rsa_4_k_en(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 8, 9) }; + + raw > 0 + } + + ///Read the `tzm_image_type` field of the register. + /// + + pub fn tzm_image_type(&self) -> super::TzmImageType { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 13, 15) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `psa_bstate_skip` field of the register. + /// + /// If set, ROM skips computation of boot state defined by PSA specification. As part of boot state computation ROM includes OTP words + /// - Shadow register values of 95 to 104 + /// - Fuse values of words 128 to 147 + + pub fn psa_bstate_skip(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 15, 16) }; + + raw > 0 + } + + ///Read the `psa_bstate_inc_keys` field of the register. + /// + /// If set, boot state computation includes OTP shadow register values of words 106 to 127. + + pub fn psa_bstate_inc_keys(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 16, 17) }; + + raw > 0 + } + + ///Read the `redundant_spi_port` field of the register. + /// + + pub fn redundant_spi_port(&self) -> super::RedundantSpiPort { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 17, 20) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `secure_boot_en` field of the register. + /// + + pub fn secure_boot_en(&self) -> super::SecureBoot { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 20, 22) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `dice_inc_otp` field of the register. + /// + /// Include non-field updatable OTP Fields in DICE computation. OTP values in shadow registers are used in computation for words 95, 96, 98, 99, 104, 120 - 127. + + pub fn dice_inc_otp(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 22, 23) }; + + raw > 0 + } + + ///Read the `dice_skip` field of the register. + /// + /// If set, ROM skips computation of Composite Device Identifier (CDI) defined in DICE specification. But ROM will continue to hide UDS source in OTP and PUF (index 15) before passing control to user code. + + pub fn dice_skip(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 23, 24) }; + + raw > 0 + } + + ///Read the `boot_fail_pin_port` field of the register. + /// + /// GPIO port to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system. + + pub fn boot_fail_pin_port(&self) -> u8 { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 24, 27) }; + + raw + } + + ///Read the `boot_fail_pin_num` field of the register. + /// + /// GPIO pin number to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system. + + pub fn boot_fail_pin_num(&self) -> u8 { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 27, 32) }; + + raw + } + + ///Write the `primary_boot_src` field of the register. + /// + + pub fn set_primary_boot_src(&mut self, value: super::BootSrc) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 0, 4, &mut self.bits) }; + } + + ///Write the `default_isp_mode` field of the register. + /// + + pub fn set_default_isp_mode(&mut self, value: super::DefaultIspMode) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 4, 7, &mut self.bits) }; + } + + ///Write the `boot_clk_speed` field of the register. + /// + + pub fn set_boot_clk_speed(&mut self, value: super::BootClkSpeed) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 7, 8, &mut self.bits) }; + } + + ///Write the `rsa_4_k_en` field of the register. + /// + /// Use 4096 bit RSA keys only for certificate validations. By default the ROM assume 2048-bit keys. + + pub fn set_rsa_4_k_en(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 8, 9, &mut self.bits) }; + } + + ///Write the `tzm_image_type` field of the register. + /// + + pub fn set_tzm_image_type(&mut self, value: super::TzmImageType) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 13, 15, &mut self.bits) }; + } + + ///Write the `psa_bstate_skip` field of the register. + /// + /// If set, ROM skips computation of boot state defined by PSA specification. As part of boot state computation ROM includes OTP words + /// - Shadow register values of 95 to 104 + /// - Fuse values of words 128 to 147 + + pub fn set_psa_bstate_skip(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 15, 16, &mut self.bits) }; + } + + ///Write the `psa_bstate_inc_keys` field of the register. + /// + /// If set, boot state computation includes OTP shadow register values of words 106 to 127. + + pub fn set_psa_bstate_inc_keys(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 16, 17, &mut self.bits) }; + } + + ///Write the `redundant_spi_port` field of the register. + /// + + pub fn set_redundant_spi_port(&mut self, value: super::RedundantSpiPort) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 17, 20, &mut self.bits) }; + } + + ///Write the `secure_boot_en` field of the register. + /// + + pub fn set_secure_boot_en(&mut self, value: super::SecureBoot) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 20, 22, &mut self.bits) }; + } + + ///Write the `dice_inc_otp` field of the register. + /// + /// Include non-field updatable OTP Fields in DICE computation. OTP values in shadow registers are used in computation for words 95, 96, 98, 99, 104, 120 - 127. + + pub fn set_dice_inc_otp(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 22, 23, &mut self.bits) }; + } + + ///Write the `dice_skip` field of the register. + /// + /// If set, ROM skips computation of Composite Device Identifier (CDI) defined in DICE specification. But ROM will continue to hide UDS source in OTP and PUF (index 15) before passing control to user code. + + pub fn set_dice_skip(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 23, 24, &mut self.bits) }; + } + + ///Write the `boot_fail_pin_port` field of the register. + /// + /// GPIO port to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system. + + pub fn set_boot_fail_pin_port(&mut self, value: u8) { + let raw = value; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 24, 27, &mut self.bits) }; + } + + ///Write the `boot_fail_pin_num` field of the register. + /// + /// GPIO pin number to use for indicating boot failure. Boot ROM will drive this pin high before locking the chip on error conditions. Applications can use this pin to power cycle the system. + + pub fn set_boot_fail_pin_num(&mut self, value: u8) { + let raw = value; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 27, 32, &mut self.bits) }; + } + } + + impl From<[u8; 4]> for BootCfg0 { + fn from(bits: [u8; 4]) -> Self { + Self { bits } + } + } + + impl From for [u8; 4] { + fn from(val: BootCfg0) -> Self { + val.bits + } + } + + impl core::fmt::Debug for BootCfg0 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + let mut d = f.debug_struct("BootCfg0"); + + d.field("primary_boot_src", &self.primary_boot_src()); + + d.field("default_isp_mode", &self.default_isp_mode()); + + d.field("boot_clk_speed", &self.boot_clk_speed()); + + d.field("rsa_4_k_en", &self.rsa_4_k_en()); + + d.field("tzm_image_type", &self.tzm_image_type()); + + d.field("psa_bstate_skip", &self.psa_bstate_skip()); + + d.field("psa_bstate_inc_keys", &self.psa_bstate_inc_keys()); + + d.field("redundant_spi_port", &self.redundant_spi_port()); + + d.field("secure_boot_en", &self.secure_boot_en()); + + d.field("dice_inc_otp", &self.dice_inc_otp()); + + d.field("dice_skip", &self.dice_skip()); + + d.field("boot_fail_pin_port", &self.boot_fail_pin_port()); + + d.field("boot_fail_pin_num", &self.boot_fail_pin_num()); + + d.finish() + } + } + + #[cfg(feature = "defmt")] + impl defmt::Format for BootCfg0 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "BootCfg0 {{ "); + + defmt::write!(f, "primary_boot_src: {}, ", &self.primary_boot_src()); + + defmt::write!(f, "default_isp_mode: {}, ", &self.default_isp_mode()); + + defmt::write!(f, "boot_clk_speed: {}, ", &self.boot_clk_speed()); + + defmt::write!(f, "rsa_4_k_en: {=bool}, ", &self.rsa_4_k_en()); + + defmt::write!(f, "tzm_image_type: {}, ", &self.tzm_image_type()); + + defmt::write!(f, "psa_bstate_skip: {=bool}, ", &self.psa_bstate_skip()); + + defmt::write!(f, "psa_bstate_inc_keys: {=bool}, ", &self.psa_bstate_inc_keys()); + + defmt::write!(f, "redundant_spi_port: {}, ", &self.redundant_spi_port()); + + defmt::write!(f, "secure_boot_en: {}, ", &self.secure_boot_en()); + + defmt::write!(f, "dice_inc_otp: {=bool}, ", &self.dice_inc_otp()); + + defmt::write!(f, "dice_skip: {=bool}, ", &self.dice_skip()); + + defmt::write!(f, "boot_fail_pin_port: {=u8}, ", &self.boot_fail_pin_port()); + + defmt::write!(f, "boot_fail_pin_num: {=u8}, ", &self.boot_fail_pin_num()); + + defmt::write!(f, "}}"); + } + } + + impl core::ops::BitAnd for BootCfg0 { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } + } + + impl core::ops::BitAndAssign for BootCfg0 { + fn bitand_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l &= *r; + } + } + } + + impl core::ops::BitOr for BootCfg0 { + type Output = Self; + fn bitor(mut self, rhs: Self) -> Self::Output { + self |= rhs; + self + } + } + + impl core::ops::BitOrAssign for BootCfg0 { + fn bitor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l |= *r; + } + } + } + + impl core::ops::BitXor for BootCfg0 { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } + } + + impl core::ops::BitXorAssign for BootCfg0 { + fn bitxor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l ^= *r; + } + } + } + + impl core::ops::Not for BootCfg0 { + type Output = Self; + fn not(mut self) -> Self::Output { + for val in self.bits.iter_mut() { + *val = !*val; + } + self + } + } + + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct BootCfg1 { + /// The internal bits + bits: [u8; 4], + } + + impl ::device_driver::FieldSet for BootCfg1 { + const SIZE_BITS: u32 = 32; + fn new_with_zero() -> Self { + Self::new_zero() + } + fn get_inner_buffer(&self) -> &[u8] { + &self.bits + } + fn get_inner_buffer_mut(&mut self) -> &mut [u8] { + &mut self.bits + } + } + + impl BootCfg1 { + /// Create a new instance, loaded with the reset value (if any) + pub const fn new() -> Self { + Self { bits: [0, 0, 0, 0] } + } + /// Create a new instance, loaded with all zeroes + pub const fn new_zero() -> Self { + Self { bits: [0; 4] } + } + + ///Read the `qspi_reset_pin_enable` field of the register. + /// + /// Use QSPI_RESET_PIN to reset the flash device. + + pub fn qspi_reset_pin_enable(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 14, 15) }; + + raw > 0 + } + + ///Read the `qspi_reset_pin_port` field of the register. + /// + /// GPIO port to use for O/QSPI reset function. + + pub fn qspi_reset_pin_port(&self) -> u8 { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 15, 18) }; + + raw + } + + ///Read the `qspi_reset_pin_num` field of the register. + /// + /// GPIO pin number to use for O/QSPI reset function. + + pub fn qspi_reset_pin_num(&self) -> u8 { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 18, 23) }; + + raw + } + + ///Write the `qspi_reset_pin_enable` field of the register. + /// + /// Use QSPI_RESET_PIN to reset the flash device. + + pub fn set_qspi_reset_pin_enable(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 14, 15, &mut self.bits) }; + } + + ///Write the `qspi_reset_pin_port` field of the register. + /// + /// GPIO port to use for O/QSPI reset function. + + pub fn set_qspi_reset_pin_port(&mut self, value: u8) { + let raw = value; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 15, 18, &mut self.bits) }; + } + + ///Write the `qspi_reset_pin_num` field of the register. + /// + /// GPIO pin number to use for O/QSPI reset function. + + pub fn set_qspi_reset_pin_num(&mut self, value: u8) { + let raw = value; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 18, 23, &mut self.bits) }; + } + } + + impl From<[u8; 4]> for BootCfg1 { + fn from(bits: [u8; 4]) -> Self { + Self { bits } + } + } + + impl From for [u8; 4] { + fn from(val: BootCfg1) -> Self { + val.bits + } + } + + impl core::fmt::Debug for BootCfg1 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + let mut d = f.debug_struct("BootCfg1"); + + d.field("qspi_reset_pin_enable", &self.qspi_reset_pin_enable()); + + d.field("qspi_reset_pin_port", &self.qspi_reset_pin_port()); + + d.field("qspi_reset_pin_num", &self.qspi_reset_pin_num()); + + d.finish() + } + } + + #[cfg(feature = "defmt")] + impl defmt::Format for BootCfg1 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "BootCfg1 {{ "); + + defmt::write!(f, "qspi_reset_pin_enable: {=bool}, ", &self.qspi_reset_pin_enable()); + + defmt::write!(f, "qspi_reset_pin_port: {=u8}, ", &self.qspi_reset_pin_port()); + + defmt::write!(f, "qspi_reset_pin_num: {=u8}, ", &self.qspi_reset_pin_num()); + + defmt::write!(f, "}}"); + } + } + + impl core::ops::BitAnd for BootCfg1 { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } + } + + impl core::ops::BitAndAssign for BootCfg1 { + fn bitand_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l &= *r; + } + } + } + + impl core::ops::BitOr for BootCfg1 { + type Output = Self; + fn bitor(mut self, rhs: Self) -> Self::Output { + self |= rhs; + self + } + } + + impl core::ops::BitOrAssign for BootCfg1 { + fn bitor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l |= *r; + } + } + } + + impl core::ops::BitXor for BootCfg1 { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } + } + + impl core::ops::BitXorAssign for BootCfg1 { + fn bitxor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l ^= *r; + } + } + } + + impl core::ops::Not for BootCfg1 { + type Output = Self; + fn not(mut self) -> Self::Output { + for val in self.bits.iter_mut() { + *val = !*val; + } + self + } + } + + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct SecBootCfg5 { + /// The internal bits + bits: [u8; 2], + } + + impl ::device_driver::FieldSet for SecBootCfg5 { + const SIZE_BITS: u32 = 16; + fn new_with_zero() -> Self { + Self::new_zero() + } + fn get_inner_buffer(&self) -> &[u8] { + &self.bits + } + fn get_inner_buffer_mut(&mut self) -> &mut [u8] { + &mut self.bits + } + } + + impl SecBootCfg5 { + /// Create a new instance, loaded with the reset value (if any) + pub const fn new() -> Self { + Self { bits: [0, 0] } + } + /// Create a new instance, loaded with all zeroes + pub const fn new_zero() -> Self { + Self { bits: [0; 2] } + } + + ///Read the `revoke_rootkey` field of the register. + /// + /// Revoke upto 4 root keys. When a bit is set corresponding root key is revoked. + + pub fn revoke_rootkey(&self) -> u8 { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 0, 4) }; + + raw + } + + ///Read the `fa_mode_en` field of the register. + /// + /// Enable Fault Analysis mode. + /// - When set ROM checks and erases customer sensitive assets (AES keys or key codes) stored in IFR/OTP. + /// - Issues zeroized command to PUF (disables key decoding until POR). + /// - Blocks all HW routed OTP keys and set lock bits on those registers. + /// - Enables all debug ports and waits in loop for tester. + + pub fn fa_mode_en(&self) -> bool { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 4, 5) }; + + raw > 0 + } + + ///Read the `enable_crc_check` field of the register. + /// + /// Enable CRC checking of OTP words. + + pub fn enable_crc_check(&self) -> super::CrcCheck { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 5, 7) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `use_puf` field of the register. + /// + /// Use PUF to store AES keys and UDS. + + pub fn use_puf(&self) -> super::KeyIn { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 7, 8) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `puf_block_enroll` field of the register. + /// + /// Block further enrollement of PUF block. When this bit is set ROM blocks generation of new activation codes. + + pub fn puf_block_enroll(&self) -> super::Enroll { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 8, 9) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Read the `puf_block_set_key` field of the register. + /// + /// Block further enrollement of PUF block. When this bit is set ROM blocks generation of new key codes. + + pub fn puf_block_set_key(&self) -> super::KeyGen { + let raw = unsafe { ::device_driver::ops::load_lsb0::(&self.bits, 9, 10) }; + + unsafe { raw.try_into().unwrap_unchecked() } + } + + ///Write the `revoke_rootkey` field of the register. + /// + /// Revoke upto 4 root keys. When a bit is set corresponding root key is revoked. + + pub fn set_revoke_rootkey(&mut self, value: u8) { + let raw = value; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 0, 4, &mut self.bits) }; + } + + ///Write the `fa_mode_en` field of the register. + /// + /// Enable Fault Analysis mode. + /// - When set ROM checks and erases customer sensitive assets (AES keys or key codes) stored in IFR/OTP. + /// - Issues zeroized command to PUF (disables key decoding until POR). + /// - Blocks all HW routed OTP keys and set lock bits on those registers. + /// - Enables all debug ports and waits in loop for tester. + + pub fn set_fa_mode_en(&mut self, value: bool) { + let raw = value as _; + + unsafe { ::device_driver::ops::store_lsb0::(raw, 4, 5, &mut self.bits) }; + } + + ///Write the `enable_crc_check` field of the register. + /// + /// Enable CRC checking of OTP words. + + pub fn set_enable_crc_check(&mut self, value: super::CrcCheck) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 5, 7, &mut self.bits) }; + } + + ///Write the `use_puf` field of the register. + /// + /// Use PUF to store AES keys and UDS. + + pub fn set_use_puf(&mut self, value: super::KeyIn) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 7, 8, &mut self.bits) }; + } + + ///Write the `puf_block_enroll` field of the register. + /// + /// Block further enrollement of PUF block. When this bit is set ROM blocks generation of new activation codes. + + pub fn set_puf_block_enroll(&mut self, value: super::Enroll) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 8, 9, &mut self.bits) }; + } + + ///Write the `puf_block_set_key` field of the register. + /// + /// Block further enrollement of PUF block. When this bit is set ROM blocks generation of new key codes. + + pub fn set_puf_block_set_key(&mut self, value: super::KeyGen) { + let raw = value.into(); + + unsafe { ::device_driver::ops::store_lsb0::(raw, 9, 10, &mut self.bits) }; + } + } + + impl From<[u8; 2]> for SecBootCfg5 { + fn from(bits: [u8; 2]) -> Self { + Self { bits } + } + } + + impl From for [u8; 2] { + fn from(val: SecBootCfg5) -> Self { + val.bits + } + } + + impl core::fmt::Debug for SecBootCfg5 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + let mut d = f.debug_struct("SecBootCfg5"); + + d.field("revoke_rootkey", &self.revoke_rootkey()); + + d.field("fa_mode_en", &self.fa_mode_en()); + + d.field("enable_crc_check", &self.enable_crc_check()); + + d.field("use_puf", &self.use_puf()); + + d.field("puf_block_enroll", &self.puf_block_enroll()); + + d.field("puf_block_set_key", &self.puf_block_set_key()); + + d.finish() + } + } + + #[cfg(feature = "defmt")] + impl defmt::Format for SecBootCfg5 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "SecBootCfg5 {{ "); + + defmt::write!(f, "revoke_rootkey: {=u8}, ", &self.revoke_rootkey()); + + defmt::write!(f, "fa_mode_en: {=bool}, ", &self.fa_mode_en()); + + defmt::write!(f, "enable_crc_check: {}, ", &self.enable_crc_check()); + + defmt::write!(f, "use_puf: {}, ", &self.use_puf()); + + defmt::write!(f, "puf_block_enroll: {}, ", &self.puf_block_enroll()); + + defmt::write!(f, "puf_block_set_key: {}, ", &self.puf_block_set_key()); + + defmt::write!(f, "}}"); + } + } + + impl core::ops::BitAnd for SecBootCfg5 { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } + } + + impl core::ops::BitAndAssign for SecBootCfg5 { + fn bitand_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l &= *r; + } + } + } + + impl core::ops::BitOr for SecBootCfg5 { + type Output = Self; + fn bitor(mut self, rhs: Self) -> Self::Output { + self |= rhs; + self + } + } + + impl core::ops::BitOrAssign for SecBootCfg5 { + fn bitor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l |= *r; + } + } + } + + impl core::ops::BitXor for SecBootCfg5 { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } + } + + impl core::ops::BitXorAssign for SecBootCfg5 { + fn bitxor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l ^= *r; + } + } + } + + impl core::ops::Not for SecBootCfg5 { + type Output = Self; + fn not(mut self) -> Self::Output { + for val in self.bits.iter_mut() { + *val = !*val; + } + self + } + } + + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct Rkth { + /// The internal bits + bits: [u8; 32], + } + + impl ::device_driver::FieldSet for Rkth { + const SIZE_BITS: u32 = 256; + fn new_with_zero() -> Self { + Self::new_zero() + } + fn get_inner_buffer(&self) -> &[u8] { + &self.bits + } + fn get_inner_buffer_mut(&mut self) -> &mut [u8] { + &mut self.bits + } + } + + impl Rkth { + /// Create a new instance, loaded with the reset value (if any) + pub const fn new() -> Self { + Self { + bits: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + } + } + /// Create a new instance, loaded with all zeroes + pub const fn new_zero() -> Self { + Self { bits: [0; 32] } + } + } + + impl From<[u8; 32]> for Rkth { + fn from(bits: [u8; 32]) -> Self { + Self { bits } + } + } + + impl From for [u8; 32] { + fn from(val: Rkth) -> Self { + val.bits + } + } + + impl core::fmt::Debug for Rkth { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + let mut d = f.debug_struct("Rkth"); + + d.finish() + } + } + + #[cfg(feature = "defmt")] + impl defmt::Format for Rkth { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "Rkth {{ "); + + defmt::write!(f, "}}"); + } + } + + impl core::ops::BitAnd for Rkth { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } + } + + impl core::ops::BitAndAssign for Rkth { + fn bitand_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l &= *r; + } + } + } + + impl core::ops::BitOr for Rkth { + type Output = Self; + fn bitor(mut self, rhs: Self) -> Self::Output { + self |= rhs; + self + } + } + + impl core::ops::BitOrAssign for Rkth { + fn bitor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l |= *r; + } + } + } + + impl core::ops::BitXor for Rkth { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } + } + + impl core::ops::BitXorAssign for Rkth { + fn bitxor_assign(&mut self, rhs: Self) { + for (l, r) in self.bits.iter_mut().zip(&rhs.bits) { + *l ^= *r; + } + } + } + + impl core::ops::Not for Rkth { + type Output = Self; + fn not(mut self) -> Self::Output { + for val in self.bits.iter_mut() { + *val = !*val; + } + self + } + } + + /// Enum containing all possible field set types + pub enum FieldSetValue { + BootCfg0(BootCfg0), + + BootCfg1(BootCfg1), + + SecBootCfg5(SecBootCfg5), + + Rkth(Rkth), + } + impl core::fmt::Debug for FieldSetValue { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::BootCfg0(val) => core::fmt::Debug::fmt(val, f), + + Self::BootCfg1(val) => core::fmt::Debug::fmt(val, f), + + Self::SecBootCfg5(val) => core::fmt::Debug::fmt(val, f), + + Self::Rkth(val) => core::fmt::Debug::fmt(val, f), + + #[allow(unreachable_patterns)] + _ => unreachable!(), + } + } + } + + #[cfg(feature = "defmt")] + impl defmt::Format for FieldSetValue { + fn format(&self, f: defmt::Formatter) { + match self { + Self::BootCfg0(val) => defmt::Format::format(val, f), + + Self::BootCfg1(val) => defmt::Format::format(val, f), + + Self::SecBootCfg5(val) => defmt::Format::format(val, f), + + Self::Rkth(val) => defmt::Format::format(val, f), + } + } + } + + impl From for FieldSetValue { + fn from(val: BootCfg0) -> Self { + Self::BootCfg0(val) + } + } + + impl From for FieldSetValue { + fn from(val: BootCfg1) -> Self { + Self::BootCfg1(val) + } + } + + impl From for FieldSetValue { + fn from(val: SecBootCfg5) -> Self { + Self::SecBootCfg5(val) + } + } + + impl From for FieldSetValue { + fn from(val: Rkth) -> Self { + Self::Rkth(val) + } + } +} + +/// Primary boot Source. (a.k.a. Master boot source) + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum BootSrc { + /// ISP pins will determine boot source. + IspPinBoot = 0, + + /// Boot from Octal/Quad SPI flash device using FlexSpi channel A interface pins. + QpsiABoot = 1, + + /// Boot from eMMC device or SD card connected to SDHC0 port. + Sdhc0Boot = 2, + + /// Boot from eMMC device or SD card connected to SDHC0 port. + Sdhc1Boot = 3, + + /// Boot using SPI slave interface using master boot mode. + SpiSlvBoot = 4, + + /// Boot from Octal/Quad SPI flash device using FlexSpi channel B interface pins. Only load-to-RAM image are supported in this mode. + QspiBBoot = 5, + + /// Boot using UART interface using master boot mode. + UartBoot = 6, + + /// Boot from 1-bit SPI flash device from FlexCom interface pins selected by REDUNDANT_SPI_PORT field. Only load-to-RAM images are supported in this mode. + SpiFcBoot = 7, + + /// Always enter ISP mode. DEFAULT_ISP_MODE field will determine the ISP interface. + IspMode = 9, + + /// Boot from Octal/Quad SPI flash device using FlexSPI channel B interface pins. If image is not found check recovery boot using SPI-flash device through FlexComm. + QspiBRecBoot = 11, + + /// Boot from Octal/Quad SPI flash device using FlexSPI channel A interface pins. If image is not found check recovery boot using SPI-flash device through FlexComm. + QspiARecBoot = 12, + + /// Boot from SDHC0 port device. If image is not found check recovery boot using SPI-flash device through FlexComm. + Sdhc0RecBoot = 13, + + /// Boot from SDHC1 port device. If image is not found check recovery boot using SPI-flash device through FlexComm. + Sdhc1RecBoot = 15, +} + +impl core::convert::TryFrom for BootSrc { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::IspPinBoot), + + 1 => Ok(Self::QpsiABoot), + + 2 => Ok(Self::Sdhc0Boot), + + 3 => Ok(Self::Sdhc1Boot), + + 4 => Ok(Self::SpiSlvBoot), + + 5 => Ok(Self::QspiBBoot), + + 6 => Ok(Self::UartBoot), + + 7 => Ok(Self::SpiFcBoot), + + 9 => Ok(Self::IspMode), + + 11 => Ok(Self::QspiBRecBoot), + + 12 => Ok(Self::QspiARecBoot), + + 13 => Ok(Self::Sdhc0RecBoot), + + 15 => Ok(Self::Sdhc1RecBoot), + + val => Err(::device_driver::ConversionError { + source: val, + target: "BootSrc", + }), + } + } +} + +impl From for u8 { + fn from(val: BootSrc) -> Self { + match val { + BootSrc::IspPinBoot => 0, + + BootSrc::QpsiABoot => 1, + + BootSrc::Sdhc0Boot => 2, + + BootSrc::Sdhc1Boot => 3, + + BootSrc::SpiSlvBoot => 4, + + BootSrc::QspiBBoot => 5, + + BootSrc::UartBoot => 6, + + BootSrc::SpiFcBoot => 7, + + BootSrc::IspMode => 9, + + BootSrc::QspiBRecBoot => 11, + + BootSrc::QspiARecBoot => 12, + + BootSrc::Sdhc0RecBoot => 13, + + BootSrc::Sdhc1RecBoot => 15, + } + } +} + +/// When a valid image is not available to master boot, ROM switches to ISP mode for programming primary boot devices. This field determines the default ISP mode. + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum DefaultIspMode { + /// Auto detect ISP mode. ROM monitors USB, UART, SPI and I2C interfaces for any activity. + AutoIsp = 0, + + /// Support ISP command interface using USB HID class only. + UsbHidIsp = 1, + + /// Support ISP command interface on UART port only. + UartIsp = 2, + + /// Support ISP command interface on SPI port only. + SpiIsp = 3, + + /// Support ISP command interface on I2C port only. + I2CIsp = 4, + + /// Disable ISP fall through when proper image is not found on primary boot device. + DisableIsp = 7, +} + +impl core::convert::TryFrom for DefaultIspMode { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::AutoIsp), + + 1 => Ok(Self::UsbHidIsp), + + 2 => Ok(Self::UartIsp), + + 3 => Ok(Self::SpiIsp), + + 4 => Ok(Self::I2CIsp), + + 7 => Ok(Self::DisableIsp), + + val => Err(::device_driver::ConversionError { + source: val, + target: "DefaultIspMode", + }), + } + } +} + +impl From for u8 { + fn from(val: DefaultIspMode) -> Self { + match val { + DefaultIspMode::AutoIsp => 0, + + DefaultIspMode::UsbHidIsp => 1, + + DefaultIspMode::UartIsp => 2, + + DefaultIspMode::SpiIsp => 3, + + DefaultIspMode::I2CIsp => 4, + + DefaultIspMode::DisableIsp => 7, + } + } +} + +/// Defines clock speeds during boot. + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum BootClkSpeed { + /// Normal boot. All clocks are set to 48MHz using IRC48M, except USB block. USB block will use external XTAL clock. + NormalClk = 0, + + /// High-speed boot. + /// * Core clock is set to 198MHz using main_pll with IRC48M as input + /// * UART, I2C : 48MHz (IRC48M) + /// * SPI, SDHC: 198MHz (main_pll) + /// * USB: external XTAL + /// * OSPI: Set to differnet speed using aux0_pll. Speed of OSPI interface is obtained from Boot Configuration Block present on OSPI-flash device. + /// - SDR: 30/50/60/72/80/90/100 MHz + /// - DDR: 30/50/60/72/80 MHz + HispeedClk = 1, +} + +impl core::convert::TryFrom for BootClkSpeed { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::NormalClk), + + 1 => Ok(Self::HispeedClk), + + val => Err(::device_driver::ConversionError { + source: val, + target: "BootClkSpeed", + }), + } + } +} + +impl From for u8 { + fn from(val: BootClkSpeed) -> Self { + match val { + BootClkSpeed::NormalClk => 0, + + BootClkSpeed::HispeedClk => 1, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum TzmImageType { + /// TrustZone-M mode is determined by the image header. + TzmNormal = 0, + + /// Disable TrustZone-M features. ROM will always boot to a non-secure code and all TZ-M features are disabled. + TzmDisable = 1, + + /// TrustZone-M features are enabled. ROM will always boot to secure code. + TzmEnable = 2, + + /// TrustZone-M features are enabled and setting are loaded from image header and locked before branching to user code. + TzmPreset = 3, +} + +impl core::convert::TryFrom for TzmImageType { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::TzmNormal), + + 1 => Ok(Self::TzmDisable), + + 2 => Ok(Self::TzmEnable), + + 3 => Ok(Self::TzmPreset), + + val => Err(::device_driver::ConversionError { + source: val, + target: "TzmImageType", + }), + } + } +} + +impl From for u8 { + fn from(val: TzmImageType) -> Self { + match val { + TzmImageType::TzmNormal => 0, + + TzmImageType::TzmDisable => 1, + + TzmImageType::TzmEnable => 2, + + TzmImageType::TzmPreset => 3, + } + } +} + +/// FlexComm port to use for redundant SPI flash boot. + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum RedundantSpiPort { + /// Use FlexCom0 pins P0_0 (SCK), P0_1 (MISO), P0_2 (MOSI), P0_3 (SEL) + Fc0 = 0, + + /// Use FlexCom1 pins P0_7 (SCK), P0_8 (MISO), P0_9 (MOSI), P0_10 (SEL) + Fc1 = 1, + + /// Use FlexCom2 pins P0_14 (SCK), P0_15 (MISO), P0_16 (MOSI), P0_17 (SEL) + Fc2 = 2, + + /// Use FlexCom3 pins P0_21 (SCK), P0_22 (MISO), P0_23 (MOSI), P0_24 (SEL) + Fc3 = 3, + + /// Use FlexCom4 pins P0_28 (SCK), P0_29 (MISO), P0_30 (MOSI), P0_31 (SEL) + Fc4 = 4, + + /// Use FlexCom5 pins P1_3 (SCK), P1_4 (MISO), P1_5 (MOSI), P1_6 (SEL) + Fc5 = 5, + + /// Use FlexCom6 pins P3_25 (SCK), P3_26 (MISO), P3_27 (MOSI), P3_28 (SEL) + Fc6 = 6, + + /// Use FlexCom7 pins P4_0 (SCK), P4_1 (MISO), P4_2 (MOSI), P4_3 (SEL) + Fc7 = 7, +} + +impl core::convert::TryFrom for RedundantSpiPort { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::Fc0), + + 1 => Ok(Self::Fc1), + + 2 => Ok(Self::Fc2), + + 3 => Ok(Self::Fc3), + + 4 => Ok(Self::Fc4), + + 5 => Ok(Self::Fc5), + + 6 => Ok(Self::Fc6), + + 7 => Ok(Self::Fc7), + + val => Err(::device_driver::ConversionError { + source: val, + target: "RedundantSpiPort", + }), + } + } +} + +impl From for u8 { + fn from(val: RedundantSpiPort) -> Self { + match val { + RedundantSpiPort::Fc0 => 0, + + RedundantSpiPort::Fc1 => 1, + + RedundantSpiPort::Fc2 => 2, + + RedundantSpiPort::Fc3 => 3, + + RedundantSpiPort::Fc4 => 4, + + RedundantSpiPort::Fc5 => 5, + + RedundantSpiPort::Fc6 => 6, + + RedundantSpiPort::Fc7 => 7, + } + } +} + +/// Force secure image only. + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum SecureBoot { + Disabled = 0, + + Enabled = 1, +} + +impl Default for SecureBoot { + fn default() -> Self { + Self::Enabled + } +} + +impl From for SecureBoot { + fn from(val: u8) -> Self { + match val { + 0 => Self::Disabled, + + _ => Self::default(), + } + } +} + +impl From for u8 { + fn from(val: SecureBoot) -> Self { + match val { + SecureBoot::Disabled => 0, + + SecureBoot::Enabled => 1, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum CrcCheck { + Disable = 0, + + Enable = 1, + + NxpOnly = 2, + + Enable2 = 3, +} + +impl core::convert::TryFrom for CrcCheck { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::Disable), + + 1 => Ok(Self::Enable), + + 2 => Ok(Self::NxpOnly), + + 3 => Ok(Self::Enable2), + + val => Err(::device_driver::ConversionError { + source: val, + target: "CrcCheck", + }), + } + } +} + +impl From for u8 { + fn from(val: CrcCheck) -> Self { + match val { + CrcCheck::Disable => 0, + + CrcCheck::Enable => 1, + + CrcCheck::NxpOnly => 2, + + CrcCheck::Enable2 => 3, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum KeyIn { + Otp = 0, + + Ouf = 1, +} + +impl core::convert::TryFrom for KeyIn { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::Otp), + + 1 => Ok(Self::Ouf), + + val => Err(::device_driver::ConversionError { + source: val, + target: "KeyIn", + }), + } + } +} + +impl From for u8 { + fn from(val: KeyIn) -> Self { + match val { + KeyIn::Otp => 0, + + KeyIn::Ouf => 1, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum Enroll { + Enable = 0, + + Disable = 1, +} + +impl core::convert::TryFrom for Enroll { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::Enable), + + 1 => Ok(Self::Disable), + + val => Err(::device_driver::ConversionError { + source: val, + target: "Enroll", + }), + } + } +} + +impl From for u8 { + fn from(val: Enroll) -> Self { + match val { + Enroll::Enable => 0, + + Enroll::Disable => 1, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub enum KeyGen { + Enable = 0, + + Disable = 1, +} + +impl core::convert::TryFrom for KeyGen { + type Error = ::device_driver::ConversionError; + fn try_from(val: u8) -> Result { + match val { + 0 => Ok(Self::Enable), + + 1 => Ok(Self::Disable), + + val => Err(::device_driver::ConversionError { + source: val, + target: "KeyGen", + }), + } + } +} + +impl From for u8 { + fn from(val: KeyGen) -> Self { + match val { + KeyGen::Enable => 0, + + KeyGen::Disable => 1, + } + } +} diff --git a/libs/imxrt-rom/src/skboot.rs b/libs/imxrt-rom/src/skboot.rs new file mode 100644 index 00000000..072b7f92 --- /dev/null +++ b/libs/imxrt-rom/src/skboot.rs @@ -0,0 +1,107 @@ +//! Interface to the skboot ROM function 'skboot_authenticate'. + +use core::ptr::{null, null_mut}; + +use defmt_or_log::error; +#[cfg(feature = "rt")] +use embassy_imxrt::pac::interrupt; + +use crate::api::{api_table, BootStatus, KbAuthenticate, KbOperation, KbOptions, KbSettings, KbStatus, SecureBool}; + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AuthenticateError { + /// Failed to verify signature. + SignUnverified, + /// Failed to verify signature with unknown error. + SignUnknown, + /// Failed to authenticate image when parsing certificate header, certificate chain RKH or signature verification fails. + Fail, + /// Found an unexpected value in image. + UnexpectedValueInImage, + /// The keystore marker on the image is invalid. + KeyStoreMarkerInvalid, + /// The function passed an undefined return value. + BootStatusUnknown, + /// The function passed an undefined value as `is_sign_verified`` value. + IsSignVerifiedUnknown, +} + +/// Perform ROM authentication of an image. +/// +/// If RHK is provided it will use that hash to verify the certificate chain instead. +#[allow(dead_code)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub fn skboot_authenticate( + start: *const u32, + max_image_length: u32, + rhk: Option<[u8; 32]>, +) -> Result<(), AuthenticateError> { + // Note: + // The ROM reserved space for global variables in RAM on this device is: + // 0x1001_2000 to 0x1000_A000 + + // 43.9 Secure ROM API page 1282 of RT6xx User manual + + let mut session_ref = null_mut(); + let mut user_buf = [0u32; 1024]; + + let user_rhk = rhk.map(|rhk| rhk.as_ptr() as *const u32).unwrap_or(null()); + + let options = KbOptions { + version: 1, + buffer: user_buf.as_mut_ptr() as *mut u8, + buffer_length: core::mem::size_of_val(&user_buf) as u32, + op: KbOperation::AuthenticateImage, + settings: KbSettings { + authenticate: KbAuthenticate { + profile: 0, + min_build_number: 0, + max_image_length, + user_rhk, + }, + }, + }; + + let status = unsafe { (api_table().iap_driver.init)(&mut session_ref, &options) }; + if status != KbStatus::Success as u32 { + error!("kinit failed with {:?}", status); + return Err(AuthenticateError::Fail); + } + + // Placeholder value that will be mutated by skboot_authenticate. + let mut is_sign_verified: u32 = 0xffffffff; + let result = unsafe { (api_table().skboot.authenticate)(start, &mut is_sign_verified) }; + + // ROM API keeps HASHCRYPT unmasked + cortex_m::peripheral::NVIC::mask(embassy_imxrt::pac::Interrupt::HASHCRYPT); + + let status = unsafe { (api_table().iap_driver.deinit)(session_ref) }; + if status != KbStatus::Success as u32 { + error!("kdeinit failed with {:?}", status); + return Err(AuthenticateError::Fail); + } + + let status = BootStatus::try_from(result).map_err(|()| AuthenticateError::BootStatusUnknown)?; + let is_sign_verified = + SecureBool::try_from(is_sign_verified).map_err(|()| AuthenticateError::IsSignVerifiedUnknown); + + match status { + BootStatus::Success => match is_sign_verified { + Ok(SecureBool::TrackerVerified) => Ok(()), + Ok(SecureBool::False) => Err(AuthenticateError::SignUnverified), + _ => Err(AuthenticateError::SignUnknown), + }, + BootStatus::Fail => Err(AuthenticateError::Fail), + BootStatus::InvalidArgument => Err(AuthenticateError::UnexpectedValueInImage), + BootStatus::KeyStoreMarkerInvalid => Err(AuthenticateError::KeyStoreMarkerInvalid), + _ => Err(AuthenticateError::BootStatusUnknown), + } +} + +#[cfg(feature = "rt")] +#[interrupt] +#[allow(non_snake_case)] +fn HASHCRYPT() { + unsafe { (api_table().skboot.hashcrypt_irq_handler)() } +} diff --git a/link-imxrt.x b/link-imxrt.x deleted file mode 100644 index 1f0af1e5..00000000 --- a/link-imxrt.x +++ /dev/null @@ -1,270 +0,0 @@ -/* - * NOTE: The NXP ROM bootloader uses 0x00000--0x1BFFF. Therefore TEXT cannot be located - * there or on any of the overlays. - */ -MEMORY { - IRAM : ORIGIN = 0x0010E000, LENGTH = 24K - RAM : ORIGIN = 0x20116000, LENGTH = 32K - CONFIG_FLASH_OTFAD : ORIGIN = 0x08000000, LENGTH = 1024 - CONFIG_FLASH_FCB : ORIGIN = 0x08000400, LENGTH = 512 - CONFIG_FLASH_BIV : ORIGIN = 0x08000600, LENGTH = 4 - FLASH : ORIGIN = 0x08001000, LENGTH = 24K -} - -/* link descriptors at FLASH address after 24KB Bootloader Range */ -__bootable_region_descriptors_address = ORIGIN(FLASH) + LENGTH(FLASH); - -__bootloader_ivec_size = 0x130; /* */ - -/* # Entry point = reset vector */ -EXTERN(__RESET_VECTOR); -EXTERN(Reset); -ENTRY(Reset); - -EXTERN(DefaultHandler); - -PROVIDE(NonMaskableInt = DefaultHandler); -EXTERN(HardFaultTrampoline); -PROVIDE(MemoryManagement = DefaultHandler); -PROVIDE(BusFault = DefaultHandler); -PROVIDE(UsageFault = DefaultHandler); -PROVIDE(SecureFault = DefaultHandler); -PROVIDE(SVCall = DefaultHandler); -PROVIDE(DebugMonitor = DefaultHandler); -PROVIDE(PendSV = DefaultHandler); -PROVIDE(SysTick = DefaultHandler); - -PROVIDE(DefaultHandler = DefaultHandler_); -PROVIDE(HardFault = HardFault_); - -/* # Interrupt vectors */ -EXTERN(__INTERRUPTS); /* `static` variable similar to `__EXCEPTIONS` */ - -/* # Pre-initialization function */ -/* If the user overrides this using the `pre_init!` macro or by creating a `__pre_init` function, - then the function this points to will be called before the RAM is initialized. */ -PROVIDE(__pre_init = DefaultPreInit); - -/* # Sections */ -SECTIONS -{ - PROVIDE(_ram_start = ORIGIN(RAM)); - PROVIDE(_ram_end = ORIGIN(RAM) + LENGTH(RAM)); - PROVIDE(_stack_start = _ram_end); - - .otfad : { - . = ALIGN(4); - KEEP(* (.otfad)) - . = ALIGN(4); - } > CONFIG_FLASH_OTFAD - - .fcb : { - . = ALIGN(4); - KEEP(* (.fcb)) - . = ALIGN(4); - } > CONFIG_FLASH_FCB - - .biv : { - . = ALIGN(4); - KEEP(* (.biv)) - . = ALIGN(4); - } > CONFIG_FLASH_BIV - - /* ## Sections in IRAM */ - /* ### Vector table */ - .vector_table ORIGIN(IRAM) : - { - __vector_table = .; - - /* Initial Stack Pointer (SP) value. - * We mask the bottom three bits to force 8-byte alignment. - * Despite having an assert for this later, it's possible that a separate - * linker script could override _stack_start after the assert is checked. - */ - LONG(_stack_start & 0xFFFFFFF8); - - /* Reset vector */ - KEEP(*(.vector_table.reset_vector)); /* this is the `__RESET_VECTOR` symbol */ - - /* NMI vector */ - KEEP(*(.vector_table.nmi_vector)); - - /* HardFault vector */ - KEEP(*(.vector_table.hardfault_vector)); - - /* MemManage vector */ - KEEP(*(.vector_table.memmanage_vector)); - - /* BusFault vector */ - KEEP(*(.vector_table.busfault_vector)); - - /* UsageFault vector */ - KEEP(*(.vector_table.usagefault_vector)); - - /* SecureFault vector */ - KEEP(*(.vector_table.securefault_vector)); - - /* === image length === */ - LONG(__ro_image_size); - - /* === image type === */ - /* NOTE: this must be 0x000 (Plain image), as we're using the alternative "Plain Image" - layout for the IRAM loader here (by embedding in vtable) */ - LONG(0x0000); /* Image Type: 0x0000 - Plain image - 0x0001 - Plain signed image - 0x0002 - Plain CRC image - 0x0004 - Plain signed XIP image - 0x0005 - Plain CRC XIP image - 0x8001 - Plain signed image with KeyStore included */ - - /* === auth block/crc checksum === */ - LONG(0); - - /* svc vector */ - KEEP(*(.vector_table.svc_vector)); - - /* DebugMon vector */ - KEEP(*(.vector_table.debugmon_vector)); - - /* === image load address === */ - /* NOTE: IRAM bootloader will copy fIRAM 0x8001000 to this address */ - LONG(__vector_table); - - /* PendSV vector */ - KEEP(*(.vector_table.pendsv_vector)); - - /* SysTick vector */ - KEEP(*(.vector_table.systick_vector)); - - /* remaining iMXRT interrupts */ - /*KEEP(*(.vector_table.interrupts)); */ /* this is the `__INTERRUPTS` symbol */ - - . = ORIGIN(IRAM) + __bootloader_ivec_size; - . = ALIGN(4); - } > IRAM AT>FLASH =0x00 - - PROVIDE(_stext = ADDR(.vector_table) + SIZEOF(.vector_table)); - - /* ### .text */ - .text _stext : - { - __stext = .; - *(.Reset); - - *(.text .text.*); - - /* The HardFaultTrampoline uses the `b` instruction to enter `HardFault`, - so must be placed close to it. */ - *(.HardFaultTrampoline); - *(.HardFault.*); - - . = ALIGN(4); /* Pad .text to the alignment to workaround overlapping load section bug in old lld */ - __etext = .; - } > IRAM AT>FLASH - - /* ### .rodata */ - .rodata : ALIGN(4) - { - . = ALIGN(4); - __srodata = .; - *(.rodata .rodata.*); - - /* 4-byte align the end (VMA) of this section. - This is required by LLD to ensure the LMA of the following .data - section will have the correct alignment. */ - . = ALIGN(4); - __erodata = .; - } > IRAM AT>FLASH - - /* RO image size */ - __ro_image_size = (__edata - __sdata) + (__erodata - __srodata) + (__etext - __stext) + __bootloader_ivec_size; - - /* ## Sections in RAM */ - /* ### .data */ - .data : ALIGN(4) - { - . = ALIGN(4); - __sdata = .; - *(.data .data.*); - . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ - } > RAM AT>FLASH - - . = ALIGN(4); - __edata = .; - - /* LMA of .data */ - __sidata = LOADADDR(.data); - - /* ### .gnu.sgstubs - This section contains the TrustZone-M veneers put there by the Arm GNU linker. */ - /* Security Attribution Unit blocks must be 32 bytes aligned. */ - /* Note that this pads the FLASH usage to 32 byte alignment. */ - .gnu.sgstubs : ALIGN(32) - { - . = ALIGN(32); - __veneer_base = .; - *(.gnu.sgstubs*) - . = ALIGN(32); - } > FLASH - /* Place `__veneer_limit` outside the `.gnu.sgstubs` section because veneers are - * always inserted last in the section, which would otherwise be _after_ the `__veneer_limit` symbol. - */ - . = ALIGN(32); - __veneer_limit = .; - - /* ### .bss */ - .bss (NOLOAD) : ALIGN(4) - { - . = ALIGN(4); - __sbss = .; - *(.bss .bss.*); - *(COMMON); /* Uninitialized C statics */ - . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ - } > RAM - /* Allow sections from user `memory.x` injected using `INSERT AFTER .bss` to - * use the .bss zeroing mechanism by pushing __ebss. Note: do not change - * output region or load region in those user sections! */ - . = ALIGN(4); - __ebss = .; - - /* ### .uninit */ - .uninit (NOLOAD) : ALIGN(4) - { - . = ALIGN(4); - __suninit = .; - *(.uninit .uninit.*); - . = ALIGN(4); - __euninit = .; - } > RAM - - /* Place the heap right after `.uninit` in RAM */ - PROVIDE(__sheap = __euninit); - - /* Place stack end at the end of allocated RAM */ - PROVIDE(_stack_end = __euninit); - - /* ## .got */ - /* Dynamic relocations are unsupported. This section is only used to detect relocatable code in - the input files and raise an error if relocatable code is found */ - .got (NOLOAD) : - { - KEEP(*(.got .got.*)); - } - - /* ## Discarded sections */ - /DISCARD/ : - { - /* Unused exception related info that only wastes space */ - *(.ARM.exidx); - *(.ARM.exidx.*); - *(.ARM.extab.*); - } -} - -/* # Other checks */ -ASSERT(SIZEOF(.got) == 0, " -ERROR(cortex-m-rt): .got section detected in the input object files -Dynamic relocations are not supported. If you are linking to C code compiled using -the 'cc' crate then modify your build script to compile the C code _without_ -the -fPIC flag. See the documentation of the `cc::Build.pic` method for details."); -/* Do not exceed this mark in the error messages above | */ \ No newline at end of file diff --git a/src/imxrt/interrupts.rs b/src/imxrt/interrupts.rs deleted file mode 100644 index 6d335b3d..00000000 --- a/src/imxrt/interrupts.rs +++ /dev/null @@ -1,108 +0,0 @@ -#[link_section = ".vector_table.nmi_vector"] -#[used] -#[no_mangle] -static NMI_VECTOR: unsafe extern "C" fn() = nmi_vector; - -#[no_mangle] -unsafe extern "C" fn nmi_vector() { - loop {} -} - -/* HardFault vector */ -#[link_section = ".vector_table.hardfault_vector"] -#[used] -#[no_mangle] -static HARDFAULT_VECTOR: unsafe extern "C" fn() = hardfault_vector; - -#[no_mangle] -unsafe extern "C" fn hardfault_vector() { - loop {} -} - -/* MemManage vector */ -#[link_section = ".vector_table.memmanage_vector"] -#[used] -#[no_mangle] -static MEMMANAGE_VECTOR: unsafe extern "C" fn() = memmanage_vector; - -#[no_mangle] -unsafe extern "C" fn memmanage_vector() { - loop {} -} - -/* BusFault vector */ -#[link_section = ".vector_table.busfault_vector"] -#[used] -#[no_mangle] -static BUSFAULT_VECTOR: unsafe extern "C" fn() = busfault_vector; - -#[no_mangle] -unsafe extern "C" fn busfault_vector() { - loop {} -} - -/* UsageFault vector */ -#[link_section = ".vector_table.usagefault_vector"] -#[used] -#[no_mangle] -static USAGEFAULT_VECTOR: unsafe extern "C" fn() = usagefault_vector; - -#[no_mangle] -unsafe extern "C" fn usagefault_vector() { - loop {} -} - -/* SecureFault vector */ -#[link_section = ".vector_table.securefault_vector"] -#[used] -#[no_mangle] -static SECUREFAULT_VECTOR: unsafe extern "C" fn() = securefault_vector; - -#[no_mangle] -unsafe extern "C" fn securefault_vector() { - loop {} -} - -/* svc vector */ -#[link_section = ".vector_table.svc_vector"] -#[used] -#[no_mangle] -static SVC_VECTOR: unsafe extern "C" fn() = svc_vector; - -#[no_mangle] -unsafe extern "C" fn svc_vector() { - loop {} -} - -/* DebugMon vector */ -#[link_section = ".vector_table.debugmon_vector"] -#[used] -#[no_mangle] -static DEBUGMON_VECTOR: unsafe extern "C" fn() = debugmon_vector; - -#[no_mangle] -unsafe extern "C" fn debugmon_vector() { - loop {} -} - -/* PendSV vector */ -#[link_section = ".vector_table.pendsv_vector"] -#[used] -#[no_mangle] -static PENDSV_VECTOR: unsafe extern "C" fn() = pendsv_vector; - -#[no_mangle] -unsafe extern "C" fn pendsv_vector() { - loop {} -} - -/* SysTick vector */ -#[link_section = ".vector_table.systick_vector"] -#[used] -#[no_mangle] -static SYSTICK_VECTOR: unsafe extern "C" fn() = systick_vector; - -#[no_mangle] -unsafe extern "C" fn systick_vector() { - loop {} -} diff --git a/src/imxrt/mod.rs b/src/imxrt/mod.rs deleted file mode 100644 index 8c91e98a..00000000 --- a/src/imxrt/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod fcb; -mod interrupts; - -// auto-generated version information from Cargo.toml -#[cfg(feature = "imxrt")] -include!(concat!(env!("OUT_DIR"), "/biv.rs")); - -#[cfg(feature = "imxrt")] -#[link_section = ".otfad"] -#[used] -static OTFAD: [u8; 256] = [0x00; 256]; - -use ec_slimloader_descriptors::*; - -pub fn validate_crc(app_descriptor: &AppImageDescriptor) -> bool { - // TODO - integrate imxrt CRC hardware engine for computation - use crc::*; - - let crc_engine = Crc::::new(&CRC_32_ISO_HDLC); - let mut digest = crc_engine.digest(); - const VPAGE_SIZE: usize = 1024; // a chunk size "virtual page" that aligns with most page boundary multiples - let num_pages = (app_descriptor.image_size_bytes as usize).div_ceil(VPAGE_SIZE); - - // ... - for page in 0..num_pages { - // can't pass memory directly into digest API as requires WIDE pointer type (meta data for slice) - let mut page_slice = [0u8; VPAGE_SIZE]; - - let remaining = (app_descriptor.image_size_bytes as usize).saturating_sub(page * VPAGE_SIZE); - - for (i, local_copy) in page_slice[0..remaining].iter_mut().enumerate() { - *local_copy = unsafe { *(app_descriptor.stored_address as *const u8).add(i + page * VPAGE_SIZE) }; - } - - digest.update(&page_slice); - } - - let image_crc = unsafe { *(app_descriptor.stored_crc_address as *const u32) }; - - image_crc == digest.finalize() -} - -pub unsafe fn raw_copy_to_ram(from: *const u32, to: *mut u32, len_words: usize) { - core::ptr::copy_nonoverlapping(from, to, len_words); -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 1c62ede9..00000000 --- a/src/main.rs +++ /dev/null @@ -1,189 +0,0 @@ -#![no_std] -#![no_main] - -#[cfg(feature = "defmt")] -use defmt_rtt as _; -use panic_probe as _; - -#[cfg(feature = "imxrt")] -mod imxrt; - -#[cfg(feature = "imxrt")] -use imxrt::{raw_copy_to_ram, validate_crc}; - -#[cfg(not(feature = "imxrt"))] -mod unsupported { - use super::*; - - pub unsafe fn raw_copy_to_ram(_from: *const u32, _to: *mut u32, _len_words: usize) {} - pub fn validate_crc(_app_descriptor: &AppImageDescriptor) -> bool { - true - } -} -#[cfg(not(feature = "imxrt"))] -use unsupported::*; - -fn copy_image(_app_descriptor: &AppImageDescriptor) { - // TODO allow other scenarios supported from bootloader aside from copy to RAM - unsafe { - raw_copy_to_ram( - _app_descriptor.stored_address as *const u32, - _app_descriptor.execution_address as *mut u32, - _app_descriptor.execution_copy_size_bytes as usize / core::mem::size_of::(), - ); - } -} - -#[cfg(feature = "defmt")] -fn dump_descriptor_header(descriptor_header_address: *const u32) { - let mut memory_copy = [0u32; core::mem::size_of::() / core::mem::size_of::()]; - for (i, b) in memory_copy.iter_mut().enumerate() { - *b = unsafe { *descriptor_header_address.add(i) }; - } - - defmt::info!("Descriptor Header Bytes: {:x}", memory_copy); -} - -#[cfg(feature = "defmt")] -fn dump_app_descriptor(app_descriptor_address: *const u32) { - let mut memory_copy = [0u32; core::mem::size_of::() / core::mem::size_of::()]; - for (i, b) in memory_copy.iter_mut().enumerate() { - *b = unsafe { *app_descriptor_address.add(i) }; - } - - defmt::info!("App Descriptor Bytes: {:x}", memory_copy); -} - -use ec_slimloader_descriptors::*; - -extern "C" { - static __bootable_region_descriptors_address: u32; -} - -#[cortex_m_rt::pre_init] -unsafe fn pre_init() { - cortex_m::interrupt::disable(); -} - -#[cortex_m_rt::entry] -fn main() -> ! { - #[cfg(feature = "defmt")] - defmt::info!("Bootloader: Initializing Hardware."); - - let boot_descriptors_address = unsafe { &__bootable_region_descriptors_address as *const u32 }; - - #[cfg(feature = "defmt")] - defmt::info!( - "Bootloader: Fetching App Descriptors from {:X}.", - boot_descriptors_address - ); - - // TODO error handling - // ? should the bootloader be responsible for re-formatting? This may be a security decision - let descriptors = match BootableRegionDescriptors::from_address(boot_descriptors_address) { - Ok(descriptors) => descriptors, - Err(_e) => { - #[cfg(feature = "defmt")] - defmt::error!( - "Invalid boot region descriptors: ParseError |{}|", - match _e { - ParseError::InvalidSignature => "Invalid Header Signature", - ParseError::InvalidAppSlot => "Invalid App Slot", - ParseError::InvalidSlotCount => "Invalid Slot Count", - _ => "CRC Error", - } - ); - - #[cfg(feature = "defmt")] - match _e { - ParseError::InvalidHeaderCrc { found, expected } => { - defmt::error!("Header CRC found = {:x}, expected = {:x}", found, expected); - dump_descriptor_header(boot_descriptors_address); - } - ParseError::InvalidAppCrc { - address, - found, - expected, - } => { - defmt::error!("App @{:x} CRC found = {:x}, expected = {:x}", address, found, expected); - dump_app_descriptor(address); - } - _ => (), - } - - loop { - cortex_m::asm::wfi(); - } - } - }; - - let active_app_descriptor = descriptors.get_active_slot(); - - #[cfg(feature = "defmt")] - { - let slot = active_app_descriptor.app_slot_number; - defmt::info!("Bootloader: Active App slot is {}", slot); - } - - let boot_image = if active_app_descriptor.flags & APP_IMAGE_FLAG_SKIP_IMAGE_CRC_CHECK != 0 { - #[cfg(feature = "defmt")] - defmt::info!("Bootloader: skipping image CRC due to SKIP_IMAGE_CRC_CHECK flag."); - - true - } else { - #[cfg(feature = "defmt")] - defmt::info!("Bootloader: validating image CRC."); - - validate_crc(&active_app_descriptor) - }; - - // validate integrity of app image - if boot_image { - // note that use of this flag requires proper app image address backing for .text section-- ensure you have linked the app against RAM addresses! (Or other required adjustments) - if active_app_descriptor.flags & APP_IMAGE_FLAG_COPY_TO_EXECUTION_ADDRESS != 0 { - #[cfg(feature = "defmt")] - defmt::info!("Bootloader: Performing image copy to execution location."); - - copy_image(&active_app_descriptor); - } - - #[cfg(feature = "defmt")] - { - let xaddr = active_app_descriptor.execution_address; - defmt::info!( - "Bootloader: Validation complete and branching to application execution address {:x}.", - xaddr - ); - } - - // branch to location as described by descriptor - unsafe { - // set the VTOR - cortex_m::Peripherals::steal() - .SCB - .vtor - .write(active_app_descriptor.execution_address); - - // enable interrupts - cortex_m::interrupt::enable(); - - // perform the branch - // NOTE: two separate steps are used here for easier debug inspection - let p_sp = active_app_descriptor.execution_address as *const u32; - let p_rv = (active_app_descriptor.execution_address as *const u32).add(1); - - let sp = *p_sp; - let rv = *p_rv; - - cortex_m::asm::bootstrap(sp as *const u32, rv as *const u32); - } - } else { - // TODO any boot recovery logic - #[cfg(feature = "defmt")] - defmt::error!("Active App CRC checksum failure!"); - - loop { - cortex_m::asm::wfi(); - } - } -}