From 1f54a690d0988b688e3b56a3e2d37feb52a9fd8c Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 25 May 2026 20:20:22 -0700 Subject: [PATCH] refactor: own CredentialStore from PersonalizationApplet instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepares for AMD-H by moving the singleton's anchor off a static field onto the PersonalizationApplet instance. The static INSTANCE remains solely as an in-package publish-point so AliroApplet/StepUpApplet keep working unchanged. Adds CredentialStore.bootstrap() (called from PersonalizationApplet's constructor) and CredentialStore.republish() (reserved for the upcoming onRestore hook). Drops the lazy-init path in get() — every install path now goes through PersonalizationApplet first, so INSTANCE is always set by the time AliroApplet looks it up. Test setUps that previously called CredentialStore.get().resetForTesting() before the applet was installed are reordered or dropped: each test now gets a fresh store via the constructor's bootstrap() call. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../aliro/CredentialStore.java | 38 +++++++++++++---- .../aliro/PersonalizationApplet.java | 16 ++++++- .../aliro/AliroAppletAuth1Test.java | 4 +- .../aliro/PersonalizationAppletTest.java | 42 ++++++++++++++++++- .../aliro/StepUpAppletTest.java | 4 +- 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java index 33b1317..f8c5ca6 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java +++ b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java @@ -6,8 +6,18 @@ package com.dangerousthings.aliro; * {@link PersonalizationApplet} writes into it; {@link AliroApplet} reads * from it when it needs long-term keys for AUTH1 / step-up. * - *

Two applets in the same CAP file share this class's static state - * directly — no {@code Shareable} interface needed. + *

