# 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](https://github.com/martinpaljak/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 ` 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 `--create`d 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 ` 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 — runs IssuerAuth COSE_Sign1 verify | | `0x25` | `0000` | 64B credential_issuer_PubK (x\|\|y) | Credential Issuer pubkey — trust anchor for IssuerAuth verify | | `0x2C` | `0000` | (none) | COMMIT — locks all writes | Required order: SELECT provisioning AID → write all keys (including credential_issuer_PubK) + AD chunks → finalize → COMMIT. The issuer pubkey MUST be set before FINALIZE — without it FINALIZE returns `SW_CONDITIONS_NOT_SATISFIED`. A signature mismatch at FINALIZE returns `SW_DATA_INVALID` and leaves the AD un-finalized. 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/issuer.pem` → derive Credential Issuer pubkey - `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 ` 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 ` 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.