What RS-Key defends against, what it deliberately does not, and the honest residuals in between. The defenses compose in tiers — each one assumes the ones before it.
The FIDO master seed (every non-resident credential derives from it), resident passkeys, OpenPGP private keys and their DEK chain, PIV private keys, OATH secrets, OTP slot secrets, PINs.
Everything arriving over USB is attacker-controlled: CTAPHID frames, the CCID bulk stream, ISO-7816 APDUs, CTAP2 CBOR. Defenses:
- Memory safety.
no_stdRust end to end; the parsers and applet dispatch are safe code. The handful ofunsafesites are enumerated and justified in unsafe.md. - Fuzzing. Every parser and every applet's full dispatch path has a
cargo-fuzztarget (30+); see testing.md. - Protocol gates. PINs/UV with retry counters and lockout, physical-touch requirements on FIDO operations and OpenPGP UIF, OATH access codes, PIV management-key auth.
- What a hostile host can do: drive any operation you have authorized while the device is plugged in and unlocked (sign, decrypt, assert). A security key authenticates presence and possession, not the intent of every byte the host sends. Touch requirements bound the rate.
- The residual gap is intent — the trusted-display flavor closes it. Because
a standard key attests presence and possession, a malicious page can silently
drive an authorized key over WebUSB to phish a real sign-in (demonstrated
against YubiKeys in Chrome).
The trusted-display flavor paints the true
rpIdon the device's own screen and gates each signature on a tap there, so a compromised host cannot fake what you approve. Qubes OS's CTAP proxy tackles the same gap in software, mediating CTAP through a trusted VM.
- All key material is sealed in flash: FIDO seed and PIV keys under
AES-256-CBC/GCM keyed by a device key (
kbase= HKDF of the chip serial and the OTP master key once provisioned), OpenPGP keys under the PIN-wrapped DEK chain. - OTP master key (production.md stage 1): with the MKEK fused and page-58 hard-locked, a flash dump — even with BOOTSEL access and the chip id — does not reproduce the sealing key. Without the burn, the sealing key derives from on-chip state an attacker with full flash + chip access could reconstruct; the burn is what makes at-rest real.
- Soft-lock (guides/soft-lock.md): optionally, the seed at rest is additionally wrapped with ChaCha20-Poly1305 under a 32-byte key only you hold (BIP-39/SLIP-39 words). A stolen device — even running genuine firmware — refuses every FIDO operation until that key is presented over an encrypted channel at power-up. Device + words, two factors.
- Caveat — superseded records linger: the flash log is append-only, so
re-sealing or deleting a secret leaves the old copy on flash until its page
is reclaimed. Two cases differ in how much that matters:
- The OTP-burn migration supersedes the pre-OTP seed, which was sealed
under the chip-serial-only root (no fuse secret). Left alone, a flash dump
plus the chip id would recover it — bypassing the burn. So it is not
left to lazy healing: the first boot after provisioning runs a one-shot
compaction (
Fs::compact, gated by theEF_HARDENEDmarker, crash-safe) that drives a full GC lap over the credential partition and physically erases every superseded pre-OTP record before the device re-attaches to USB. - The soft-lock transition leaves the same kind of lingering record, but on a provisioned device it is already sealed under the fused root — moot against anything short of a fused-key compromise — so soft-lock's at-rest guarantee simply hardens over time as natural compaction overwrites it.
- The OTP-burn migration supersedes the pre-OTP seed, which was sealed
under the chip-serial-only root (no fuse secret). Left alone, a flash dump
plus the chip id would recover it — bypassing the burn. So it is not
left to lazy healing: the first boot after provisioning runs a one-shot
compaction (
- The FIDO seed is never PIN-wrapped at rest (a deliberate design
decision): UP-only operations —
ssh ed25519-sk, U2F, no-PIN assertions — must work from a cold boot with no PIN presented, so a PIN-keyed at-rest copy adds no protection an attacker couldn't bypass via the always-loadable copy, while breaking those flows. At-rest strength is the kbase (tier above), not the PIN.
- Secure boot (production.md stage 2): the bootrom refuses unsigned images, so no foreign code ever runs to read the OTP key in secure mode. Glitch detectors are fused on along the way.
- Anti-rollback (anti-rollback.md, optional): with
ROLLBACK_REQUIREDfused, images below your board's rollback floor — or carrying no version at all, i.e. anything sealed before the feature — no longer boot. A kept copy of an old signed release with a since-fixed bug stops being a downgrade path. - Before secure boot is enabled, this attacker wins against the OTP tier: their firmware reads the MKEK exactly like ours does. That is why the production page calls the two stages one story.
Decapping, microprobing, advanced fault injection beyond the RP2350's glitch detectors, power/EM side channels, and the XIP TOCTOU — interposing on the QSPI bus to serve the genuine image to secure boot's verifier and a tampered one to the CPU, since nothing binds checked bytes to executed bytes and the image is too large to verify-in-place from SRAM. An in-package-flash part (RP2354) leaves no discrete flash chip to tap, raising a reliable swap to decap-class effort. The RP2350 is not a secure element and RS-Key does not pretend otherwise. If your threat model includes a funded lab, buy a certified key.
None. The device speaks USB only; there is no radio and no IP stack.
Raspberry Pi has publicly stress-tested the RP2350 die, and the results bound RS-Key's physical-attack posture.
Challenge 1 broke the A2 stepping (results). The task was to extract an OTP secret from a board running secure boot. The winning attacks:
- Aedan Cullen — voltage glitch on the
USB_OTP_VDDrail, reading OTP secrets out of the guarded path (erratum E16) (writeup, talk). - Marius Muench — a glitch plus a boot-ROM flaw, bypassing secure boot to run unsigned code.
- Kévin Courdesses — laser fault injection corrupting the boot-time signature check (erratum E24) (writeup).
- IOActive — focused-ion-beam (FIB) plus passive voltage contrast (PVC), reading the antifuse array directly: the bitwise OR of two physically paired bitcell rows (writeup).
The first three are comparatively cheap fault / boot-ROM attacks; the IOActive
readout needs FIB-class lab equipment (a tool worth hundreds of thousands of
dollars, one to two days per target) and applies to every device built on the
Synopsys dwc_nvm_ts40* antifuse IP on TSMC's 40 nm node — an antifuse property,
not an RP2350-specific defect.
The A4 stepping fixes the fault and boot-ROM attacks in silicon — but not the antifuse readout (announcement). A4 closes the boot-ROM errata (E20/E21/E24, including the laser signature bypass) in a new boot ROM, the OTP power-glitch (E16) through changes to the wrapper circuitry around the OTP macro, and the GPIO errata (E9, E3). The antifuse-array PVC readout is explicitly not fixed in A4; Raspberry Pi's guidance is to mitigate it by how secrets are stored in OTP — the chaffing RS-Key applies (see otp-fuses.md). A third challenge — power side-channel analysis of the secure-boot AES — is open with no break reported (challenge 2).
What this means for RS-Key. Our development boards are A2 — the broken stepping, kept as the conservative worst case. The firmware is A4-compatible, and A4 is recommended for the fault / boot-ROM attacks above. Against the antifuse readout — which no stepping fixes — RS-Key applies the chaffing mitigation directly (otp-fuses.md). What remains out of scope is unchanged: a funded lab with FIB/PVC, laser fault injection, or power/EM analysis against a device in hand. No software or provisioning choice on a general-purpose die closes those — that is what a dedicated secure element is for (limitations.md).
A FIDO authenticator's pitch is non-exportable keys; the wallet-style backup
is a conscious trade for recoverability, gated accordingly. Export moves the
seed over an ephemeral encrypted channel (P-256 ECDH → HKDF →
ChaCha20-Poly1305), and requires — all at once — physical touch, the FIDO
PIN/UV token when a PIN is set, and the one-time setup window: after an
explicit finalize, export is refused until a full reset regenerates a new
seed. Malware cannot exfiltrate the seed silently or later. Restore re-seals
the seed under the destination chip's root. The host driving a backup
necessarily sees the seed plaintext — do it on a machine you trust.
Scope: the deterministic identity only (resident passkeys, OpenPGP, PIV are
not covered).
On the trusted-display flavor the host need not be in that trust path: the
device can render its BIP-39 recovery phrase on its own screen (the seed is
turned into words on-device and never crosses USB), so a backup can be taken
without trusting any host. That trades the host-observation surface for a
physical/visual one — the words are briefly on the panel (shoulder-surf, camera).
It is gated to keep that surface small: it requires a device PIN set and
re-entered, a deliberate hold past an explicit "no one watching" warning, runs
only inside the same one-time window (and seal closes it), is disabled on the
fips-profile (non-exportable) build, zeroizes the seed/words from RAM on exit,
and auto-clears the panel after a short idle.
sequenceDiagram
participant U as You
participant H as Host
participant D as Device
U->>D: touch + PIN/UV (when set)
H->>D: ephemeral P-256 ECDH
D-->>H: seed over HKDF → ChaCha20-Poly1305 channel
Note over H: the host necessarily sees the seed in the clear
H-->>U: BIP-39 / SLIP-39 words
U->>D: finalize → export refused until a full reset
Key-grade material in RAM is wiped (zeroize, volatile writes) when its use
ends: session state and PIN/UV tokens on drop, transient key copies at end of
scope including error paths, and the transport/exchange buffers as soon as a
message completes (requests carry PINs and imported keys). Accepted
residuals: Copy temporaries inside RustCrypto curve arithmetic, digest
internals, and heap temporaries inside the rsa crate — short-lived,
library-internal, not wipeable without forking the crates.
cargo audit+cargo deny(advisories, license allow-list, source policy) andgitleaksrun inscripts/check.shand the pre-commit hook.- Dependencies are pinned (
Cargo.lock); the git dependencies are restricted to the embassy organization. - One known-unfixed advisory is accepted deliberately: RUSTSEC-2023-0071
(Marvin timing side channel in
rsa) — the OpenPGP RSA backend, mitigated by per-operation base blinding on every private-key path (PKCS#1 v1.5 sign, decipher, and the raw fallbackrsa_raw); rationale indeny.toml. The constant-time audit verified that this blinding leaves no unblinded private-exponent path.
ML-DSA-44 (COSE −48) and ML-DSA-65 (COSE −49) FIDO2 credentials — both the
in-tree rsk-mldsa crate — with hedged signing (32 fresh DRBG bytes
per signature; the hedge and expanded keys are zeroized). rsk-mldsa streams
the FIPS 204 matrix A on the fly so ML-DSA-65's keygen+sign fit the RP2350
stack — a hand-written implementation, so its constant-time posture is a
source-level claim (branch-free reductions, masked norm checks, no secret
division), not proven at machine code; it is checked byte-for-byte against
NIST ACVP KATs, with Kani proofs over the reductions and rounding. ML-DSA-87 (−50) is out of
reach on this chip (keygen overflows the stack; its makeCredential response
also overruns the CTAPHID message ceiling). ML-KEM-768 is compiled in as
scaffolding but nothing calls it until a CTAP PQC PIN/UV protocol exists.
None of these has a third-party audit yet — the same standing as the rest of
the RustCrypto stack, tracked via cargo-audit/deny.
This is an experimental hobby project. If you find a security issue, please report it privately to the maintainer rather than opening a public issue.