Mastodon

Fapico2 vs NSD — The Signer and the Serial Cable

A five-dollar ESP32 with a color touchscreen, and a Raspberry Pi Pico 2 with an LED and a button. Both are called signing devices. Both sign your events. Only one assumes your computer is already compromised.

Compartilhar
Fapico2 vs NSD — The Signer and the Serial Cable

Two devices. One question: who does the signer obey?

In Nostr there is no account. There is a key. Your npub is your public handle, your nsec is the private key that is your identity, and there is no recovery desk, no password reset, no support line. Whoever holds the key is you — forever. That is the promise and the terror.

Two boxes for it: a five-dollar ESP32 with a color touchscreen, and a Raspberry Pi Pico 2 with an LED and a button. Both are called signing devices — small boards that hold a key and sign on your behalf, so the key never has to sit in a browser. Both sign your events. Only one assumes your computer is already compromised.

Picture the failure in ordinary terms: you plug the device in, a tab you forgot about asks it to sign something, and your identity posts a message you never wrote. No warning, no password, no trace until someone replies to it. The other box makes that tab ask, out loud, in front of you. That is the whole difference — worth fifteen minutes of reading. This is for buyers, not firmware engineers; I gloss a wire or a chip once.

Author's note: I work on fapico2 — a conflict of interest, up front. Every claim came from the two source trees with files in hand, cross-checked against an internal audit — the same method I used on the ColdCard seed bug in the postmortem linked below; I did not build or flash an NSD myself, so treat the code citations as my reading, not a lab report. Where NSD's README concedes a point I quote it, because an author's admission is the strongest evidence there is. I also list what NSD does better and what fapico2 gets wrong. A comparison listing only the other side's failures is an ad. And fapico2 is alpha and unaudited — the last two sections say exactly what that costs.

NSD is LNBits' C++/Arduino nostr-signing-device (37d7ef4) on an ESP32; fapico2 is Rust/no_std on the RP2350 (938327c) plus a WebHID extension. ~2,300 lines against ~30,000. Both are real. This is not a feature count — the question is what happens when something goes wrong.


The one-sentence versions

NSD: a private key held as plaintext hex in external SPIFFS flash, driven over an unauthenticated plaintext serial line — whose own README calls the transport "not the most secure data transmission method, but x1000 better than storing the privatekey on a computer."

fapico2: keys sealed as AES-256-GCM records under an OTP-rooted key the chip cannot export, reached through a private USB channel where the client never sees key material, every secret operation behind a PIN gate and a physical touch, and no export verb exists at all — with the honest caveat that a key you imported still exists wherever you imported it from.

The difference is not a spec sheet. It is which side of the trust boundary each device puts the user on.

Here is the one test I apply to any signing device — the spine of this article: key material never leaves the board; the host gets no authority it was not explicitly given; and the user can see what they are about to approve. Everything below is that test run against two pieces of hardware.

Scope, so the comparison stays honest. Everything above, and the table below, compares one job only: Nostr event signing. fapico2 is not a Nostr only device — Nostr is one applet out of six — and not one extra feature is used as an argument here. The rest of the board gets two sections at the end, because it changes what the $5 buys you, not who wins this test.

The backstory: horse, a serial cable, and a livestream

When I added Nostr to fapico2, I did what I always do before writing code: read what was there. The interesting piece was horse — a Chromium extension built by fiatjaf and later maintained by LNBits, the same team that went on to build the Nostr Signing Device. horse implements NIP-07 (window.nostr) as a bridge to a hardware signer over Web Serial. It was the first thing I found that connected a browser to a signing device instead of a key in the browser [9]. Exactly the shape I wanted, and exactly the piece I could not reuse.

The problem was the transport. Web Serial is a plaintext, unauthenticated command line: whoever owns the port owns the device. Folding that into fapico2 would have meant taking a board whose whole reason to exist is that the host is hostile and handing the host a serial console. I could not downgrade fapico2's security to ship a feature. So I studied how the serious hardware wallets moved bytes — Trezor, Ledger, OneKey — and landed on WebHID. I cloned FIDO's CTAPHID framing onto a private vendor interface dedicated to Nostr, deliberately not FIDO's own usage page, so the two protocols could never collide or be mistaken for one another.

