Expands M2A.3 from the original plan because the credential issuer pubkey had no provisioning path. Adds INS_SET_CREDENTIAL_ISSUER_PUBK = 0x25 to PersonalizationApplet, bumps CredentialStore FIELD_VERSION 2 -> 3 to add a 64 B credentialIssuerPubKey slot + set-flag, and pivots finalizeAccessDocument to run a one-shot CoseVerifier RFC 9052 COSE_Sign1 verify against the staged AD bytes before atomically setting accessDocumentLen + accessDocumentFinalized + accessDocumentVerified inside a JCSystem transaction. PersonalizationApplet holds one CoseVerifier instance constructed at install (the 768 B CLEAR_ON_DESELECT scratch only allocates once) plus a 65 B EEPROM scratch for staging the uncompressed issuer pubkey. Status word mapping: - missing issuer pubkey at finalize -> SW_CONDITIONS_NOT_SATISFIED (0x6985) - IssuerAuth signature mismatch -> SW_DATA_INVALID (0x6984) - length out of range -> SW_WRONG_DATA (0x6A80) On verify failure the store stays untouched -- both accessDocumentFinalized and accessDocumentVerified remain false. Harness side: TrustArtifacts gains credential_issuer_pub (loaded from trust_dir/issuer.pem). The orchestrator sends INS 0x25 after the existing key writes and before the AD chunks so the trust anchor is staged by the time FINALIZE_AD runs. Test vector: AD bytes + matching issuer pubkey x||y are hardcoded into PersonalizationAppletTest from the canonical trustgen artifacts at /home/work/aliro-trust (272 B AD against a known issuer.pem). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 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.
pcscdrunning (sudo systemctl start pcscdon 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 — 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 bytesout/reader.pem→ derive pub bytesout/issuer.pem→ derive Credential Issuer pubkeyout/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 Nto pick.
Failure modes:
- "trust dir not found" / missing
reader.pem— pass the right--trust-dir, or regenerate withaliro-trustgen init. - "No reader with a card" — card not powered on the field, or the
wrong reader index.
--list-readershelps. SW=6985on SELECT-EXPEDITED — card wasn't personalized (no keys COMMITted). Runaliro-personalizefirst.signature verification failedafter AUTH1 — thecredential_PubKreturned by the card doesn't matchaccess_credential.pemin 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_PubKin 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,pcscdmust 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 — checkgp --listand 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
newin clinit": regression from someone adding astatic final ... = new ...()initializer. JC 3.0.5 forbids these. Move allocation to a constructor or to lazy init inside an instance method.