Ownership: the single live instance is owned by the + * {@link PersonalizationApplet} instance (created in its constructor via + * {@link #bootstrap()}). The static {@link #INSTANCE} field is just an + * in-package publish-point so AliroApplet/StepUpApplet can find it via + * {@link #get()} without going through a {@code Shareable} SIO. + * + *

The on-instance ownership matters for GlobalPlatform Amendment H + * (Executable Load File Upgrade): AMD-H preserves registered applet + * instances and their reachable object graph but wipes static fields. With + * the live reference held on the PersonalizationApplet instance, an upgrade + * keeps enrollment data; PersonalizationApplet's restore hook then calls + * {@link #republish(CredentialStore)} to re-establish the static alias. * *

After {@link #commit()} is called, further writes via the * personalization interface are refused. The only way to re-unlock is @@ -23,8 +33,11 @@ final class CredentialStore { * accommodates a typical Aliro Access Document with room to spare. */ static final short ACCESS_DOC_MAX_LEN = 1024; - /** Lazily initialized in {@link #get()}. Java Card bans {@code new} in - * static initializers, so we can't declare {@code = new CredentialStore()}. */ + /** Publish-point read by AliroApplet/StepUpApplet via {@link #get()}. + * PersonalizationApplet owns the actual instance; this is just an alias + * so other applets in the same package can find it without SIO. The + * reference is re-published after an AMD-H restore (see + * PersonalizationApplet.onRestore). */ private static CredentialStore INSTANCE; private final byte[] credentialPrivKey; @@ -49,10 +62,21 @@ final class CredentialStore { accessDocument = new byte[ACCESS_DOC_MAX_LEN]; } + /** Called once from {@link PersonalizationApplet}'s constructor. */ + static CredentialStore bootstrap() { + INSTANCE = new CredentialStore(); + return INSTANCE; + } + + /** Called by {@code PersonalizationApplet.onRestore} to re-publish a + * restored store after an AMD-H Executable Load File upgrade, where + * static fields are wiped but the PersonalizationApplet instance (and + * its CredentialStore reference) survive. */ + static void republish(CredentialStore restored) { + INSTANCE = restored; + } + static CredentialStore get() { - if (INSTANCE == null) { - INSTANCE = new CredentialStore(); - } return INSTANCE; } diff --git a/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java b/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java index 9d1545e..fa1ba03 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java +++ b/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java @@ -26,6 +26,18 @@ public class PersonalizationApplet extends Applet { private static final byte INS_FINALIZE_ACCESS_DOC = (byte) 0x24; private static final byte INS_COMMIT = (byte) 0x2C; + /** The owning reference to the shared CredentialStore. AliroApplet and + * StepUpApplet reach it via the {@link CredentialStore#get()} static + * publish-point; we keep the actual reference on this applet instance + * so AMD-H Executable Load File upgrades (which wipe static fields but + * preserve registered applet instances and their reachable object + * state) don't lose enrollment data. */ + private final CredentialStore store; + + public PersonalizationApplet() { + this.store = CredentialStore.bootstrap(); + } + public static void install(byte[] bArray, short bOffset, byte bLength) { PersonalizationApplet applet = new PersonalizationApplet(); if (bArray == null || bLength == 0) { @@ -46,7 +58,9 @@ public class PersonalizationApplet extends Applet { ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); } - CredentialStore store = CredentialStore.get(); + // Use the instance-owned reference rather than the static publish-point + // so this applet keeps working even before/around an AMD-H restore. + CredentialStore store = this.store; byte ins = buf[ISO7816.OFFSET_INS]; switch (ins) { diff --git a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java index 6f404dd..6a88627 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java +++ b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java @@ -30,7 +30,9 @@ class AliroAppletAuth1Test { @BeforeEach void setUp() { - CredentialStore.get().resetForTesting(); + // reader.provision() below installs PersonalizationApplet, whose + // constructor calls CredentialStore.bootstrap() and gives us a fresh + // store for every test — no explicit reset needed any more. sim = new CardSimulator(); AID expeditedAid = new AID(AliroAids.EXPEDITED, (short) 0, (byte) AliroAids.EXPEDITED.length); sim.installApplet(expeditedAid, AliroApplet.class); diff --git a/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java b/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java index a6a6cb9..3204af7 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java +++ b/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java @@ -2,12 +2,15 @@ package com.dangerousthings.aliro; import com.licel.jcardsim.smartcardio.CardSimulator; import javacard.framework.AID; +import javacard.framework.Applet; import javax.smartcardio.CommandAPDU; import javax.smartcardio.ResponseAPDU; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; /** * Tests for the PersonalizationApplet — our proprietary provisioning @@ -28,8 +31,8 @@ class PersonalizationAppletTest { @BeforeEach void setUp() { - CredentialStore.get().resetForTesting(); - + // installApplet → PersonalizationApplet. → CredentialStore.bootstrap() + // gives us a fresh store for every test — no explicit reset needed any more. sim = new CardSimulator(); AID aid = new AID(AliroAids.PROVISIONING, (short) 0, (byte) AliroAids.PROVISIONING.length); sim.installApplet(aid, PersonalizationApplet.class); @@ -162,4 +165,39 @@ class PersonalizationAppletTest { assertEquals(0x6985, send(INS_COMMIT, null).getSW(), "a second COMMIT must also be refused"); } + + @Test + void personalizationAppletOwnsCredentialStoreInstance() throws Exception { + // setUp() has already installed PersonalizationApplet via jcardsim. + // Reach into the runtime and pull out the installed applet instance, + // then assert it owns a non-null `store` instance field and that the + // static publish-point in CredentialStore aliases the same object. + Applet applet = getInstalledApplet(sim, AliroAids.PROVISIONING); + org.junit.jupiter.api.Assertions.assertTrue(applet instanceof PersonalizationApplet, + "installed applet at PROVISIONING AID must be a PersonalizationApplet"); + + java.lang.reflect.Field storeField = PersonalizationApplet.class.getDeclaredField("store"); + storeField.setAccessible(true); + Object instanceStore = storeField.get(applet); + assertNotNull(instanceStore, + "PersonalizationApplet must own a CredentialStore instance field"); + assertSame(instanceStore, CredentialStore.get(), + "Static publish-point must alias the instance-owned store"); + } + + /** Reaches through jcardsim's protected runtime/applet APIs to fetch the + * installed Applet instance for a given AID. Reflection-only — this is + * test infrastructure, not production code. */ + private static Applet getInstalledApplet(CardSimulator sim, byte[] aidBytes) throws Exception { + java.lang.reflect.Field runtimeField = null; + for (Class c = sim.getClass(); c != null && runtimeField == null; c = c.getSuperclass()) { + try { runtimeField = c.getDeclaredField("runtime"); } catch (NoSuchFieldException ignore) {} + } + runtimeField.setAccessible(true); + Object runtime = runtimeField.get(sim); + java.lang.reflect.Method getApplet = runtime.getClass().getDeclaredMethod("getApplet", AID.class); + getApplet.setAccessible(true); + AID aid = new AID(aidBytes, (short) 0, (byte) aidBytes.length); + return (Applet) getApplet.invoke(runtime, aid); + } } diff --git a/applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java b/applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java index 5f38d85..9ddd23c 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java +++ b/applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java @@ -21,7 +21,9 @@ class StepUpAppletTest { @BeforeEach void setUp() { - CredentialStore.get().resetForTesting(); + // StepUpApplet doesn't touch CredentialStore yet; no need to reset + // it here. Once StepUpApplet starts reading credentials, this test + // will need to install PersonalizationApplet first. sim = new CardSimulator(); AID aid = new AID(AliroAids.STEP_UP, (short) 0, (byte) AliroAids.STEP_UP.length); sim.installApplet(aid, StepUpApplet.class);