9 KiB
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
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)
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:
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):
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
- In LNbits open the Bolt Cards extension and create the card as usual
(name, limits, card UID: get it with
readabove, keys auto-generated). - 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. - Put a blank card on the reader and run:
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:
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
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).
- In LNbits, QR icon on the card row → Wipe → copy the JSON (
WIPE DATA). - Save it to a file, or pipe it:
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) |
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:
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:
{
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
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