With the channel solved, the rest followed. I could implement the Nostr methods without ever letting key material cross the wire, move key management into the device, and lock it there. I balanced the UX against my own use of Nostr. I know how irritating a prompt is, which is why the touch bypass — the one convenience valve — was built like a security mechanism instead of a checkbox. And I kept the assumption I always keep: the host is malicious. Not "might be". Is. Every decision below falls out of that line.

Now the part that matters most: a comparison like this is easy to misread. This article is not an attempt to shade NSD. It started in the early days of Nostr, when the ecosystem had funding and wanted working use cases more than a threat model. NSD was built to solve one problem: get the keys off the computer. It did that, it shipped, and its authors said out loud that the channel and methods were not the best — the README line above is theirs, not something I dug out of a binary. Judging an early bridge by a later adversarial standard is cheap. I am not doing it; I am judging both against the same test, for the same job.

I did consider contributing upstream. But this was never a case of rewriting one method or improving a handler. To make it reliable you would have to change the transport, key custody, consent model and storage format. At that point you are not patching a project; you are building a different one. So I built fapico2 from scratch.

And the reason you are reading this at all: I was reviewing fapico2 on a livestream when the audience brought up NSD and asked for a comparison. This article is that answer.


Where the key actually sleeps

saveKeys() writes raw hex — one nsec per line — to a SPIFFS file (snsd/400_helpers.ino:40-56). No encryption, no wrapping, no derivation; tiny-AES-c is used for DMs only. The PIN is plaintext too, and a missing PIN file yields "00000000", which the login screen reads as unset, skip login.

  • A flash dump is a key dump. SPIFFS — the ESP32's little filesystem, stored on an external flash chip — is readable with esptool.py read_flash from the very USB port the device serves. No secure element, no fuse-rooted key. To be fair, the ESP32 has no hardware root to seal against. Without different silicon NSD could not have done what fapico2 does. But it could have encrypted the file under a PIN-derived key, and it did not. I once wrote 370 lines about what a broken RNG did to 1,196 ColdCard addresses. A plaintext file is simpler: it does not even need a bug.
  • The PIN is not a key. It gates the UI loop only, and its attempts counter is a plain file the firmware itself rewrites. There is no hardware-enforced retry budget behind it, so it protects nothing at rest.
  • Export is the design. Keys enter as hex over serial; the plaintext file is the storage. The nsec moved from one plaintext store to another, with a transport that re-exposes it added for free.

fapico2's eight slots live in a sealed record store: AES-256-GCM with metadata binding slot, generation and key domain into the ciphertext, so a copied record fails to open. The record key is HKDF(HKDF(otp_key_1 ‖ chipid) ‖ …) — derived from a one-time-programmable (OTP) fuse row in the RP2350 plus the chip's own ID, neither of which can be read out or exported; a blank or faulted row derives None, never a constant. There is no fallback to fail into. Custody is import-only, and downgrading a board is destruction, not extraction.

Finding: NSD stores the secret itself. fapico2 stores it under a root the device cannot tell you. Only one survives a flash dump.


The wire is the architecture

The transport is Arduino Serial at 9600 baud. Commands are plain ASCII: /sign-message, /add-key, /shared-secret, /decrypt-message, /wipe. Four properties matter:

  1. No pairing, no authentication, no encryption. PAIRING_CONTROL_TEXT = "lnbits" exists in the constants file and is referenced nowhere else. Pairing was declared and never built. Any process that can open the port is the device's authority.
  2. Secrets cross the wire both ways. /add-key carries the raw nsec in; /shared-secret hands the raw ECDH secret to any caller; /decrypt-message returns plaintext. The boundary the device exists to draw is drawn nowhere.
  3. The README concedes it — while claiming "better than a plaintext file on the computer." The previous section showed NSD is, at rest, also a plaintext file.
  4. Serial outlives consent. Web Serial permission is per-site, but the OS port is first-come-first-served: a daemon, another user, malware — anything claiming /dev/ttyUSB0 between sessions owns the device. It cannot know who is talking.

