feat(applet): StepUpApplet INS_EXCHANGE decrypt-and-discard with 9000 ack
X-CUBE-ALIRO firmware sends a Reader Status sub-event via EXCHANGE (CLA=0x80, INS=0xC9) after the step-up AID SELECT, per Aliro §8.3.3.5 Table 8-14. The payload is encrypted with StepUpSKReader using AES-256-GCM with IV = 0x0000000000000000 || stepup_reader_counter (4B BE) per §8.3.1.8. For Milestone 1 we decrypt-and-discard: tag verification proves we have matching session keys, then we return SW=9000 with empty payload. M2 will add a proper encrypted Reader Status response sub-event. Extends CryptoSingletons to hold the shared AliroGcm instance too -- opt 1 sibling of the AliroCrypto sharing from M1A.2. Adds AliroGcm.decrypt since the prior pipeline only encrypted (AUTH1 response path). Counter starts at 1 per session-bound init (spec §8.4.3 -> mdoc [6] §9.1.1.5), incremented after each successful decrypt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import com.licel.jcardsim.smartcardio.CardSimulator;
|
||||
import java.security.KeyPair;
|
||||
import javacard.framework.AID;
|
||||
import javacard.framework.Applet;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.smartcardio.CommandAPDU;
|
||||
import javax.smartcardio.ResponseAPDU;
|
||||
@@ -142,6 +144,81 @@ class StepUpAppletTest {
|
||||
"StepUpSKReader = HKDF-Expand(HKDF-Extract(empty, StepUpSK), \"SKReader\", 32)");
|
||||
}
|
||||
|
||||
/**
|
||||
* After SELECT-Step-Up has armed {@code StepUpSKReader}, the X-CUBE-ALIRO
|
||||
* firmware sends a "Reader Status sub-event" via the EXCHANGE command
|
||||
* (CLA=0x80, INS=0xC9) per spec §8.3.3.5 / Table 8-14. The payload is
|
||||
* AES-256-GCM encrypted with {@code StepUpSKReader}; IV layout from
|
||||
* §8.3.1.8 is {@code 0x0000000000000000 || stepup_reader_counter (4B BE)},
|
||||
* with the counter session-bound and initialized to 1 per §8.4.3 (mdoc
|
||||
* [6] §9.1.1.5 derivation).
|
||||
*
|
||||
* <p>For Milestone 1 the applet only needs to decrypt-and-discard: tag
|
||||
* verification proves the session keys match, then we return SW=9000 with
|
||||
* empty payload. The real Step-Up "Reader Status response sub-event"
|
||||
* (encrypted with StepUpSKDevice) lands in M2.
|
||||
*/
|
||||
@Test
|
||||
void exchangeAfterStepUpSelectDecryptsAndAcksWithEmptyPayload() throws Exception {
|
||||
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 the existing
|
||||
// selectAfterArmedAuth1DerivesStepUpSessionKeys test.
|
||||
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 StepUpSKReader the card now holds.
|
||||
byte[] stepUpSK = java.util.Arrays.copyOfRange(
|
||||
reader.deriveExpeditedKeyMaterial(credentialEphemPubKey), 64, 96);
|
||||
byte[] stepUpSKReader = hkdfStepUp(stepUpSK, "SKReader");
|
||||
|
||||
// SELECT the Step-Up AID -- arms StepUpApplet's StepUpSKReader and
|
||||
// initializes its stepup_reader_counter session-bound to 0x00000001.
|
||||
assertEquals(0x9000, sim.transmitCommand(
|
||||
new CommandAPDU(0x00, 0xA4, 0x04, 0x00, AliroAids.STEP_UP, 256)).getSW(),
|
||||
"SELECT step-up must succeed");
|
||||
|
||||
// Reader-side encrypt of a 4-byte fake Reader Status payload under
|
||||
// IV = 00 00 00 00 00 00 00 00 00 00 00 01 (8B zero prefix + counter=1).
|
||||
byte[] iv = new byte[12];
|
||||
iv[11] = 0x01;
|
||||
byte[] plaintext = new byte[] { 0x42, 0x42, 0x42, 0x42 };
|
||||
Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
gcm.init(Cipher.ENCRYPT_MODE,
|
||||
new SecretKeySpec(stepUpSKReader, "AES"),
|
||||
new GCMParameterSpec(128, iv));
|
||||
byte[] ctAndTag = gcm.doFinal(plaintext); // 4 + 16 = 20 bytes
|
||||
assertEquals(20, ctAndTag.length);
|
||||
|
||||
ResponseAPDU exchangeResp = sim.transmitCommand(
|
||||
new CommandAPDU(0x80, 0xC9, 0x00, 0x00, ctAndTag, 256));
|
||||
assertEquals(0x9000, exchangeResp.getSW(),
|
||||
"EXCHANGE with valid GCM tag must return SW=9000");
|
||||
assertEquals(0, exchangeResp.getData().length,
|
||||
"M1 EXCHANGE handler returns empty payload (decrypt-and-discard)");
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
|
||||
Reference in New Issue
Block a user