Files
aliro-project/applet/INSTALL.md
michael 9081990b0e feat(harness): aliro-bench-test --step-up + INSTALL.md install-recovery notes
Adds a PC/SC verification path for Step-Up Milestone 1 so M1 can be
validated against a personalized card without depending on Nucleo /
X-CUBE-ALIRO bring-up. Verdict run on J3R452 04555A4A0B2190 with M1
CAP installed: AUTH1 OK (3.4s), EXCHANGE 0xC9 OK, ENVELOPE 0xC3 OK
with response decrypting under StepUpSKDevice to the spec 0xA0 ack.

- crypto.py: derive_step_up_session_keys (HKDF parity with
  AliroCrypto.deriveStepUpSessionKeys)
- transaction.py: expose step_up_sk on TransactionResult
- step_up.py: verify_step_up_m1 -- SELECT 5502 + GCM-encrypted
  C9/C3 round-trip, IVs per StepUpApplet (0x00*8 || counter
  reader-side; 0x00*7 || 0x01 || counter device-side)
- cli.py: --step-up flag on aliro-bench-test
- tests: stdlib-RFC-5869 cross-check on the new KDF (122/122 green)
- INSTALL.md: fix multi-place PKG AID typo (missing 02 version byte),
  document partial-install recovery, document package-static
  credential store (aliro-personalize success !=> 5501/5502 installed)
2026-06-17 12:55:16 -07:00

9.9 KiB

Installing the Aliro applet on a real Java Card

Tested target: NXP J3R180 (JC 3.0.5, GP 2.3) over a USB PC/SC reader. Other GP-2.2+ JC 3.0.5+ cards should work too.

What you'll need

  • A USB PC/SC reader (e.g. ACR1252U, ACR122U, OMNIKEY 5022).

  • A Java Card with default GlobalPlatform test keys still in place (any freshly-bought J3R180 from VivoKey-friendly suppliers ships this way).

  • Java 11+ on the host. pcscd running (sudo systemctl start pcscd on Linux).

  • GlobalPlatformPro (gp.jar — single fat jar, no install needed):

    curl -sLO https://github.com/martinpaljak/GlobalPlatformPro/releases/latest/download/gp.jar
    

Build the CAP

cd applet
./scripts/dt-mvn.sh clean package -DskipTests

Output: applet/target/aliro-applet-j3r180.cap (~42 KB). Built inside the vivokey/smartcard-ci container by the ant-javacard plugin against the JC 3.0.5 SDK. Add -Pj3r452 to build the AMD-H variant (aliro-applet-j3r452.cap) instead; the j3r180 default is what's shipped in the field today, so use it if you're not sure.

Install

java -jar gp.jar --info                          # confirm reader sees the card

# Multi-applet CAPs: load the package once, then create each instance.
# --create needs the instance AID, the applet class AID (same as instance
# here), and the package AID (printed by --load).
PKG=A0000009094454414C49524F02
java -jar gp.jar --load applet/target/aliro-applet-j3r180.cap
java -jar gp.jar --create A000000909ACCE5501   --applet A000000909ACCE5501   --package $PKG
java -jar gp.jar --create A000000909ACCE5502   --applet A000000909ACCE5502   --package $PKG
java -jar gp.jar --create A000000909ACCE559901 --applet A000000909ACCE559901 --package $PKG

(--install <cap> alone fails with "CAP contains more than one applet, specify the right one with --[applet]", and --create AID without --applet/--package fails with "Need --[package, pkg] and --[applet] or --[cap]".)

Partial install / recovery: if a --create call fails mid-sequence (card got reset, USB hiccup, RF dropout), the package stays loaded. Just re-run the failed --create — no need to redo --load or the earlier --create calls. The package only needs --delete + --load again if the package itself is in a bad state.

Credential store is package-static: keys and the Access Document written via aliro-personalize live on a static class shared by all three applets. Personalization only requires ACCE559901 to be SELECTABLE; 5501 and 5502 can be --created after personalize ran and they'll see the same credentials. (Note: aliro-personalize returning "Personalization complete" only confirms 559901 accepted the writes — always cross-check gp --list to confirm 5501 and 5502 are also SELECTABLE.)

Default GP test keys (404142434445464748494A4B4C4D4E4F) are tried automatically. For non-default keys, pass --key <hex> to every command.

Three applets get registered, one CAP:

AID Class Role
A0000009 09 ACCE 55 01 AliroApplet Aliro EXPEDITED phase (spec)
A0000009 09 ACCE 55 02 StepUpApplet Aliro STEP_UP phase (spec; scaffold only ATM)
A0000009 09 ACCE 55 99 01 PersonalizationApplet DT-internal provisioning channel

All three share the package AID A0000009 09 4454414C49524F 02 (CSA RID

  • ASCII "DTALIRO" + version byte). JavaCard requires applets in one CAP to share the package's RID, hence the proprietary AID for personalization is also under the CSA RID with a non-spec PIX (99 01).

Verify

java -jar gp.jar --list

You should see all three AIDs in the Application list.

Personalize

After install the card holds three applets but no Access Credential keys or Access Document — every Aliro flow returns SW_CONDITIONS_NOT_SATISFIED.

Provision via the PersonalizationApplet (CLA 0x80):