fapico2 exposes a third USB HID interface — the same class as keyboards, but a private vendor channel — on page 0xFF00, deliberately not FIDO's 0xF1D0, so protocols cannot collide. It carries framed 64-byte reports and CBOR payloads (a compact binary encoding). Routing is by interface, pinned by a test named us1660_a_nostr_command_is_refused_loudly_and_never_reaches_fido; an unsupported command answers 6D00 inside the envelope, never a hang or a fallthrough. One wire for everything versus wires disjoint by construction.

Finding: NSD's serial line is authenticated-by-assumption: it assumes the host is honest, builds custody on that assumption, and the whole architecture inherits it. fapico2 treats the host as hostile by default — the strictest assumption a signing device can make, and not the only defensible one.

What a compromised host can do, silently

actionNSDfapico2
sign as the user/sign-message: one blind button pressSIGN needs a per-identity-bound touch
read decrypted DMsplaintext over the wire, no gatePIN session + touch; plaintext only to the consenting extension
obtain an ECDH secret/shared-secret hands it to anyoneno such verb; NIP-44 runs on device
extract a private keyflash dump, any readback toolit can't: no export verb, OTP-rooted seal
destroy all identities/wipe over serial, no confirmationtouch-confirmed; CTAP2 reset spares Nostr
guess the PINdefault 00000000 = off, counter is a filedurable budget a power cycle cannot refund

The /wipe row is the cleanest demonstration: SPIFFS.format(); ESP.restart(); (708_cmd_device_state.ino:20-27), fireable by any host process with zero interaction. The button-confirm flow exists only in the menu path. A signing device its own host can silently erase is a signing device the host controls.

Back to the test: key material never leaves, the host gets no unearned authority, the user sees what they approve. NSD fails all three; fapico2 is built around all three, with the residuals below marking where it still falls short.

One correction to my own table: the fapico2 rows describe the device boundary, not the granted-client layer. Optionally and manually allowed by the user, a host with an indefinite persisted grant and an armed touch bypass can keep signing with no visible prompt — the practical equivalent of a much weaker touch gate. The key still cannot be extracted, but if the user has manually weaken the access control, it is not impossible to abuse. I list it as residual #1 below.


The randomness finding (yes, again)

Why drag ColdCard into a Nostr comparison? Because the failure I keep meeting in hardware is not a broken cipher — it is randomness nobody audited. In that postmortem: a #define set to 0, a guard that checked the macro's existence instead of its value, and a 32-bit PRNG quietly generating seeds meant to carry 256 bits. 1,196 addresses drained in 41 minutes [5]. The lesson was not "check your macros." It was: look at where the randomness comes from. NSD's IVs are the same kind of unexamined corner.

snsd/400_helpers.ino:123-127   random(0,256)  ← Arduino PRNG, seeded once at boot
snsd/500_crypto.ino            AES-256-CBC, legacy NIP-04, "?iv=" string

Every IV for the device's lifetime comes from one deterministic stream, so it is predictable and repeats. In CBC mode a repeated IV under the same key reveals when two messages share a plaintext prefix, and a predictable IV lets a chosen-plaintext attacker confirm guesses about what you wrote — this is the old NIP-04 ?iv= scheme, and the leak is DM content, not the private key. Careful key storage cannot fix it: the randomness architecture was never examined. Across the aisle: NIP-44 v2 on device, HKDF conversation keys, RP2350 TRNG nonces, constant-time MAC verify ordered before padding parsing. To be precise about the analogy: this is not the ColdCard bug — nobody drains a wallet from a weak IV. It is the same blind spot (unexamined randomness) with a much smaller blast radius, and I should not borrow ColdCard's body count to make it feel bigger.

NSD's sign screen shows the key label and nothing else — "Top to accept / Bottom to reject". Message, hash, requesting client, npub: none appear. There is no consent layer on the host at all.

