LabWired The easy way to build hardware

CRA secure-boot evidence in CI

Category: Security

CRA (and UNECE R155/R156) expect you to show that secure boot, signed updates, and anti-rollback still work after every relevant change — not that someone once tried them on a bench.

LabWired is how you put that check on rails: open-source virtual boards, real firmware ELFs, same model in the Playground and headless in CI. For this example we use it like this:

  1. Model the security path — simulated Nordic nRF52840 + secure-element-shaped peripheral, firmware that provisions OTP-style state, verifies signed OTA, rejects rollback.
  2. Assert it in CI — bit-for-bit checks on UART, UICR, AES goldens, SE verdicts (not only “it printed OK”).
  3. Export a pack — each green run of labwired-cra-evidence uploads claims, logs, a signed run-manifest, and public keys only. Private OEM keys are generated in the job and discarded.

That pack is supporting material for a technical file (mechanisms exercised, reproducible). It is not a Notified Body certificate by itself.

Why one-off bench runs are weak evidence

“Engineer tried it on a board in March” does not survive the next firmware merge. CRA-style evidence needs a repeatable pipeline: green CI → folder you can re-check (assertions, logs, digests) → no OEM private keys in the monorepo.

The lab

Source: labwired-core examples/nrf52840-secure-boot-lab — simulated nRF52840, ATECC608A-shaped SE on I²C, open firmware, 45 bit-for-bit assertions.

Boot 1 — provision. UICR (OTP-style) is erased. Firmware takes a root key from the sim RNG (deterministic in CI), burns it into UICR CUSTOMER, sets anti-rollback to v1, writes APPROTECT, prints ROT: PROVISIONED, reboots.

Boot 2 — boot check + signed OTA. AES-128-ECB challenge vs OpenSSL golden → SECURE BOOT OK (v1). UART delivers a 140-byte update; firmware SHA-256s it; SE checks OEM ECDSA P-256 against the pubkey in data slot 0. Private key stays off device. Commit to flash, counter advances, reboot.

Boot 3 — enforce + attest. As v2: re-hash slot (UPDATE SLOT VERIFIED); old signed v1 → ROLLBACK REJECTED; forged v3 → BAD SIGNATURE REJECTED; SE sign/verify → ATTESTATION OK.

UART from a green run:

ROT: PROVISIONED
SECURE BOOT OK (v1)
OTA v2 SIGNATURE OK
OTA v2 COMMITTED
SECURE BOOT OK (v2)
ANTI-ROLLBACK v2 ACTIVE
UPDATE SLOT VERIFIED
ROLLBACK REJECTED
BAD SIGNATURE REJECTED
ATTESTATION OK

The smoke script also pins UICR words, AES ciphertext goldens, SE flags, and package digests in RAM. String match alone would be too weak for CI.

MechanismWhat runsCaveat
OTP root keyUICR 1→0Sim UICR, not silicon OTP
Boot checkAES-ECB vs goldenDemo challenge, not full measured boot
SE / OTAReal P-256 ECDSAATECC command shape, not full datasheet
Anti-rollbackMonotonic counterSame UICR model
APPROTECTValue writtenStored, not enforced

Playground

Same firmware drives an SSD1306 on the shared I²C bus:

Open full playground →

Evidence pack contents

LabWired/labwired-cra-evidence uploads artifact cra-evidence-pack on green:

FileWhat it is
claims.json / claims.mdClaims tied to passed UART + memory assertions
run-manifest.jsonInputs + results digest
run-manifest.digest + .sigDigest + OpenSSL ECDSA detached sig
pack-signing-pubkey.pemPublic half of the pack-signing key for that run
result.json, uart.log, junit.xmlRaw lab outputs
oem-verify-pubkey.hexOEM OTA verify pubkey for that run
limitations.mdSim gaps, in the pack

Claim IDs look like otp_root_key_provisioned, aes_boot_challenge, ecdsa_ota_verify. They must match assertions already true in result.json. They are not an Annex I legal opinion.

Ephemeral keys

  1. CI runs make_packages.py --ephemeral
  2. OpenSSL makes a P-256 keypair in a temp dir
  3. Signs three packages (good v2, old v1, forged v3)
  4. Public key is set as oem_pubkey_hex on the SE in system.yaml
  5. Temp dir (and private key) is discarded
  6. Pack retains oem-verify-pubkey.hex only

A second throwaway key signs the run-manifest digest; only its public key is kept. Production would use HSM/KMS and a long-lived org key — the demo is the pipeline shape, not production PKI.

If private-key PEM shows up in either repo, that is a bug: file an issue.

Separate repo

Same split as udslib and labwired-nokia-ci-demo:

Keeps compliance packaging out of engine PRs.

Limitations

How to run

Evidence pack:

git clone https://github.com/LabWired/labwired-cra-evidence
cd labwired-cra-evidence
./scripts/run_evidence.sh
# → out/.../cra-evidence-pack/

Or download cra-evidence-pack from a green Actions run.

Lab only (from labwired-core):

cargo build -p firmware-nrf52840-secure-boot --release --target thumbv7em-none-eabi
cargo run -q -p labwired-cli -- test \
  --script examples/nrf52840-secure-boot-lab/secure-boot-smoke.yaml \
  --output-dir out/nrf52840-secure-boot

Repo: LabWired/labwired-cra-evidence.

Andrii Shylenko
Andrii Shylenko

Founder, LabWired.