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:
@@ -4,6 +4,7 @@ import javacard.framework.APDU;
|
||||
import javacard.framework.Applet;
|
||||
import javacard.framework.ISO7816;
|
||||
import javacard.framework.ISOException;
|
||||
import javacard.framework.JCSystem;
|
||||
import javacard.framework.Util;
|
||||
|
||||
/**
|
||||
@@ -62,6 +63,24 @@ public class StepUpApplet extends Applet {
|
||||
|
||||
private static final byte CLA_PROPRIETARY = (byte) 0x80;
|
||||
|
||||
/** Length of each derived Step-Up session key (spec §8.4.3). */
|
||||
private static final short STEP_UP_SK_LEN = 32;
|
||||
|
||||
/** {@code StepUpSKDevice} — UD→reader leg of the Step-Up AES-256-GCM
|
||||
* session, derived from {@code StepUpSK} via HKDF (§8.4.3) when SELECT
|
||||
* finds an armed {@link SessionContext}. Transient, cleared on deselect. */
|
||||
private final byte[] stepUpSKDevice;
|
||||
|
||||
/** {@code StepUpSKReader} — reader→UD leg of the Step-Up GCM session.
|
||||
* Same derivation context as {@link #stepUpSKDevice}. */
|
||||
private final byte[] stepUpSKReader;
|
||||
|
||||
/** 32-byte scratch used only during {@link #select()} to stage the
|
||||
* StepUpSK copied out of {@link SessionContext} before HKDF derives the
|
||||
* two session keys. Wiped after derivation so the parked StepUpSK never
|
||||
* outlives the call. */
|
||||
private final byte[] stepUpSKScratch;
|
||||
|
||||
public static void install(byte[] bArray, short bOffset, byte bLength) {
|
||||
StepUpApplet applet = new StepUpApplet();
|
||||
if (bArray == null || bLength == 0) {
|
||||
@@ -71,8 +90,38 @@ public class StepUpApplet extends Applet {
|
||||
}
|
||||
}
|
||||
|
||||
private StepUpApplet() {
|
||||
// CLEAR_ON_DESELECT: the next deselect (e.g. reader moves back to
|
||||
// EXPEDITED or pulls the field) zeroes the session keys, matching the
|
||||
// "fresh keys per Step-Up phase" invariant we'll need when ENVELOPE /
|
||||
// EXCHANGE handlers run AES-GCM.
|
||||
stepUpSKDevice = JCSystem.makeTransientByteArray(STEP_UP_SK_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||
stepUpSKReader = JCSystem.makeTransientByteArray(STEP_UP_SK_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||
stepUpSKScratch = JCSystem.makeTransientByteArray(STEP_UP_SK_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* SELECT entry point. If {@link SessionContext} is armed (i.e. AUTH1 on
|
||||
* {@link AliroApplet} just succeeded and parked {@code StepUpSK}), copy
|
||||
* it out and derive {@code StepUpSKDevice}/{@code StepUpSKReader} via
|
||||
* the §8.4.3 HKDF so the ENVELOPE / EXCHANGE handlers in M1B / M1C can
|
||||
* AES-256-GCM with them. Always returns true — an un-armed SELECT (e.g.
|
||||
* a reader that touches the Step-Up AID before AUTH1) still gets a
|
||||
* successful FCI; downstream handlers will reject commands that need a
|
||||
* live session.
|
||||
*/
|
||||
@Override
|
||||
public boolean select() {
|
||||
if (SessionContext.isStepUpArmed()) {
|
||||
SessionContext.copyStepUpSK(stepUpSKScratch, (short) 0);
|
||||
CryptoSingletons.getAliroCrypto().deriveStepUpSessionKeys(
|
||||
stepUpSKScratch, (short) 0,
|
||||
stepUpSKDevice, (short) 0,
|
||||
stepUpSKReader, (short) 0);
|
||||
// Wipe the staged StepUpSK — the derived keys are sufficient
|
||||
// from here on and we don't want the IKM lingering in transient.
|
||||
Util.arrayFillNonAtomic(stepUpSKScratch, (short) 0, STEP_UP_SK_LEN, (byte) 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -117,4 +166,17 @@ public class StepUpApplet extends Applet {
|
||||
buf[outerLenPos] = (byte) (off - 2);
|
||||
apdu.setOutgoingAndSend((short) 0, off);
|
||||
}
|
||||
|
||||
// --- Testing hooks ------------------------------------------------------
|
||||
// Package-private accessors that let tests verify SELECT-time key
|
||||
// derivation without exposing the session keys to any production caller.
|
||||
// NOT for use outside the applet's own test module.
|
||||
|
||||
void copyStepUpSKDeviceForTesting(byte[] out, short outOff) {
|
||||
Util.arrayCopyNonAtomic(stepUpSKDevice, (short) 0, out, outOff, STEP_UP_SK_LEN);
|
||||
}
|
||||
|
||||
void copyStepUpSKReaderForTesting(byte[] out, short outOff) {
|
||||
Util.arrayCopyNonAtomic(stepUpSKReader, (short) 0, out, outOff, STEP_UP_SK_LEN);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user