fapico2's is layered. An extension prompt shows host, method, event kind and a preview (never plaintext for NIP-44 ops), with per-host grants; the device adds a per-identity touch, a RAM-only PIN session, and a bypass armable only from an open PIN session plus a physical press. NSD's friction is screen-local; fapico2's is protocol-local — it lives in the wire contract, so the client cannot skip, forge or talk around it. The transport is not plumbing; it is what makes consent possible.


The honest ledger

What NSD does better. $5 off-the-shelf, flashed from the browser in one click. A real touchscreen — menus, QR, on-device key switching; fapico2 has an LED and a button, and everything visible happens on the host, exactly the thing a signer distrusts. A real trade, not a point in our favor. Keys de facto export, so a broken device loses nothing; a board-generated fapico2 identity has no backup, and a dead board takes it with it — the honest price of the guarantees above. Imported identities, the documented path, keep the paper copy you made before the board ever saw them. And ~2,300 lines you can audit in ten minutes is a property 30,000+ lines of Rust lack.

What fapico2 still gets wrong, recorded rather than hidden:

  1. Optional and manually allowed by the user, but persisted grants are indefinite — a remembered host never prompts again; with an armed touch bypass (it can be bypassed only with PIN enabled), a malicious page signs continuously until restart. It was designed to improve the UX, and the security balanced solution was to made it optional for the user.
  2. Previews truncate at 80 characters, tags unrendered — deceptive long events slip through.
  3. The port-handshake race is acknowledged in code, not closed. Since the connection is exclusive to the app that connected first to the device, no other application can connect until the former leaves the channel.
  4. Touch-free identity enumeration enables silent cross-site linkage; the bypass is invisible at prompt time. The public key is always available for reading it.
  5. Eight identities, against an unbounded list.

Each lives inside the boundary; none exposes key material. But they are the difference between "the stronger design" and "a finished design". What would change my mind about the overall verdict: an independent audit finding that NSD's serial channel is authenticated, or a fapico2 release that closes residual #1. Until either exists, this is a reading of two codebases, not a measurement of two products.

What the five dollars actually buys

The comparison above is deliberately narrow. Here is what I left out. NSD is a Nostr signer. fapico2 is one firmware, one binary, one USB composite device — CCID, CTAP-HID and a private Nostr HID interface — carrying six applets: FIDO2/U2F passkeys, OpenPGP 3.4, OATH TOTP/HOTP, YubiKey-protocol OTP, a Management applet, and Nostr. 848 resident passkeys against 100 on a YubiKey 5, 68 OATH slots, 8 Nostr identities. Chrome, ykman, Yubico Authenticator and GnuPG are verified against real hardware; FIDO2 works immediately with no driver to install. No vendor account, no cloud, no subscription. The firmware is AGPLv3 — if you can read the source, you can rebuild the board [6][7].

One backup path the Nostr sections above never mentioned: an optional 24-word seed (BIP39) is the FIDO derivation root, so a board wiped to bare silicon, reflashed and restored from the phrase alone re-derives the same passkey private keys — proven on hardware. That seed is scoped to FIDO; it does not back up Nostr identities. For those, the documented onboarding path is to generate the nsec in software, write it down, then import it. That corrects something I got wrong in the ledger above: import-only custody means the board cannot leak a key, not that you are forbidden from having a backup. A board-generated identity has none; an imported one has the paper copy you made before the board was involved.

None of this belongs in the verdict. It is context for the price, not evidence in the comparison.

Security first, on a chip with no secure element

Now the part that gives "security first" a meaning you can check. A Raspberry Pi Pico 2 is a $5 Cortex-M33 board. It is not a secure element; it has no FIPS or Common Criteria evaluation and no NFC. And — the one that matters — the SWD debug port is open on every published alpha image, deliberately, so bricked boards stay recoverable. Anyone with brief physical access and a debug probe can extract every key the device holds. Until that closes, this is evaluation hardware: do not make it the only key to anything you cannot afford to lose. It is also not independently audited; the published red-team assessment is of an earlier build, some findings addressed, not all. PIV is deferred, and a known RSA timing advisory has no upstream fix [6][8].

