boltcard-writer/README.md
Padreug 5df4ce7e73 docs: record the Omnikey 5022 field test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 22:15:32 +02:00

250 lines
9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# boltcard-writer
Write, verify and wipe **Bolt Cards** (NXP NTAG 424 DNA) from a Linux PC with a
USB NFC reader, straight from the LNbits **Bolt Cards** extension's auth link.
No phone app needed.
Field-tested on Arch Linux with a **HID Omnikey 5022 CL** (write + read-back
verified 2026-09-20); the **ACS ACR1252U** is the other intended reader. Any
PC/SC (CCID) reader that supports ISO 14443-4 should work.
What it does is exactly what the official
[Bolt Card programmer app](https://github.com/boltcard/bolt-card-programmer)
does with the same link: write the `lnurlw://` URL, enable SDM mirroring
(UID + tap counter encrypted with k1, CMAC with k2), lock the file to k0, set
keys 1–4 then 0. After writing it reads the card back and checks the mirrored
`p`/`c` values with k1/k2 — the same check LNbits does on every tap.
## Setup (Arch Linux / Omarchy)
```sh
sudo pacman -S --needed pcsclite ccid uv git
sudo systemctl enable --now pcscd.socket
git clone https://git.atitlan.io/aiolabs/boltcard-writer.git
cd boltcard-writer
uv run boltcard-writer readers
```
`uv run` creates a virtualenv and installs the two Python dependencies on
first use (needs internet once).
**No uv?** (e.g. the Omarchy mirror 404s on the package, try
`sudo pacman -Syu uv` first). The only dependencies are click and
cryptography, both in Arch's repos, and the checkout ships a launcher:
```sh
sudo pacman -S --needed python python-click python-cryptography
./boltcard-writer readers # instead of `uv run boltcard-writer readers`
```
With the ACR1252U plugged in you should see two readers, the starred one is
used:
```
* ACS ACR1252 1S CL Reader PICC 00 00
ACS ACR1252 1S CL Reader SAM 00 01
```
Smoke test with any NTAG 424 DNA card (blank or not):
```sh
uv run boltcard-writer read
```
```
Tap the card on: ACS ACR1252 1S CL Reader PICC 00 00
Card:
UID: 04A1B2C3D4E5F6
chip: NTAG 424 DNA (HW 30.00, SW 01.00, storage 0x11)
ATR: 3B8180018080
key versions: k0=0 k1=0 k2=0 k3=0 k4=0
NDEF file: SDM off, comm plain, read=free write=free change=key0
NDEF URL: (empty)
state: blank / factory
```
## Writing a card
1. In LNbits open the **Bolt Cards** extension and create the card as usual
(name, limits, **card UID**: get it with `read` above, keys auto-generated).
2. Click the QR icon on the card row, then **Keys / Auth link**: this copies
`https://<your lnbits>/boltcards/api/v1/auth?a=<code>` to the clipboard.
Back up the keys from that dialog somewhere safe.
3. Put a blank card on the reader and run:
```sh
uv run boltcard-writer write 'https://<your lnbits>/boltcards/api/v1/auth?a=<code>'
```
```
Card: my card
LNURLw: lnurlw://lnbits.example.com/boltcards/api/v1/scan/9f2c…
Keys backed up to /home/you/.local/share/boltcard-writer/backups/20260920-181200-write-my_card.json
Tap the card on: ACS ACR1252 1S CL Reader PICC 00 00
UID: 04A1B2C3D4E5F6
…
state: blank / factory
Write this card now? Keep it still on the reader until done [y/N]: y
… writing NDEF URL (119 chars, p@88 c@124)
… authenticating with key 0
… enabling SDM mirroring on the NDEF file
… changing key 1
… changing key 2
… changing key 3
… changing key 4
… changing key 0
… reading back and verifying p/c
… verified: uid 04A1B2C3D4E5F6 counter 1
Done. Bolt Card 04A1B2C3D4E5F6 written and verified (counter 1).
```
**Do not move the card until "Done"** — the whole write takes about a second.
The auth link is **single-use**: LNbits rotates the code every time it is
fetched. The keys are saved to a backup file *before* the card is touched, so
if something fails mid-way you can retry with the backup instead of a new link:
```sh
uv run boltcard-writer write ~/.local/share/boltcard-writer/backups/<file>.json
```
The `boltcard://program?url=…` deep link behind the QR code is accepted too,
and `-` reads a keys JSON from stdin.
## Checking a written card
```sh
uv run boltcard-writer read --keys ~/.local/share/boltcard-writer/backups/<file>.json
```
reads the live `p`/`c` the card mirrors and verifies them with k1/k2, printing
the tap counter. Or just tap it on any Lightning POS / wallet that speaks
LNURL-withdraw.
## Wiping a card
Resets keys to zero, disables SDM and clears the NDEF so the card is blank
again (delete it in LNbits afterwards).
1. In LNbits, QR icon on the card row → **Wipe** → copy the JSON (`WIPE DATA`).
2. Save it to a file, or pipe it:
```sh
uv run boltcard-writer wipe wipe.json
# or
pbpaste | uv run boltcard-writer wipe - # wl-paste on Wayland, xclip -o on X11
```
A backup file from `write` or the card's auth link work as well. The JSON's
`uid` is checked against the card on the reader so you cannot wipe the wrong
card by mistake.
## Options
```
boltcard-writer [-r READER] [--debug] [--timeout SECS] COMMAND
readers list readers
read [--keys FILE] inspect the card, optionally verify p/c
write SOURCE [-y] [--force] [--no-verify] [--backup-dir DIR]
wipe SOURCE [-y] [--backup-dir DIR]
```
`--debug` prints every APDU exchanged with the card to stderr. **Include that
output when reporting a problem.** Key material is never in the trace (key
changes travel encrypted).
## Troubleshooting
| Symptom | Fix |
|---|---|
| `libpcsclite.so.1 not found` | `sudo pacman -S pcsclite` (NixOS: use `nix-shell`) |
| `SCARD_E_NO_SERVICE` | `sudo systemctl enable --now pcscd.socket` |
| `SCARD_E_NO_READERS_AVAILABLE` | reader unplugged, or the `ccid` package is missing. `lsusb` should list `072f:223b Advanced Card Systems`; `pcsc_scan` (package `pcsc-tools`) should show it |
| `SCARD_E_SHARING_VIOLATION` | close `pcsc_scan` or the other program using the reader |
| Waits forever at "Tap the card" | card not detected: put it flat on the reader's centre; a different card type (NTAG 213, credit card) is rejected on first command instead |
| `AUTHENTICATION_ERROR` / `RndA' mismatch` on write | the card is not factory-fresh; its k0 is not zero. Wipe it with the keys it was written with |
| `AUTHENTICATION_ERROR` on wipe | wrong keys for this card |
| `AUTHENTICATION_DELAY` | too many bad auths; leave the card on the reader ~1 s and retry |
| `card already looks provisioned` | it has SDM on or non-zero key versions; wipe first (`--force` only if you know k0 is zero) |
| `this is not an NTAG 424 DNA` | wrong tag; Bolt Cards need NTAG 424 DNA |
| `SCARD_W_SECURITY_VIOLATION` (0x8010006A) | polkit refused your user access to pcscd. Arch's pcsclite only auto-allows local *active* logins, so SSH sessions and some Wayland setups are denied. Grant it explicitly (below) |
```sh
sudo tee /etc/polkit-1/rules.d/50-pcscd.rules >/dev/null <<'EOF'
polkit.addRule(function(action, subject) {
if ((action.id == "org.debian.pcsc-lite.access_pcsc" ||
action.id == "org.debian.pcsc-lite.access_card") &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
EOF
```
Takes effect immediately, no restart.
## Nix / NixOS
The repo is a flake. Run it without installing anything:
```sh
nix run git+https://git.atitlan.io/aiolabs/boltcard-writer -- readers
nix run git+https://git.atitlan.io/aiolabs/boltcard-writer -- write '<auth link>'
```
pcscd must be running on the host (`services.pcscd.enable = true;` on NixOS,
which also ships the CCID driver). On NixOS the module does that for you:
```nix
{
inputs.boltcard-writer.url = "git+https://git.atitlan.io/aiolabs/boltcard-writer";
# in your configuration:
imports = [ inputs.boltcard-writer.nixosModules.default ];
programs.boltcard-writer.enable = true; # installs the CLI + pcsc_scan, enables pcscd
}
```
`packages.default` / `overlays.default` expose the package on its own. The
package build runs the test suite, so `nix flake check` doubles as CI. For
hacking, `nix develop` (or `nix-shell`) gives `uv`, Python and `libpcsclite`;
then `uv run boltcard-writer …` and `uv run pytest` work as on Arch.
## Development
```sh
uv run pytest # AN12196 vectors + a software NTAG 424 simulator
uv run ruff check
```
No hardware is needed for the tests. `tests/test_vectors.py` replays the
worked examples from NXP application note AN12196; `tests/simcard.py` is a
minimal software card that verifies MACs, decrypts, changes keys and renders
SDM mirroring, so the write/wipe flows run end to end.
## How it maps to the card
| Step | Command | Comm mode |
|---|---|---|
| NDEF URL with `p=`/`c=` placeholders | ISOSelectFile + ISOUpdateBinary | plain (factory file is free-write) |
| Session | AuthenticateEV2First key 0 | AES, LRP not used |
| SDM on, offsets, write locked to k0 | ChangeFileSettings file 02 | full (`40 00E0 C1 FF12 …`) |
| k1..k4 then k0, key version 1 | ChangeKey | full |
| Verify | ISOReadBinary, decrypt `p` with k1, CMAC with k2 | plain |
LNbits stores `k3 = k1` and `k4 = k2`, as the phone app does. Random-UID mode
(`uid_privacy`) is not used by LNbits and not implemented.
## Limitations
- Linux only (ctypes bindings to `libpcsclite`; no compiled extension).
- Assumes factory keys (all zero) when writing; a card written with other
keys must be wiped with them first.
## License
MIT