INS P1|P2 Data Description
0x20 0000 32B credential_PrivK Access Credential long-term privkey
0x21 0000 64B credential_PubK (x||y) …matching pubkey
0x22 0000 64B reader_PubK (x||y) reader long-term pubkey
0x23 offset (BE) up to 255B chunk Access Document chunk write
0x24 total_len (BE) (none, Lc=0) Finalize Access Document
0x2C 0000 (none) COMMIT — locks all writes

Required order: SELECT provisioning AID → write all keys + AD chunks → finalize → COMMIT. After COMMIT, every write returns SW_CONDITIONS_NOT_SATISFIED (no factory-reset mechanism in v1).

Source bytes come from aliro-trustgen init --out-dir ./out:

  • out/access_credential.pem → derive priv/pub bytes
  • out/reader.pem → derive pub bytes
  • out/access_document.bin → chunk into ≤255B writes

One-shot personalization: the harness ships an aliro-personalize CLI that walks the entire INS sequence above against a PC/SC reader:

pip install -e harness/    # one-time, if not already installed
aliro-trustgen init --out-dir ./trust-out
aliro-personalize --trust-dir ./trust-out

Use aliro-personalize --list-readers to enumerate readers if more than one is plugged in, then --reader N to pick by index.

The CLI fails fast on the first non-9000 status word and tells you which step. After COMMIT, the card refuses every further write — there is no factory-reset path in v1, so verify the trust artifacts before running.

Validate after personalization

Once personalization has COMMITted, aliro-bench-test drives a full AUTH0+AUTH1 exchange against the card from a Python orchestrator over PC/SC. It's the real-hardware validation path that works without any reader firmware (X-CUBE-ALIRO on X-NUCLEO-NFC09A1 is still pending).

pip install -e harness/    # one-time, same package that ships aliro-personalize
aliro-bench-test --trust-dir ~/aliro-trust

The --trust-dir must point at the same aliro-trustgen init output that personalization used (it reads reader.pem to sign the AUTH1 challenge and access_credential.pem to verify the UD signature the applet returns).

Successful output looks like:

Using reader: ACS ACR1252 1S CL Reader 0
Connected. ATR: 3B8F8001804F0CA000000306030001000000006A
Loaded trust artifacts from /home/you/aliro-trust
RESULT: OK — applet round-trip on real hardware.
  0x5A credential_PubK: 65B
  0x9E UD signature: 64B
  0x5E signaling_bitmap: 0x0000

0x5E signaling_bitmap is 0x0000 when no Access Document was provisioned and 0x0005 when one was (bit 0 = "Access Document retrievable", bit 2 = "retrieval requires step-up AID SELECT" — see Aliro Table 8-11).

Useful flags:

  • --list-readers — enumerate PC/SC readers, then --reader N to pick.

Failure modes:

  • "trust dir not found" / missing reader.pem — pass the right --trust-dir, or regenerate with aliro-trustgen init.
  • "No reader with a card" — card not powered on the field, or the wrong reader index. --list-readers helps.
  • SW=6985 on SELECT-EXPEDITED — card wasn't personalized (no keys COMMITted). Run aliro-personalize first.
  • signature verification failed after AUTH1 — the credential_PubK returned by the card doesn't match access_credential.pem in the trust dir. Either the card was personalized from a different trust bundle, or the bundle on disk was regenerated post-personalization. There is no factory-reset in v1 — re-personalize on a fresh card.
  • AES-GCM decrypt failure — ExpeditedSKDevice mismatch; usually means the reader and applet derived different Kdh (check that reader_PubK in the trust dir matches the one that was provisioned).

This exercises the full EXPEDITED protocol end-to-end — every crypto step the real firmware will eventually run — so a green bench-test is strong evidence the applet is correct independently of any future reader implementation.

Uninstall / re-install

gp --delete <pkg_AID> won't succeed while applet instances still reference the package, and --deletedeps isn't recognised by every gp.jar build. The portable sequence is to delete each instance first, then the package:

java -jar gp.jar --delete A000000909ACCE5501
java -jar gp.jar --delete A000000909ACCE5502
java -jar gp.jar --delete A000000909ACCE559901
java -jar gp.jar --delete A0000009094454414C49524F02

Then --load + the three --create commands above to reinstall the fresh CAP.

--uninstall <cap> is also supported (parses the CAP for the AIDs and deletes them) but only if the CAP file is reachable from the current directory, since gp resolves it as a path:

java -jar gp.jar --uninstall ./aliro-applet-j3r180.cap

Troubleshooting

  • "No reader with a card found": confirm pcsc_scan (Linux/macOS) sees both reader and card. On Linux, pcscd must be running.
  • SW=6985 (Conditions not satisfied) on Aliro commands: provisioning hasn't been COMMITted yet, or no AUTH0 preceded an AUTH1.
  • SW=6A82 (File not found) on SELECT: applet didn't install — check gp --list and re-run --install.
  • Auth failure on gp --install: card uses non-default GP keys. Either find the issuer keys or factory-reset (vendor-specific).
  • CAP build error "unsupported bytecode new in clinit": regression from someone adding a static final ... = new ...() initializer. JC 3.0.5 forbids these. Move allocation to a constructor or to lazy init inside an instance method.