So security first on this silicon cannot mean "we bought better silicon". It means every decision is spent on the boundary instead. No network stack in the device build — no channel to exfiltrate through, because there is no channel. Keys born from the RP2350 TRNG and used only on the chip. Records sealed under an OTP root, with the firmware refusing to boot if that row is unavailable rather than falling back to a constant. Interface-routed dispatch so a Nostr frame can never reach FIDO. Typed, in-band errors instead of hangs. A PIN budget a power cycle cannot refund. A physical touch bound per identity. No export verb. Signed secure boot is available (opt-in, off by default in alpha). CI gates clippy, a 3.5 MiB size ceiling, fuzzing and mutation tests, host unit tests, and the same black-box suites over an emulator [6][7].

That is the honest version of the claim. You cannot buy a secure element for $5, so you do not pretend to have one. You publish the threat model, the red-team findings and the open debug port; you keep the attack surface small enough to read; and you put every trust boundary inside the device. On a board this cheap, transparency is not a consolation prize — it is the only security property you can actually verify.

Conclusion: the signer obeys someone

NSD is a legitimate project: hardware signing for Nostr at commodity prices, with a real button-confirm and on-screen UI. It also meets the goal it set: getting the nsec off a general-purpose computer is a thousand times better than a text file. For an identity you can rotate in an afternoon, that may be rational. Judged against its threat model, NSD is not a failure; judged against fapico2's, it is. Confusing those two tests would be the cheapest argument in this article, so I will name it. But its README states the ceiling: a serial, unauthenticated, "not the most secure" transport; keys at rest that are the plaintext file it claims to improve upon; a default PIN policy that is off; IV randomness from a boot-seeded PRNG; a wipe verb any host process fires without confirmation.

fapico2 closes each of those specific ceilings — every one by a recorded decision, all variations on one assumption: the host is hostile. The point is not paranoid hardware for its own sake; it is that your identity should belong to you even on a bad day, on a borrowed laptop, with a browser full of tabs you did not choose. But it pays for them: a board-generated identity cannot be backed up, and a dead board takes it with it. Its residuals are real and sit inside the boundary, whereas every NSD weakness here is the boundary. That is the honest shape of the trade, not a clean win.

The persuasive sentence, if one is wanted: NSD moves the private key off the computer and onto a device the computer fully commands; fapico2 moves the computer's authority over the private key onto the device, and then locks the device.

The ColdCard story ended with do you trust your vault? — you do not depend on the vault, you depend on the entropy. This one ends colder. Your nsec does not care where it sleeps. It cares who can give orders.

So do something with it. Before you buy a signer, ask the vendor three questions and do not accept a feature list as an answer: Can the host read my private key off the wire? Can any process on my machine issue commands the device will obey without a physical confirmation? And what, exactly, am I shown before I press the button? If the answers are "no", "no" and "the whole request", you are on the right side of the boundary. If they are vague, read the source — or pick the box that assumes your computer is already lost.


Sources

  1. nostr-signing-device 37d7ef4 — https://github.com/lnbits/nostr-signing-device
  2. fapico2 938327c — https://github.com/eddieoz/fapico2
  3. fapico2-nostr-extension v0.5.0 — https://github.com/eddieoz/fapico2-nostr-extension
  4. Internal audit docs/nostr-signing-device-comparison.md (2026-11-10); ADR 0005, ADR 0006. Scope: source reading only — no hardware test bench, no independent third-party audit of either device. Attached below.
  5. ColdCard Hack — O Bug que Quebrou o Cofre — https://www.eddieoz.com/coldcard-hack-o-bug-que-quebrou-o-cofre/
  6. fapico2 README + verified facts (promo/facts.md, single source of truth) — https://github.com/eddieoz/fapico2
  7. fapico2 user manual — https://eddieoz.github.io/fapico2/ (Security, Threat model, Features chapters)
  8. fapico2 SECURITY.md, docs/debug-access-risk.md, docs/supply-chain.md — published limitations.
  9. horse — fiatjaf / LNBits, Chromium NIP-07 bridge to a hardware signer over Web Serial — https://github.com/lnbits/horse

Send sats if you liked.

⚡️eddieoz@sats4.life