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);