feat(applet): StepUpApplet derives session keys on SELECT when armed
After AUTH1 parks StepUpSK in SessionContext, the reader issues SELECT to the Step-Up AID per spec §10.2. StepUpApplet.select() now picks up the parked SK, derives StepUpSKDevice / StepUpSKReader via the §8.4.3 HKDF (already implemented in AliroCrypto.deriveStepUpSessionKeys), and stages both in transient CLEAR_ON_DESELECT fields for the ENVELOPE (M1C.1) and EXCHANGE (M1B.1) handlers. Introduces CryptoSingletons -- a lazy package-private holder for the single AliroCrypto instance shared between AliroApplet and StepUpApplet. Saves ~352 B of transient (kdfWorkbuf + hkdfPrevT + expandScratch) versus a per-applet duplicate. Java Card forbids new in <clinit> so the singleton uses lazy null-check init. Opt 1 prelude per docs/plans/2026-06-11-step-up-implementation-v2.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
package com.dangerousthings.aliro;
|
||||
|
||||
import com.licel.jcardsim.smartcardio.CardSimulator;
|
||||
import java.security.KeyPair;
|
||||
import javacard.framework.AID;
|
||||
import javacard.framework.Applet;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
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.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
@@ -66,6 +71,105 @@ class StepUpAppletTest {
|
||||
"wrong CLA must return SW_CLA_NOT_SUPPORTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* After AUTH1 succeeds on {@link AliroApplet}, {@code SessionContext} is
|
||||
* armed with the 32-byte {@code StepUpSK}. When the reader subsequently
|
||||
* SELECTs the Step-Up AID (per spec §10.2.x and the empirical X-CUBE-ALIRO
|
||||
* flow), {@link StepUpApplet#select()} must derive
|
||||
* {@code StepUpSKDevice}/{@code StepUpSKReader} via
|
||||
* {@link AliroCrypto#deriveStepUpSessionKeys} and stage both in
|
||||
* {@code CLEAR_ON_DESELECT} transient fields. This pins the wiring end-to-end.
|
||||
*/
|
||||
@Test
|
||||
void selectAfterArmedAuth1DerivesStepUpSessionKeys() throws Exception {
|
||||
// Full applet stack: PersonalizationApplet (via ReaderSide.provision),
|
||||
// AliroApplet under EXPEDITED, StepUpApplet under STEP_UP. The bare
|
||||
// StepUpApplet install done by setUp() is overwritten by re-installing
|
||||
// here; jcardsim treats the AID install as idempotent for our purposes.
|
||||
sim = new CardSimulator();
|
||||
AID expeditedAid = new AID(AliroAids.EXPEDITED, (short) 0, (byte) AliroAids.EXPEDITED.length);
|
||||
sim.installApplet(expeditedAid, AliroApplet.class);
|
||||
AID stepUpAid = new AID(AliroAids.STEP_UP, (short) 0, (byte) AliroAids.STEP_UP.length);
|
||||
sim.installApplet(stepUpAid, StepUpApplet.class);
|
||||
|
||||
KeyPair credentialKeyPair = Auth0Command.generateEphemeralKeyPair();
|
||||
ReaderSide reader = new ReaderSide();
|
||||
reader.provision(sim, credentialKeyPair);
|
||||
|
||||
// SELECT expedited, run AUTH0 + AUTH1 (mirrors
|
||||
// AliroAppletAuth1Test.auth1SuccessArmsStepUpContextWithExpectedStepUpSK).
|
||||
assertEquals(0x9000, sim.transmitCommand(
|
||||
new CommandAPDU(0x00, 0xA4, 0x04, 0x00, AliroAids.EXPEDITED, 256)).getSW(),
|
||||
"SELECT expedited must succeed");
|
||||
|
||||
reader.startTransaction();
|
||||
ResponseAPDU auth0Resp = sim.transmitCommand(new CommandAPDU(
|
||||
Auth0Command.CLA & 0xFF, Auth0Command.INS & 0xFF, 0x00, 0x00,
|
||||
reader.buildAuth0Data(), 256));
|
||||
assertEquals(0x9000, auth0Resp.getSW(), "AUTH0 must succeed");
|
||||
byte[] credentialEphemPubKey = TlvUtil.findTopLevel(auth0Resp.getData(), 0x86);
|
||||
|
||||
ResponseAPDU auth1Resp = sim.transmitCommand(new CommandAPDU(
|
||||
Auth0Command.CLA & 0xFF, 0x81, 0x00, 0x00,
|
||||
reader.buildAuth1Data(credentialEphemPubKey), 256));
|
||||
assertEquals(0x9000, auth1Resp.getSW(), "AUTH1 must succeed");
|
||||
|
||||
// Compute the StepUpSK the test knows the card holds: it's bytes
|
||||
// 64..96 of the expedited-standard derived_keys_volatile material.
|
||||
byte[] stepUpSK = java.util.Arrays.copyOfRange(
|
||||
reader.deriveExpeditedKeyMaterial(credentialEphemPubKey), 64, 96);
|
||||
|
||||
// SELECT the Step-Up AID — this should trigger StepUpApplet.select(),
|
||||
// which sees SessionContext armed, copies out StepUpSK, derives the
|
||||
// two session keys, and stashes them.
|
||||
assertEquals(0x9000, sim.transmitCommand(
|
||||
new CommandAPDU(0x00, 0xA4, 0x04, 0x00, AliroAids.STEP_UP, 256)).getSW(),
|
||||
"SELECT step-up must succeed regardless of armed state");
|
||||
|
||||
// Reference HKDF-SHA-256 with empty salt — matches AliroCrypto.deriveStepUpSessionKeys.
|
||||
byte[] expectedDevice = hkdfStepUp(stepUpSK, "SKDevice");
|
||||
byte[] expectedReader = hkdfStepUp(stepUpSK, "SKReader");
|
||||
|
||||
byte[] gotDevice = new byte[32];
|
||||
byte[] gotReader = new byte[32];
|
||||
StepUpApplet appletInstance = stepUpAppletInstance(sim, stepUpAid);
|
||||
appletInstance.copyStepUpSKDeviceForTesting(gotDevice, (short) 0);
|
||||
appletInstance.copyStepUpSKReaderForTesting(gotReader, (short) 0);
|
||||
|
||||
assertArrayEquals(expectedDevice, gotDevice,
|
||||
"StepUpSKDevice = HKDF-Expand(HKDF-Extract(empty, StepUpSK), \"SKDevice\", 32)");
|
||||
assertArrayEquals(expectedReader, gotReader,
|
||||
"StepUpSKReader = HKDF-Expand(HKDF-Extract(empty, StepUpSK), \"SKReader\", 32)");
|
||||
}
|
||||
|
||||
/** HKDF-SHA-256 with empty salt and single-block Expand (L=32). Mirrors
|
||||
* AliroCrypto.deriveStepUpSessionKeys's spec-pinned HKDF computation. */
|
||||
private static byte[] hkdfStepUp(byte[] ikm, String info) throws Exception {
|
||||
Mac extract = Mac.getInstance("HmacSHA256");
|
||||
extract.init(new SecretKeySpec(new byte[32], "HmacSHA256"));
|
||||
byte[] prk = extract.doFinal(ikm);
|
||||
Mac expand = Mac.getInstance("HmacSHA256");
|
||||
expand.init(new SecretKeySpec(prk, "HmacSHA256"));
|
||||
expand.update(info.getBytes("US-ASCII"));
|
||||
expand.update((byte) 0x01);
|
||||
return expand.doFinal();
|
||||
}
|
||||
|
||||
/** Pull the live StepUpApplet instance out of jcardsim so the test can
|
||||
* reach the package-private testing accessors. Mirrors the reflection
|
||||
* pattern in {@code PersonalizationAppletTest#getInstalledApplet}. */
|
||||
private static StepUpApplet stepUpAppletInstance(CardSimulator sim, AID aid) 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);
|
||||
return (StepUpApplet) (Applet) getApplet.invoke(runtime, aid);
|
||||
}
|
||||
|
||||
/** Unwraps the outer 6F FCI template to expose its nested TLVs. */
|
||||
private static byte[] unwrap6F(byte[] fci) {
|
||||
if (fci.length < 2 || fci[0] != 0x6F) {
|
||||
|
||||
Reference in New Issue
Block a user