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:
- Model the security path — simulated Nordic nRF52840 + secure-element-shaped peripheral, firmware that provisions OTP-style state, verifies signed OTA, rejects rollback.
- Assert it in CI — bit-for-bit checks on UART, UICR, AES goldens, SE verdicts (not only “it printed OK”).
- 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.
| Mechanism | What runs | Caveat |
|---|---|---|
| OTP root key | UICR 1→0 | Sim UICR, not silicon OTP |
| Boot check | AES-ECB vs golden | Demo challenge, not full measured boot |
| SE / OTA | Real P-256 ECDSA | ATECC command shape, not full datasheet |
| Anti-rollback | Monotonic counter | Same UICR model |
| APPROTECT | Value written | Stored, not enforced |
Playground
Same firmware drives an SSD1306 on the shared I²C bus:
Evidence pack contents
LabWired/labwired-cra-evidence uploads artifact cra-evidence-pack on green:
| File | What it is |
|---|---|
claims.json / claims.md | Claims tied to passed UART + memory assertions |
run-manifest.json | Inputs + results digest |
run-manifest.digest + .sig | Digest + OpenSSL ECDSA detached sig |
pack-signing-pubkey.pem | Public half of the pack-signing key for that run |
result.json, uart.log, junit.xml | Raw lab outputs |
oem-verify-pubkey.hex | OEM OTA verify pubkey for that run |
limitations.md | Sim 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
- CI runs
make_packages.py --ephemeral - OpenSSL makes a P-256 keypair in a temp dir
- Signs three packages (good v2, old v1, forged v3)
- Public key is set as
oem_pubkey_hexon the SE insystem.yaml - Temp dir (and private key) is discarded
- Pack retains
oem-verify-pubkey.hexonly
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:
- labwired-core — engine, peripherals, secure-boot lab
- labwired-cra-evidence — claims packaging, ephemeral-key CI, artifact upload
Keeps compliance packaging out of engine PRs.
Limitations
- Sim RNG is a deterministic PRNG (CI-friendly, not entropy proof)
- Boot check is AES challenge, not a full measured-boot chain
- APPROTECT is written, not a full debug lockout model
- SE is command shape + real ECDSA, not pin-compatible silicon
- Pack-signing key is per-run, not long-lived PKI
- No SBOM, vuln process, or support-period docs in this pack
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