Production setup — signed boot + OTP master key
⚠️ EXPERIMENTAL — IRREVERSIBLE — BRICK RISK. Everything on this page burns one-time-programmable fuses or changes what the chip will ever boot again. A mistake can permanently brick the board or permanently lose your enrolled credentials. Read the whole page before running anything. The tools refuse to act without typed confirmations and support
--dry-run— use it.
Out of the box, RS-Key’s at-rest encryption roots in a key derived on the device and stored sealed in flash. That stops casual key extraction, but a sufficiently motivated attacker who steals the board can dump flash over BOOTSEL and grind offline. The production path closes that, in two independent stages:
- OTP master key (MKEK) — fuse a random 32-byte key into RP2350 OTP page 58 and re-root all at-rest sealing in it, then hard-lock the page so neither BOOTSEL nor non-secure code can ever read it. A flash dump alone is now worthless.
- Secure boot — fuse your public-key fingerprint and the
SECURE_BOOT_ENABLEbit so the bootrom runs only images you signed. Attacker-flashed firmware (the remaining way to read the OTP key) no longer runs.
A third, optional stage builds on those: anti-rollback, so that old images you signed — with bugs you have since fixed — stop booting too. It has its own page: anti-rollback.md.
Each stage is usable alone; together they are the full story. All are driven
from the host — the firmware never burns a fuse behind your back. The two
exceptions are rows that physically cannot be written from BOOTSEL
(bootloader-read-only OTP pages): the page-58 lock and the
ROLLBACK_REQUIRED flag, each applied by the firmware on explicit command. The
fuses these stages write are explained in otp-fuses.md.
flowchart TD
d["Default<br/>flash-derived root · any image boots"]
d --> s1["Stage 1 — OTP master key<br/>burn page 58 · migrate · lock"]
s1 --> s2a["Stage 2 — secure boot<br/>load-key · harden (non-enforcing)"]
s2a --> s2b{{"ENABLE<br/>the one irreversible bit"}}
s2b --> s2c["lock key slots + fuse pages"]
s2c --> s3["Stage 3 — anti-rollback<br/>(optional)"]
s2b -. "a correctly-signed UF2 can always be re-flashed over BOOTSEL" .-> rec["BOOTSEL recovery"]
Before you start
- Make a seed backup first if you haven’t —
rsk backup export(guides/seed-backup.md). It is the only thing that survives a board swap, and the production path is exactly when you start caring about that. - Plan for the signing key. Stage 2 generates an ECDSA key; losing it bricks the board for new firmware. Decide now where you’ll keep it durably.
- Understand the substrate. These stages write OTP fuses; otp-fuses.md explains what is irreversible and why.
- Rehearse. Every step supports
--dry-run, which prints the exactpicotoolcommands without touching anything.
Stage 1 — OTP master key
What it does: writes a random DEVK (device attestation key) and MKEK (master sealing key) plus anti-imaging chaff into OTP page 58, ECC-verified, then locks the page. On the next boot the firmware notices the provisioned key and migrates everything already on the device — FIDO seed, PIV keys, OpenPGP key wraps, PIN verifiers — under the new root. Your enrolled credentials survive; that is the point of the migration layer.
rsk reboot bootsel # picotool needs the chip in BOOTSEL
rsk otp burn --dry-run # preview every step
rsk otp burn # typed confirmation; keys are generated and FORGOTTEN
picotool reboot -a # back to the app; migration runs at boot
rsk otp lock-page58 # firmware applies the page-58 hard lock (typed confirm)
Facts to internalize first:
- The burn tool generates MKEK/DEVK randomly and forgets them — there is no copy to lose, and none to back up. The fuses are the key.
- After the burn, the device attestation public key changes (it now derives from the fused DEVK). FIDO/PIV identities survive; the rescue attestation key does not — expected, not data loss. If you use audit or fleet verification, re-record the device’s fingerprint afterward (guides/fleet.md).
- The lock is the half that matters for at-rest. Burning the MKEK without
lock-page58leaves the key fused but still readable over BOOTSEL — the flash dump is not yet worthless. Runlock-page58to actually close it. - After
lock-page58,picotool otp geton page 58 fails with a permission error forever. Only the secure-mode firmware can read the keys. That failure is the lock working. - A seed backup (
rsk backup) made before or after is unaffected — backups carry the seed value, which gets re-sealed under whatever root the device has. - Never flash a
FAKE_MKEKtest build onto a provisioned board. It migrates the data under a fake, greppable key and orphans it (build.md).
Stage 2 — secure boot
What it does: the RP2350 bootrom verifies an ECDSA (secp256k1 + SHA-256) signature on every image against a fingerprint fused into OTP. Unsigned or foreign-signed images do not boot — the chip falls back to BOOTSEL, where you can always drag a correctly-signed UF2 (recovery path).
flowchart TD
build["cargo / nix build"] --> uf2["firmware.uf2"]
uf2 --> seal["picotool seal --sign<br/>signing key (host-only)"]
seal --> signed["firmware-signed.uf2"]
signed --> flash["BOOTSEL flash"]
flash --> rom{"bootrom: signature<br/>vs fused fingerprint"}
rom -->|verified| run["run the app"]
rom -->|rejected| bootsel["fall back to BOOTSEL<br/>(re-flash a signed image)"]
The permanent consequences:
- Every future flash must be signed with your key. The dev loop becomes
build →
picotool seal --sign→ flash. - Losing the signing key bricks the board for new firmware (the current signed image keeps booting). Back the key up before enabling enforcement.
DEBUG_DISABLEis burned along the way — SWD is gone (flashing is BOOTSEL anyway).
2a. Generate a signing key (once, off-repo)
mkdir -p ~/.rs-key-secrets && cd ~/.rs-key-secrets
openssl ecparam -genkey -name secp256k1 -noout -out secure_boot_key.pem
openssl ec -in secure_boot_key.pem -pubout -out secure_boot_pub.pem
chmod 600 secure_boot_key.pem
# BACK IT UP somewhere that survives this machine.
This key is the root of trust for the board’s whole life. Treat it like the most
important secret you have here: if you lose it after enable, you can never
flash new firmware to that board (the running image keeps booting). It is also
what makes key rotation
possible later, so a single, well-kept key with a backup is the goal. The full
key lifecycle — passphrase protection (and the decrypt-for-seal step it
implies), backup, one fresh key per board, rotation, and recovery — is in
signing-keys.md.
2b. Sign and prove a signed image boots (before any fuse)
picotool seal --sign --hash firmware.uf2 firmware-signed.uf2 \
~/.rs-key-secrets/secure_boot_key.pem ~/.rs-key-secrets/otp_secureboot.json \
--major 1 --minor 0 --rollback 1
picotool info firmware-signed.uf2 # must say "signature: verified"
# flash firmware-signed.uf2 over BOOTSEL and confirm the device works
The arguments:
secure_boot_key.pem— your signing key; it signs the image.otp_secureboot.json—sealwrites the boot-key fingerprint here; you fuse it into OTP withload-keybelow.--major/--minor— an image version (major.minor) stamped into the RP2350 boot metadata. It is a plain version label, distinct from the firmware version RS-Key reports (5.7.x, build.md) and from the rollback version. The bootrom can use it to prefer the newer of two images in an A/B setup; RS-Key ships a single image, so here it is effectively a label — keep1 0.--rollback— the anti-rollback version, a separate counter (not the image version). It is harmless before anti-rollback is enabled, and having a version in every sealed image from day one makes that stage cheap. What it means and how to choose it: anti-rollback.md.
picotool info firmware-signed.uf2 must report signature: verified. The
firmware’s image definition is already secure-boot compatible; the sealed UF2
carries the signature block.
2c. Burn, staged
rsk secure-boot splits provisioning so every irreversible write is proven
by a real boot before the next, and the only true point of no return is one
bit:
rsk secure-boot status # read the current fuse state any time
rsk secure-boot load-key # 1. boot-key fingerprint + KEY_VALID (non-enforcing)
rsk secure-boot harden # 2. DEBUG_DISABLE + glitch detectors (non-enforcing)
rsk secure-boot enable # 3. SECURE_BOOT_ENABLE = 1 ← the brick bit
rsk secure-boot lock # 4. revoke unused key slots + lock the fuse pages
Each step has --dry-run and a typed confirmation. Between steps, reboot and
confirm the device still works. After enable, verify the negative case:
drag an unsigned UF2 — the bootrom must reject it and fall back to BOOTSEL;
re-drag the signed one to recover.
lockand key rotation — a decision to make now. Thelockstage revokes the three unused boot-key slots (KEY_INVALID), maximizing hardening against an attacker who tries to inject their own key. It also forecloses key rotation — the escape valve if you ever exhaust the 48-step anti-rollback budget (you rotate to a new signing key and revoke the old one). If you want to keep that valve, don’t run the fulllock; leave a slot. The trade-off is in anti-rollback.md. Most users should run the fulllock— you will almost certainly never reach the ceiling.
The new flash workflow (forever)
cargo build --release -p firmware
picotool uf2 convert target/thumbv8m.main-none-eabihf/release/firmware -t elf firmware.uf2
picotool seal --sign --hash firmware.uf2 firmware-signed.uf2 \
~/.rs-key-secrets/secure_boot_key.pem ~/.rs-key-secrets/otp_secureboot.json \
--major 1 --minor 0 --rollback 1
# flash firmware-signed.uf2 (BOOTSEL, or: rsk reboot bootsel && cp)
The --rollback value is your board’s current floor (see
anti-rollback.md); 1 is the usual starting value.
To seal an image others can independently verify, build it with
nix build .#firmware instead of the dev-shell cargo build — that path is
bit-for-bit reproducible from the source tree
(build.md), so anyone can rebuild
at your release commit and confirm the payload you signed.
Stage 3 — anti-rollback (optional)
This stage stops your own older signed images from booting, so a bug you have since fixed can’t be re-introduced by downgrading. Read anti-rollback.md first — it is the full model: how the floor works, the 48-burn budget, when (and whether) to raise it, what to do at the ceiling, and the new-board case. This section is only the steps to turn it on.
The mechanism is two RP2350-native pieces: a per-image rollback version
(--rollback N at seal time) checked against a 48-bit OTP thermometer, and the
ROLLBACK_REQUIRED fuse that makes that check mandatory. Until the fuse is
burned, versionless images boot and the feature is off.
Turning it on
- Seal and flash firmware with a rollback version (start at
--rollback 1), reboot, and confirmrsk secure-boot statusreportsboot version 1/48. If it still reads0/48, stop and investigate — never burn the fuse on an unproven setup. - Re-seal
rsk-wipeat the same version — the recovery escape hatch must stay bootable. - From the running firmware:
rsk otp rollback-require(typed confirmation;--dry-runreports state without burning). The firmware refuses unless secure boot is enabled — so it can only run from an image that itself passed the rollback check, and the “fuse before a versioned image” footgun can’t happen. - Negative-test: drag any versionless signed UF2 — the bootrom must refuse it and fall back to BOOTSEL; re-drag the current image to recover.
After it’s on
- Every
picotool sealmust include--rollback <your floor>. A versionless sealed image no longer boots — fail-closed; you find out at flash time and recover by re-sealing. - To raise the floor and close a downgrade-fix, seal one step higher
(
--rollback <floor + 1>). The default is not to raise it. When, why, and the budget math are all in anti-rollback.md. - A board that has burned to floor N never boots an image below N again. No undo.
Everything about whether to bump, the 48-budget, the ceiling, key rotation, and moving to a new board lives in anti-rollback.md.
Recovery and failure cases
| Situation | What happens / what to do |
|---|---|
| Bad or wrong flash | BOOTSEL stays enabled — drag a correctly-signed UF2 to recover. |
| Unsigned / foreign-signed image (secure boot on) | Bootrom refuses it, falls back to BOOTSEL. Drag your signed image. |
| Lost the signing key (secure boot on) | The current signed image keeps booting, but you can never flash new firmware. There is no recovery for the key — back it up before enable. |
| Image sealed below your rollback floor | Refused at boot → BOOTSEL. Re-seal at ≥ your floor. |
| Image sealed above your floor by accident | It boots and burns the thermometer up to it, spending budget irreversibly. Seal at exactly your floor unless you mean to raise it. |
rsk-wipe won’t boot after enabling anti-rollback | Re-seal it at your current floor — the recovery image must carry a version too. |
| 48-step rollback budget exhausted | Key rotation, or a new board — see anti-rollback.md. |
Page 58 read fails after lock-page58 | That’s the lock working — only secure firmware can read the keys now. |
| Replacing the board entirely | Provision the new chip with a new signing key; restore your FIDO identity with rsk backup restore. Resident passkeys / OpenPGP / PIV don’t migrate — see anti-rollback.md. |
Deliberate choices
- USB BOOTSEL stays enabled. It is the only reflash and the only recovery path (no debugger), it cannot bypass signature enforcement, and after the page-58 lock it cannot read the OTP keys. Disabling it (the datasheet’s full checklist) would turn every bad flash into a permanent brick.
- No image encryption. The code is open source — there is nothing secret in the image (secrets live sealed in flash, rooted in OTP). The RP2350 also has no transparent XIP decryption; encrypted boot requires fitting the image in SRAM, which a ~1.7 MB image does not.
Residual risks (still open after all stages)
- XIP TOCTOU: the image executes from external QSPI flash; hardware that swaps or emulates the flash chip between the bootrom’s signature check and execution can subvert it. Decap/side-channel-class attack, out of scope.
- A host compromised while the device is plugged in can drive normal operations (as with any security key); see threat-model.md.