Contents
What you need
- Blank J3R180 cards Use NXP J3R180 cards (JCOP 4, Java Card 3.0.5) with contact and contactless interfaces, unfused and initialized, on the factory keys
40to4F. The shops under Where to get cards sell them. - A card reader Any PC/SC reader for ISO 7816-4 cards works, contact or contactless. The project tests with the ACS ACR122U, a USB reader for about $40 to $50, sold on Amazon and by GoToTags.
- A computer You need a terminal, Git, Java 11 or later and GlobalPlatformPro 20.01.23 or later. Step 1 installs Java and GlobalPlatformPro.
Install the tools
On a Mac, PC/SC ships with the system. Install Java with Homebrew:
brew install openjdk@17
On Debian or Ubuntu, install the PC/SC daemon and make sure pcscd is running. Install Java 11 or later from your distribution too.
sudo apt install pcscd libpcsclite-dev
Then install GlobalPlatformPro, the tool that talks to the card's manager. These commands download it and add a gp command:
curl -L https://github.com/martinpaljak/GlobalPlatformPro/releases/latest/download/gp.jar \
-o /usr/local/bin/gp.jar
cat > /usr/local/bin/gp << 'EOF'
#!/bin/sh
exec java -jar /usr/local/bin/gp.jar "$@"
EOF
chmod +x /usr/local/bin/gp
Get the applet
The applet ships as one CAP file, applet version 2.1, at applet/target/cashu-javacard-2.1.0.cap in the project's repository. The file holds three applets: an opener, the card's applet and an NFC tag.
git clone https://github.com/cashucard/cashucard
cd cashucard/applet
Check that the file is the published one before you install it:
sha256sum target/cashu-javacard-2.1.0.cap
f27d92ba99581f4e42abda0efc038b2c31128f9b405bcec6de52f6a12ae4c7e5 target/cashu-javacard-2.1.0.capOn a macOS release without sha256sum, shasum -a 256 prints the same line.
Build the CAP yourself
Building needs Apache Ant 1.10 or later, Python 3.9 or later and a Java Card 3.0.5 kit. Fetch the kits once, about 200 MB:
git clone https://github.com/martinpaljak/oracle_javacard_sdks ~/.javacard/sdks
Then build in applet/. The build looks for the kit at ~/.javacard/sdks/jc305u4_kit, and -Djc.sdk names another place:
ant cap
ant cap -Djc.sdk=/path/to/jc305u4_kit
A CAP you build hashes differently from the published one, even from the same source, because the converter writes a timestamp into META-INF/MANIFEST.MF. Compare every other entry with the published file:
git show HEAD:applet/target/cashu-javacard-2.1.0.cap > /tmp/tracked.cap
rm -rf /tmp/cap-tracked /tmp/cap-built
unzip -q /tmp/tracked.cap -d /tmp/cap-tracked
unzip -q target/cashu-javacard-2.1.0.cap -d /tmp/cap-built
diff -r -x MANIFEST.MF /tmp/cap-tracked /tmp/cap-built && echo "same CAP as the tracked one"
same CAP as the tracked oneIf diff prints a difference, install the published CAP instead.
Check the card
Put a card on the reader. First read what its card manager reports about itself. This command only reads:
gp -i -v
Supports SCP02 i=15 i=35 i=55 i=75
GP SCP02 (i=55)It also lists key set version 01, with three DES3 keys.
Then log in and list what the card holds. With no keys given, gp logs in with the factory keys:
gp --list
ISD: A000000151000000 (OP_READY)Stop at a refused login. A failed login counts towards a lock that is for good. If the card refuses gp's keys, put that card aside.
Install the applet
The chip takes the card's applet only once another applet from the same package exists, so the install runs in three steps: the small opener first, then the card's applet, then the NFC tag under the NFC Forum's own name.
- 1Opener
F04341534855434152444D - 2Card's applet
F043415348554341524401 - 3NFC tag
D2760000850101
gp --install target/cashu-javacard-2.1.0.cap --applet F04341534855434152444D
gp --create F043415348554341524401 --package F0434153485543415244 --applet F043415348554341524401
gp --create D2760000850101 --package F0434153485543415244 --applet F043415348554341524454
List the card again. The three applets are there, among the rest:
gp --list
APP: F04341534855434152444D (SELECTABLE) ← the opener
APP: F043415348554341524401 (SELECTABLE) ← the card's applet
APP: D2760000850101 (SELECTABLE) ← the tagCheck the install
Select the card's applet by its whole AID. It answers with its version, 2.1, and 9000 for success:
gp --apdu 00A404000BF043415348554341524401
0201 9000Read its state with GET_INFO. A new card shows 128 empty slots, with its PIN, owner and record still unset:
gp --apdu B0010000
02 01 80 00 00 80 FF 00 04 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
SW: 9000Read in order: version 2.1, 128 slots, 0 unspent, 0 spent, 128 empty, capabilities FF, no PIN, card file format 4, 3 PIN tries, unlocked, and no record, limit, owner, clock, window or change grant.
Read the card's public key with GET_PUBKEY:
gp --apdu B0100000
A 33-byte compressed secp256k1 public key, then 9000.
Last, read the NFC tag the way a phone does: its name, the capability container and the NDEF file.
gp -d -a 00A4040007D2760000850101 -a 00A4000C02E103 -a 00B000000F -a 00A4000C02E104 -a 00B0000002 -a 00B0000214
9000 / 9000 / 000F20003B00340406E104001600FF 9000 / 9000 / 0014 9000 / D101105504 + "getfoxy.io/card" 9000When every answer matches, the card is ready to set up.
Set the card up
A card fresh from the install is empty and unowned, and any reader in range can give it a PIN, a record and an owner. Set it up before it leaves your hands.
When you give a card to someone, they set it up from their own phone, in a wallet that supports Cashu Card, with Set up this card. The wallets are listed under Apps at the foot of this page.
Set up a bench card with cardctl
cardctl, the project's command-line driver, needs Python 3 with pyscard. On a Mac, use a Homebrew Python, because the system one cannot load pyscard. From the repository's root folder:
cd tools/cardctl
brew install python@3.13
python3.13 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Then set the card up, owner last, with the tip block header as the anchor:
python3 cardctl.py set-pin 1234
python3 cardctl.py verify-pin 1234
python3 cardctl.py set-card --mint <your mint's URL>
python3 cardctl.py set-owner --owner-key <32-byte hex> --anchor <the tip header, 80 bytes hex>
python3 cardctl.py set-limit --owner-key <…> --day 50000 --payment 0 --allowance 0 --budget 7
python3 cardctl.py selftest
Use this on bench cards. When --owner-key is left out, cardctl signs with its TEST key, which anyone with the repository holds.
cardctl selftest passes a card that answers 02 01 or a later 2.x version.
Reinstall or remove
Spend the balance first. Every piece on a card is locked to the card's key, and a reinstall makes a new key. Spend a card's balance before you delete its applet.
Delete the instances one at a time, the tag first and the package last. The chip refuses to delete an instance that something still refers to, which sets the order:
gp --delete D2760000850101 # the tag
gp --delete F043415348554341524401 # the card's applet
gp --delete F04341534855434152444D # the opener
gp --delete F0434153485543415244 # the package
Then install again, the opener first and the tag last:
gp --install target/cashu-javacard-2.1.0.cap --applet F04341534855434152444D
gp --create F043415348554341524401 --package F0434153485543415244 --applet F043415348554341524401
gp --create D2760000850101 --package F0434153485543415244 --applet F043415348554341524454
Troubleshooting
gp --installfails with6F00- Check the order in step 4: the chip takes the card's applet only after the opener. With the order right, suspect the size of the load file next, then the applet's install-time check of how the chip frames its ECDH output.
- SELECT answers
6A82 - The card has an earlier applet, 0.x or 1.x, and needs an update.
- SELECT answers
9000with no version - That SELECT named the package AID alone,
F0434153485543415244, which reaches the opener, and the opener answers6D00to every command after it. Select the whole AID,F043415348554341524401, as in step 5. - The card refuses gp's keys
- Put the card aside, as step 3 explains.
- The CAP will not load on a Java Card 3.0.4 card
- The applet signs with
KeyAgreement.ALG_EC_SVDP_DH_PLAIN_XY, which arrived in Java Card 3.0.5, so a 3.0.4 card cannot run it. Use a J3R180. - cardctl stops with
ImportError: dynamic module does not define module export function (PyInit__scard) - That is the macOS system Python. Use a Homebrew Python, as the bench setup in step 6 does.
Reference
Applet IDs
| Element | AID (hex) |
|---|---|
| Package | F0 43 41 53 48 55 43 41 52 44 |
| Opener, made first | F0 43 41 53 48 55 43 41 52 44 4D |
| Card's applet | F0 43 41 53 48 55 43 41 52 44 01 |
| Tag, class (listed last in the file) | F0 43 41 53 48 55 43 41 52 44 54 |
| Tag, instance (the NFC Forum's NDEF Tag Application, which a phone selects) | D2 76 00 00 85 01 01 |
| SELECT of the card's applet, by its whole AID | 00 A4 04 00 0B F0 43 41 53 48 55 43 41 52 44 01 |
| SELECT of the package AID alone, which reaches the opener | 00 A4 04 00 0A F0 43 41 53 48 55 43 41 52 44 |
Supported cards
| Card | Java Card | Status |
|---|---|---|
| NXP JCOP4 SmartMX3 P71 (J3R180), from any seller | 3.0.5 | Primary target, with the secp256k1 custom curve supported |
| Generic Java Card 3.0.5 or later | 3.0.5 | May work; check its custom EC-FP curve and ALG_EC_SVDP_DH_PLAIN_XY support first |
| Feitian's own Java Card 3.0.4 cards | 3.0.4 | Cannot run it: it lacks ALG_EC_SVDP_DH_PLAIN_XY, so the CAP will not convert or load |
| Java Card 3.0.4 or earlier | 3.0.4 and earlier | Cannot run it: no ECDH plain-XY |
| Java Card 2.2.x | 2.2 | Cannot run it: no int type and no ECDH plain-XY |