write a bolt card to an nfc card with an nfc usb device plugged into your computer
  • Python 95%
  • Nix 4.6%
  • Shell 0.4%
Find a file
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
src/boltcard_writer fix: name SCARD_W_SECURITY_VIOLATION and point at the polkit rule 2026-09-20 22:05:01 +02:00
tests test: AN12196 vectors and a software NTAG 424 simulator 2026-09-20 21:41:24 +02:00
.gitignore feat: Bolt Card writer CLI for PC/SC NFC readers 2026-09-20 21:41:24 +02:00
boltcard-writer feat: launcher script for running from a checkout without uv 2026-09-20 21:58:54 +02:00
flake.lock feat: nix flake with package, NixOS module and dev shell 2026-09-20 21:53:07 +02:00
flake.nix feat: nix flake with package, NixOS module and dev shell 2026-09-20 21:53:07 +02:00
LICENSE feat: Bolt Card writer CLI for PC/SC NFC readers 2026-09-20 21:41:24 +02:00
pyproject.toml feat: Bolt Card writer CLI for PC/SC NFC readers 2026-09-20 21:41:24 +02:00
README.md docs: record the Omnikey 5022 field test 2026-09-20 22:15:32 +02:00
shell.nix feat: nix flake with package, NixOS module and dev shell 2026-09-20 21:53:07 +02:00
uv.lock feat: Bolt Card writer CLI for PC/SC NFC readers 2026-09-20 21:41:24 +02:00

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

  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:
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).

  1. In LNbits, QR icon on the card row → Wipe → copy the JSON (WIPE DATA).
  2. 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