Skip to content

Releases: eddieoz/fapico2

fapico2-release-alpha-v0.3.0

Pre-release

Choose a tag to compare

@github-actions github-actions released this 16 Oct 00:04
fapico2-release-alpha-v0.3.0
67a6668

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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:51
fapico2-release-alpha-v0.2.0
ddc3875

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, INS 0x10–0x1A.
  • Vendor HID — a third USB HID interface (interface 3, usage page
    0xFF00), CTAPHID framing with vendor commands 0x40–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 RESET now wipes Nostr with everything else — the CCID
    proxy forwards factory_wipe. CTAP2 authenticatorReset still 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
    runtime sw_lock write 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

  1. Check out the tag and confirm it names the commit the Release shows.

  2. Verify the image against the provenance GitHub minted for it:

    gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
  3. The same digest is in the release-manifest.json workflow 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 09 Oct 10:12
fapico2-release-alpha-v0.1.2
b943a8b

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.py exports
    and restores the vendor seed as a 24-word BIP-39 phrase, speaking the
    0x41 backup dialect (MSE / EXPORT / LOAD / FINALIZE / STATE) over a
    touch-only session. The phrase never enters argv or the environment;
    --out writes 0600 and 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
    0x36 to 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 by pages.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

  1. Check out the tag and confirm it names the commit the Release shows.

  2. Verify the image against the provenance GitHub minted for it:

    gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
  3. The same digest is in the release-manifest.json workflow 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

Pre-release

Choose a tag to compare

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

  • libfido2 resident keys work (PR #14): the credential-management
    operation arrives on the 0x41 byte libfido2 1.14.0 hard-codes, not the
    0x0A the other clients send. ssh-keygen -K now downloads resident keys.
    Two defects in the preview dialect were fixed alongside it — the
    enumerateRPs response had a chimera totalRPs key 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
    where cargo actually 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

  1. Check out the tag and confirm it names the commit the Release shows.

  2. Verify the image against the provenance GitHub minted for it:

    gh attestation verify fapico2.uf2 --repo eddieoz/fapico2
  3. The same digest is in the release-manifest.json workflow 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

Pre-release

Choose a tag to compare

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.