Repository navigation
Releases: eddieoz/fapico2
Release list
fapico2-release-alpha-v0.3.0
fapico2-release-alpha-v0.3.0 — Release Notes
Date: 2026-10-15 · Target: Raspberry Pi Pico 2 (RP2350), USB-connected
Theme: PIN-locked session custody for the Nostr applet (EPIC
EPIC-nostr-pin-custody).
This is an alpha. Alpha/beta images keep the debug port available and burn
nothing irreversible into OTP (ADR 0002).
The feature in one paragraph
The Nostr identity set can now sit behind a PIN. A PIN-set board refuses
every identity-touching command with 6982 (session required) until
VERIFY_PIN opens a session; the session lives only in RAM and dies with
the board. GET_INFO advertises the new capability bit and the PIN state in
its flags byte; the vendor-HID dialect gains VERIFY_PIN 0x4B,
SET_PIN 0x4C (envelope idx 0 = set, 1 = change) and LOCK 0x4D, and the
error map translates 63C1..63C3 (attempts left), 63C0 (locked out —
recovery is a factory reset, which destroys every identity), 6982 and the
dual-reading 6A82. A wrong PIN has a durable budget of three; the lockout
is the floor of the same range the wrong-PIN answers live in.
Upgrading from v0.2.0 — the transition warning
EXPORT_KEY is gone — export before flashing. Custody is import-only: the
export command (0x48 over the vendor HID, INS 0x1A over CCID) answers
6D00 on this firmware, on every board, with no grandfathering. Any key you
still want a host copy of must be exported from a v0.2.0 image before you
flash this one (ADR 0006).
The session rule
A verified session is a RAM flag, cleared by the power cycle — unlocking
again after a restart is normal, not a fault. The flag is applet-global: it
survives deselect and re-select and spans the CCID and vendor-HID transports;
LOCK (vendor 0x4D) closes it early. Management RESET wipes the PIN along
with everything else; CTAP2 authenticatorReset still spares the Nostr
applet entirely.
Generation cross-tolerance matrix (US-1708)
Alpha users will hit new client ↔ old firmware and old client ↔ new
firmware. The matrix below was exercised against BOTH emulator images — the
current build and the v0.2.0 image built from the release commit ddc3875
(worktree; tests/harness/test_nostr_crossgen.py drives it, binary path
overridable with FAPICO2_V020_EMULATION):
| combination | request | answer | rendered as |
|---|---|---|---|
| new ext ↔ old firmware | GET_INFO |
9000, body 00 01 00 03 07 00 00 00 — the 8-byte shape with the flags byte a reserved 0x00, caps 0x0003 (no CAP_PIN_MGMT) |
the capability gate hides every PIN UI; no error |
| new ext ↔ old firmware | VERIFY_PIN 0x4B (INS 0x1B) |
6D00 in band, empty body |
"PIN not supported by this firmware" — never a hang, never a CTAPHID-level error |
| old ext ↔ new firmware | EXPORT 0x48 (INS 0x1A) |
6D00 in band, empty body |
"export removed" |
| old ext ↔ new firmware | GET_INFO |
9000, 8-byte body, flags 0x00 on a PIN-less board |
parses unchanged — byte-compatible |
| control | LIST_KEYS (INS 0x11) |
9000 + n=0 on both images |
identical on both generations |
Measured on the v0.2.0 image, 2026-10-15: all five rows green
(pytest tests/harness/test_nostr_crossgen.py, 5 passed).
The vendor-HID arm of this matrix (byte 0x4B on the old image, byte
0x48 on the new) is pinned by the firmware host tests — the emulator
serves Nostr over CCID only (AGENTS.md §8), so the HID arm
does not run under the emulator by design.
The custody ceremony — one-time, consented, permanent
On top of the PIN gate, this release adds the custody ceremony
(ADR 0005 amendment,
ADR 0006, US-1712..1716): the explicit
CEREMONY_START verb (vendor byte 0x4E, CCID INS 0x1E) burns a
page-locked OTP secret and reseats every sealed record under a key derived
from its read-back contents. After the ceremony's completion marker stands,
no record — including the PIN record — opens under the old constant-KDF,
so a flashed-back v0.2.0 image finds nothing its EXPORT_KEY can extract
and cannot re-enable export.
The ceremony is never a side effect: no command but CEREMONY_START
writes the OTP page, and the verb refuses (6F02) on a board without a PIN,
without sealed identities, or already sealed. Declining leaves the board
legacy-sealed and migration-pending — GET_INFO's flags bit 2
(FLAG_MIGRATION_PENDING), re-offered by the extension at every connect.
A faulted custody-page read answers 6F01 (sealed-unavailable) to every
command, RAM-only, non-latching — recovery is the extension's stepped
ladder: power cycle, re-flash the current release over BOOTSEL, and only
then management RESET.
Downgrade is destruction, not extraction (ADR 0006
— the residuals): a v0.1.x image over a ceremony-completed board destroys
the identity set by its own ordinary write path; it cannot preserve or read
it. The matrix transcript
(docs/audits/2026-10-15-nostr-downgrade-matrix.md)
carries the hardware evidence for both legs.
fapico2-release-alpha-v0.2.0
fapico2-release-alpha-v0.2.0 — Release Notes
Date: 2026-10-14 · Target: Raspberry Pi Pico 2 (RP2350), USB-connected
Shipping image: fapico2.uf2 — the Release carries the image and GitHub's
own "Source code" archives, and nothing else. The SHA-256 of the image is
recorded per-build, not here: it is stamped in the release SBOM and named in
the release-manifest.json attached to the release workflow run, and it is
what the build-provenance attestation covers.
This is an alpha. Alpha/beta images keep the debug port available and burn
nothing irreversible into OTP (ADR 0002).
The headline: Nostr identities on the device (PR #18)
The board now holds up to 8 BIP-340 Nostr identities — generated, stored
and used entirely on the RP2350. Schnorr event signing and NIP-44 v2
encryption run on the chip; a private key never crosses the wire in either
direction.
A Nostr identity is a publishing identity: non-revocable, recoverable by no
one — the same class of secret as a bitcoin key. The device's existing
posture, on-device generation, AEAD-at-rest under the OTP-derived root and
physical-touch consent, is applied to it without weakening any existing
applet.
Two transports, one applet:
- CCID — plain APDUs, selected by AID
F0 00 00 00 02, INS0x10–0x1A. - Vendor HID — a third USB HID interface (interface 3, usage page
0xFF00), CTAPHID framing with vendor commands0x40–0x4A, CBOR
[idx, arg]payloads and CBOR{1: sw, 2?: data}responses.
The browser surface is the separate
fapico2-nostr-extension
(Chrome 116+, MV3 + WebHID), which exposes the NIP-07 window.nostr API —
sign, NIP-44 encrypt/decrypt, key management — with a per-site, per-action
approval prompt and the physical touch as final consent. Verified end-to-end
against hardware. A Linux user needs
platform/udev/70-fapico2-nostr.rules
(scoped to the VID, so re-identified boards keep working).
Consent model. Three reads are touch-free (GET_INFO, GET_PUBKEY,
LIST_KEYS); every command that uses or changes the private set requires a
physical press, with no config bit to change that. The press consents to the
identity, not the opcode: presence tags are 0x4000_0000 ‖ idx on CCID
and 0x6000_0000 ‖ CID on HID, disjoint from FIDO's 0x8000_0000. The gate
survived a dedicated adversarial audit
(docs/nostr-presence-audit.md, hardware-verified
2026-10-12) whose two load-bearing fixes are in this release: the domain
check is exact (a folded FIDO tag can never arm a Nostr grant), and an
armed grant dies with its request (CANCEL and expiry drop it). The HID
arm refuses while parked, it does not queue — the extension does the FIFO on
its side.
Storage (ADR 0005): a third keyregion
domain carved from the top of FIDO's range, so every existing passkey
keeps its address. Records are device-bound
(derive_payload_key(otp_key_1, chipid, NOSTR_PAYLOAD_SECRET) — no PIN
input), sealed per-record with an AAD binding domain, slot and generation,
and every write runs an occupancy guard: a stale FIDO record in the carved
range refuses 6A84 rather than dying silently.
Crypto (apps/nostr/src/{crypto,nip44}.rs): BIP-340 signing and NIP-44
v2 on secp256kfun 0.12.1, pinned against the BIP-340 reference vectors and
NIP-44's published vector set. The plaintext cap (1,792 B) is derived from
the CCID wire bound against NIP-44's chunk-rounded padding and asserted at
compile time, so the wire cap and the in-band refusal cannot drift. Honest
supply-chain note: secp256kfun is self-declared, not human-reviewed — the
cargo-vet record says so explicitly (supply-chain/config.toml), and it
carries the applet's signing keys.
Behaviour changes to know before you flash
- FIDO2 resident passkeys move 856 → 848. Eight slots are carved for
Nostr from the top of FIDO's range; existing records are untouched and
guarded by the occupancy rule. Measured to refusal at the carved geometry
(docs/capacity.md). - Management
RESETnow wipes Nostr with everything else — the CCID
proxy forwardsfactory_wipe. CTAP2authenticatorResetstill spares it.
"Reset" means what its own documents say. - Flash budget raised 1664 → 1680 KiB in
ci.yml, with the reason inline
(the real +28.5 KB delta vs the spike's ~25 KB projection).
Also in this window: the boot-freeze instrumentation (US-1301…1312)
If a board freezes after a flash or a settings save, the recovery is a
power cycle with 5 seconds off — not a reset press, not a re-flash, and
not a fast replug, which leaves the board still frozen
(docs/boot-freeze.md). This release ships the
instrumentation that turns the next report into a measurement, and two fixes
it surfaced:
- The LED tells the truth at boot: a countable blink ladder names the
boot phase a frozen board reached, and the fatal signature (one long
flash, pause, four short) is distinguishable from a silent freeze
(ADR 0004). - The boot OTP reads retry within a 20 s budget before refusing, and the
runtimesw_lockwrite that broke page-0 OTP reads is gone — both were
false "the OTP key row is unreadable" failures on healthy boards. - The watchdog was considered and refused as a recovery mechanism
(US-1309/1311/1312); the reasoning is in the epic record.
The trigger itself is not yet established — the epic's status is
instrumentation shipped, cause open. Do not attach an SWD probe to
investigate a freeze; on this hardware that alone reproduces it.
Verifying the download
-
Check out the tag and confirm it names the commit the Release shows.
-
Verify the image against the provenance GitHub minted for it:
gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
-
The same digest is in the
release-manifest.jsonworkflow artifact of the
release run, cross-checked against the digest stamped into the attached
SBOM.
The SBOM (supply-chain/sbom.cdx.json), the provenance statement and the
release manifest are workflow artifacts, not Release assets — the Release
itself carries the image and the auto-generated source archives.
Scope and caveats unchanged from v0.1.0
The single UF2 serves one USB composite device (CCID + CTAP HID, now with
the third Nostr vendor-HID interface); applets are selected by AID. The
app-by-app scope, the hardware-acceptance matrix and the provisional-VID
(fa20:0002, not USB-IF-registered) note are in
docs/release-notes-v1.0.0.md and
docs/hardware-matrix.md. The Nostr applet's
user-facing and implementation pages are in the documentation site
(book/how-to/nostr.md, book/features/nostr.md, served at
https://eddieoz.github.io/fapico2/).
fapico2-release-alpha-v0.1.2
fapico2-release-alpha-v0.1.2 — Release Notes
Date: 2026-10-09 · Target: Raspberry Pi Pico 2 (RP2350), USB-connected
Shipping image: fapico2.uf2 — the Release carries the image and GitHub's
own "Source code" archives, and nothing else. The SHA-256 of the image is
recorded per-build, not here: it is stamped in the release SBOM and named in
the release-manifest.json attached to the release workflow run, and it is
what the build-provenance attestation covers.
This is an alpha. Alpha/beta images keep the debug port available and burn
nothing irreversible into OTP (ADR 0002).
What changed since v0.1.1
The firmware image is functionally unchanged — the only touch in the
firmware tree is a stale documentation path inside one test comment. This
release is the tooling and the documentation around the device:
- Seed backup export/restore (PR #16):
scripts/backup_fido.pyexports
and restores the vendor seed as a 24-word BIP-39 phrase, speaking the
0x41backup dialect (MSE / EXPORT / LOAD / FINALIZE / STATE) over a
touch-only session. The phrase never enters argv or the environment;
--outwrites0600and refuses to overwrite; finalize refuses a
non-terminal state without--yes. The dialect is pinned byte-exactly by
tests/pico-fido/test_093_backup.py. The honest scope is in
docs/backup-seed.md: passkeys are not in this
backup, CTAP2 reset does not clear the seed, and a PIN-set board answers
0x36to this touch-only tool by design (AGENTS.md §4) — use PicoForge
there, or reset the PIN away. - Documentation site (PR #17): the mdBook in
book/now serves at
https://eddieoz.github.io/fapico2/, deployed bypages.yml— product
landing, how-tos (disk encryption, git signing, encrypt-to-recipient,
changing the USB identity), a threat model and a FAQ, all reviewed against
the device's actual behaviour. - Release workflow (8d5aaa6): the Release is titled with the tag itself,
not "fapico2 ".
Verifying the download
-
Check out the tag and confirm it names the commit the Release shows.
-
Verify the image against the provenance GitHub minted for it:
gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
-
The same digest is in the
release-manifest.jsonworkflow artifact of the
release run, cross-checked against the digest stamped into the attached
SBOM.
The SBOM (supply-chain/sbom.cdx.json), the provenance statement and the
release manifest are workflow artifacts, not Release assets — the Release
itself carries the image and the auto-generated source archives.
Scope and caveats unchanged from v0.1.0
The single UF2 serves one USB composite device (CCID + CTAP HID); applets are
selected by AID. The app-by-app scope, the hardware-acceptance matrix and the
provisional-VID (fa20:0002, not USB-IF-registered) note are in
docs/release-notes-v1.0.0.md and
docs/hardware-matrix.md; nothing in them changed here.
fapico2-release-alpha-v0.1.1
fapico2-release-alpha-v0.1.1 — Release Notes
Date: 2026-10-07 · Target: Raspberry Pi Pico 2 (RP2350), USB-connected
Shipping image: fapico2.uf2 — the Release carries the image and GitHub's
own "Source code" archives, and nothing else. The SHA-256 of the image is
recorded per-build, not here: it is stamped in the release SBOM and named in
the release-manifest.json attached to the release workflow run, and it is
what the build-provenance attestation covers.
This is an alpha. Alpha/beta images keep the debug port available and burn
nothing irreversible into OTP (ADR 0002).
What changed since v0.1.0
libfido2resident keys work (PR #14): the credential-management
operation arrives on the0x41byte libfido2 1.14.0 hard-codes, not the
0x0Athe other clients send.ssh-keygen -Know downloads resident keys.
Two defects in the preview dialect were fixed alongside it — the
enumerateRPsresponse had a chimeratotalRPskey value, and the
sub-command numbering was inverted (US-1625/US-1626).- Presence-gated destruction (PR #10, US-1600–1614):
authenticatorReset
now opens a consent window on the transport and requires user presence in
both twins — a management factory reset costs exactly one touch — and the
getInfo ciphertext is bound to the IV it advertises. - CI and gates (PRs #11, #13, #15): the pinned toolchain is installed
wherecargoactually uses it, the boot-chain gate the 1.99.0 pin made
reproducibly red was fixed, and pull requests are no longer failed for
paperwork a change did not cause. - README rebranded as the project's front door (PR #12).
Verifying the download
-
Check out the tag and confirm it names the commit the Release shows.
-
Verify the image against the provenance GitHub minted for it:
gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
-
The same digest is in the
release-manifest.jsonworkflow artifact of the
release run, cross-checked against the digest stamped into the attached
SBOM.
The SBOM (supply-chain/sbom.cdx.json), the provenance statement and the
release manifest are workflow artifacts, not Release assets — the Release
itself carries the image and the auto-generated source archives.
Scope and caveats unchanged from v0.1.0
The single UF2 serves one USB composite device (CCID + CTAP HID); applets are
selected by AID. The app-by-app scope, the hardware-acceptance matrix and the
provisional-VID (fa20:0002, not USB-IF-registered) note are in
docs/release-notes-v1.0.0.md and
docs/hardware-matrix.md; nothing in them changed here.
fapico2-release-alpha-v0.1.0
fapico2-release-alpha-v0.1.0 — Release Notes
Date: 2026-10-05 · Target: Raspberry Pi Pico 2 (RP2350), USB-connected
Shipping image: fapico2.uf2, sha256
0f4e6189e4ea0c744ec556b4cf0b74c849b776ac1a25a085d5461175a898142c
(3061 blocks = 1 absolute preamble + 3060 ARM_S payload; 1,567,232 bytes;
byte-reproducible from the tag via firmware/uf2gen.py — see
docs/size-report.md, which re-measures every figure here
and fails CI on any disagreement).
This is the first published release.
v0.1.0 scope
One firmware, one binary — a single UF2 serving one USB composite device
(CCID + CTAP HID), apps selected by AID:
| App | State in v0.1.0 |
|---|---|
OpenPGP 3.4 (CCID, AID D2 76 00 01 24 01) |
Phase-7 addendum: full opcard command set served on device (S-721-1…5), hardware-accepted P7-C7 (gpg generate + clearsign/verify on device); private keys migrate via PW1 (below) |
Management (CCID, A0 00 00 05 27 47 11 17) |
config (incl. EF_DEV_CONF persistence), migration passphrase APDU |
OATH (CCID, A0 00 00 05 27 21 01) |
v1.0.0 capability note (SUPERSEDED by the Phase-7 OATH addendum below): a YKOATH wiring shell — SELECT→FCI only, command set not served; app-restore is post-cutover, no store restore (OathApp::new()); migrated OATH data was preserved in the keystore and serves once the restore lands |
OTP (CCID, A0 00 00 05 27 20 01) |
served inside the management app crate; slots persisted + restored (otp.slots.v2) |
| FIDO2/U2F (CTAP HID) | v1.0.0 capability note (SUPERSEDED by the Phase-7 addendum below): a CTAP-HID shell — getInfo + vendor vault + error responses; the only durable secret is the persisted hkey (keydev). Resident-credential CTAP2 serving is post-cutover; migrated FIDO credentials are preserved in fido.keystore.v1 |
Hardware acceptance
The US-213 hardware matrix was executed on the RP2350 with verbatim evidence
per row (rows 1–5 recorded, row 6 PIV DEFERRED): see
hardware-matrix.md. The boot bring-up saga and its
reconciled root cause (task-frame overflow chain: store-in-async-frame →
store by-value copy → app objects/Dispatcher in the task param frame; MSPLIM
exonerated; entry = VT[1]) are recorded in the matrix's
boot bring-up section.
USB identity (provisional VID)
The device enumerates as **fa20:0002 "The BLOCO Community" "fapico2"**. **Note:** 0xFA20is **not** a USB-IF-registered vendor ID — this identity is provisional. Production requires a registered VID or a picoforge re-brand. Host implications and the libccidInfo.plist` allowlist workaround
(macOS/OpenSC; Linux pcscd matches by CCID class) are documented in the
README.