feat(applet): StepUpApplet INS_ENVELOPE (CLA=0x00) decrypt + encrypted ack
X-CUBE-ALIRO firmware sends ENVELOPE (CLA=0x00 INS=0xC3) carrying the encrypted mdoc DeviceRequest after the step-up AID SELECT. Spec §8.3.1.9 defines the inbound IV (reader-side: 0x0000000000000000 || stepup_reader_counter). For Milestone 1 we decrypt-and-discard the DeviceRequest, then return a spec-shape encrypted empty CBOR map (1 plaintext byte 0xA0, GCM-encrypted with StepUpSKDevice using device-side IV per §8.3.1.6: 0x0000000000000001 || stepup_device_counter). Total response: 17 bytes (1 ct + 16 tag). Adds stepUpDeviceCounter[4] CLEAR_ON_DESELECT alongside the existing stepUpReaderCounter; both init to [0,0,0,1] when StepUpApplet.select() derives session keys (i.e. each Step-Up phase entry). Restructures the StepUpApplet dispatch so CLA=0x00 ENVELOPE coexists with CLA=0x80 EXCHANGE -- ENVELOPE uses the ISO-standard CLA per spec/ISO 7816 convention, EXCHANGE remains Aliro-proprietary. M2 will replace the empty CBOR map with a real mdoc DeviceResponse carrying the cached-verified Access Document bytes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -61,11 +61,20 @@ import javacard.framework.Util;
|
|||||||
*/
|
*/
|
||||||
public class StepUpApplet extends Applet {
|
public class StepUpApplet extends Applet {
|
||||||
|
|
||||||
|
/** ISO 7816 CLA byte (0x00) for ISO-standardized commands. ENVELOPE
|
||||||
|
* (INS=0xC3) per ISO 7816 / Table 8-14 in the Aliro spec uses this CLA,
|
||||||
|
* not the Aliro-proprietary 0x80, because ENVELOPE is the standard
|
||||||
|
* ISO command for carrying chained data. */
|
||||||
|
private static final byte CLA_ISO = (byte) 0x00;
|
||||||
private static final byte CLA_PROPRIETARY = (byte) 0x80;
|
private static final byte CLA_PROPRIETARY = (byte) 0x80;
|
||||||
/** EXCHANGE command per spec §8.3.3.5 / Table 8-14. The reader sends a
|
/** EXCHANGE command per spec §8.3.3.5 / Table 8-14. The reader sends a
|
||||||
* Reader Status sub-event under this INS once the Step-Up AID is the
|
* Reader Status sub-event under this INS once the Step-Up AID is the
|
||||||
* active applet. */
|
* active applet. */
|
||||||
private static final byte INS_EXCHANGE = (byte) 0xC9;
|
private static final byte INS_EXCHANGE = (byte) 0xC9;
|
||||||
|
/** ENVELOPE command per ISO 7816 + spec §8.4 (Step-Up entry point). The
|
||||||
|
* X-CUBE-ALIRO firmware sends the encrypted mdoc DeviceRequest in the
|
||||||
|
* ENVELOPE body once the Step-Up AID is the active applet. */
|
||||||
|
private static final byte INS_ENVELOPE = (byte) 0xC3;
|
||||||
|
|
||||||
/** Length of each derived Step-Up session key (spec §8.4.3). */
|
/** Length of each derived Step-Up session key (spec §8.4.3). */
|
||||||
private static final short STEP_UP_SK_LEN = 32;
|
private static final short STEP_UP_SK_LEN = 32;
|
||||||
@@ -95,9 +104,18 @@ public class StepUpApplet extends Applet {
|
|||||||
* first time the reader→UD direction is used in this Step-Up session,
|
* first time the reader→UD direction is used in this Step-Up session,
|
||||||
* incremented after each successful decrypt. CLEAR_ON_DESELECT so each
|
* incremented after each successful decrypt. CLEAR_ON_DESELECT so each
|
||||||
* Step-Up phase entry starts fresh -- the matching SELECT re-initialises
|
* Step-Up phase entry starts fresh -- the matching SELECT re-initialises
|
||||||
* the counter alongside the SK derivation. */
|
* the counter alongside the SK derivation. Shared across ENVELOPE and
|
||||||
|
* EXCHANGE: both commands are reader→device so both consume from the
|
||||||
|
* same counter. */
|
||||||
private final byte[] stepUpReaderCounter;
|
private final byte[] stepUpReaderCounter;
|
||||||
|
|
||||||
|
/** Session-bound {@code StepUp_device_counter} per §8.4.3 + mdoc [6]
|
||||||
|
* §9.1.1.5: 32-bit big-endian counter for the device→reader direction,
|
||||||
|
* starting at {@code 0x00000001} on Step-Up session entry, incremented
|
||||||
|
* after each successful encrypt. In M1 only ENVELOPE returns ciphertext
|
||||||
|
* (EXCHANGE returns empty), so this advances once per ENVELOPE. */
|
||||||
|
private final byte[] stepUpDeviceCounter;
|
||||||
|
|
||||||
/** 12-byte scratch for the GCM IV: 8 zero bytes + 4-byte reader counter
|
/** 12-byte scratch for the GCM IV: 8 zero bytes + 4-byte reader counter
|
||||||
* per §8.3.1.8. Rebuilt per EXCHANGE; CLEAR_ON_DESELECT. */
|
* per §8.3.1.8. Rebuilt per EXCHANGE; CLEAR_ON_DESELECT. */
|
||||||
private final byte[] ivScratch;
|
private final byte[] ivScratch;
|
||||||
@@ -136,6 +154,7 @@ public class StepUpApplet extends Applet {
|
|||||||
stepUpSKReader = 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);
|
stepUpSKScratch = JCSystem.makeTransientByteArray(STEP_UP_SK_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
stepUpReaderCounter = JCSystem.makeTransientByteArray(COUNTER_LEN, JCSystem.CLEAR_ON_DESELECT);
|
stepUpReaderCounter = JCSystem.makeTransientByteArray(COUNTER_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
|
stepUpDeviceCounter = JCSystem.makeTransientByteArray(COUNTER_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
ivScratch = JCSystem.makeTransientByteArray(GCM_IV_LEN, JCSystem.CLEAR_ON_DESELECT);
|
ivScratch = JCSystem.makeTransientByteArray(GCM_IV_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
scratchPlaintext = JCSystem.makeTransientByteArray(SCRATCH_PLAINTEXT_LEN, JCSystem.CLEAR_ON_DESELECT);
|
scratchPlaintext = JCSystem.makeTransientByteArray(SCRATCH_PLAINTEXT_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
sessionFlags = JCSystem.makeTransientByteArray(FLAGS_LEN, JCSystem.CLEAR_ON_DESELECT);
|
sessionFlags = JCSystem.makeTransientByteArray(FLAGS_LEN, JCSystem.CLEAR_ON_DESELECT);
|
||||||
@@ -162,13 +181,15 @@ public class StepUpApplet extends Applet {
|
|||||||
// Wipe the staged StepUpSK — the derived keys are sufficient
|
// Wipe the staged StepUpSK — the derived keys are sufficient
|
||||||
// from here on and we don't want the IKM lingering in transient.
|
// from here on and we don't want the IKM lingering in transient.
|
||||||
Util.arrayFillNonAtomic(stepUpSKScratch, (short) 0, STEP_UP_SK_LEN, (byte) 0);
|
Util.arrayFillNonAtomic(stepUpSKScratch, (short) 0, STEP_UP_SK_LEN, (byte) 0);
|
||||||
// Spec §8.4.3 -> mdoc [6] §9.1.1.5: session-bound reader counter
|
// Spec §8.4.3 -> mdoc [6] §9.1.1.5: session-bound counters
|
||||||
// initialized to 0x00000001 on session entry. CLEAR_ON_DESELECT
|
// initialized to 0x00000001 on session entry, one per direction.
|
||||||
// already zeroes it on each fresh select; rewrite explicitly so a
|
// CLEAR_ON_DESELECT already zeroes them on each fresh select;
|
||||||
// Step-Up SELECT mid-session (without a deselect in between) also
|
// rewrite explicitly so a Step-Up SELECT mid-session (without a
|
||||||
// starts the counter at 1.
|
// deselect in between) also starts both counters at 1.
|
||||||
Util.arrayFillNonAtomic(stepUpReaderCounter, (short) 0, COUNTER_LEN, (byte) 0);
|
Util.arrayFillNonAtomic(stepUpReaderCounter, (short) 0, COUNTER_LEN, (byte) 0);
|
||||||
stepUpReaderCounter[3] = (byte) 0x01;
|
stepUpReaderCounter[3] = (byte) 0x01;
|
||||||
|
Util.arrayFillNonAtomic(stepUpDeviceCounter, (short) 0, COUNTER_LEN, (byte) 0);
|
||||||
|
stepUpDeviceCounter[3] = (byte) 0x01;
|
||||||
sessionFlags[FLAG_KEYS_READY] = (byte) 1;
|
sessionFlags[FLAG_KEYS_READY] = (byte) 1;
|
||||||
} else {
|
} else {
|
||||||
sessionFlags[FLAG_KEYS_READY] = (byte) 0;
|
sessionFlags[FLAG_KEYS_READY] = (byte) 0;
|
||||||
@@ -184,17 +205,24 @@ public class StepUpApplet extends Applet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte[] buf = apdu.getBuffer();
|
byte[] buf = apdu.getBuffer();
|
||||||
if (buf[ISO7816.OFFSET_CLA] != CLA_PROPRIETARY) {
|
byte cla = buf[ISO7816.OFFSET_CLA];
|
||||||
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte ins = buf[ISO7816.OFFSET_INS];
|
byte ins = buf[ISO7816.OFFSET_INS];
|
||||||
if (ins == INS_EXCHANGE) {
|
|
||||||
|
// INS-first dispatch: ENVELOPE is ISO-class (0x00) per ISO 7816 + spec
|
||||||
|
// Table 8-14, EXCHANGE is Aliro-proprietary (0x80). Don't blanket
|
||||||
|
// reject CLA=0x00 -- it's a legitimate ENVELOPE entry point.
|
||||||
|
if (cla == CLA_ISO && ins == INS_ENVELOPE) {
|
||||||
|
processEnvelope(apdu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cla == CLA_PROPRIETARY && ins == INS_EXCHANGE) {
|
||||||
processExchange(apdu);
|
processExchange(apdu);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cla != CLA_ISO && cla != CLA_PROPRIETARY) {
|
||||||
// ENVELOPE + GET RESPONSE handlers plug in here in follow-up milestones.
|
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
// GET RESPONSE handler plugs in here in follow-up milestones.
|
||||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +298,101 @@ public class StepUpApplet extends Applet {
|
|||||||
apdu.setOutgoingAndSend((short) 0, (short) 0);
|
apdu.setOutgoingAndSend((short) 0, (short) 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENVELOPE (CLA=0x00, INS=0xC3) handler — Milestone 1 decrypt-and-discard
|
||||||
|
* the mdoc DeviceRequest, then return a spec-shape encrypted empty CBOR
|
||||||
|
* map (canonical RFC 8949: single byte 0xA0).
|
||||||
|
*
|
||||||
|
* <p>Spec §8.3.1.9: inbound payload (reader→device) is decrypted with
|
||||||
|
* {@code StepUpSKReader}, IV {@code 0x0000000000000000 || stepup_reader_counter}
|
||||||
|
* (8-byte zero prefix + 4-byte BE counter), empty AAD.
|
||||||
|
*
|
||||||
|
* <p>Spec §8.3.1.6: outbound payload (device→reader) is encrypted with
|
||||||
|
* {@code StepUpSKDevice}, IV {@code 0x0000000000000001 || stepup_device_counter}
|
||||||
|
* (8-byte prefix ending in 0x01 + 4-byte BE counter), empty AAD.
|
||||||
|
*
|
||||||
|
* <p>The DeviceRequest body is discarded in M1: we don't build a real
|
||||||
|
* mdoc DeviceResponse yet -- that's M2's job. The 17-byte ciphertext
|
||||||
|
* (1 ct + 16 tag) is enough for X-CUBE-ALIRO to see a spec-shape
|
||||||
|
* encrypted response and move on. Counter sequencing: both
|
||||||
|
* stepup_reader_counter (shared with EXCHANGE) and stepup_device_counter
|
||||||
|
* advance independently after each successful use.
|
||||||
|
*/
|
||||||
|
private void processEnvelope(APDU apdu) {
|
||||||
|
if (sessionFlags[FLAG_KEYS_READY] == 0) {
|
||||||
|
// SELECT-Step-Up landed without an armed SessionContext (i.e. no
|
||||||
|
// successful AUTH1 on AliroApplet first). Spec §8.4 keeps Step-Up
|
||||||
|
// strictly post-AUTH1; reject cleanly.
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
short lc = apdu.setIncomingAndReceive();
|
||||||
|
byte[] buf = apdu.getBuffer();
|
||||||
|
short dataOff = apdu.getOffsetCdata();
|
||||||
|
|
||||||
|
// Need at least the 16-byte tag (zero-byte plaintext is degenerate
|
||||||
|
// but spec-legal); reject anything that can't possibly carry a tag.
|
||||||
|
if (lc < GCM_TAG_LEN) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
||||||
|
}
|
||||||
|
short ptLen = (short) (lc - GCM_TAG_LEN);
|
||||||
|
if (ptLen > SCRATCH_PLAINTEXT_LEN) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the reader-side IV: 8 zero bytes (§8.3.1.9) + reader counter.
|
||||||
|
Util.arrayFillNonAtomic(ivScratch, (short) 0, (short) 8, (byte) 0);
|
||||||
|
Util.arrayCopyNonAtomic(stepUpReaderCounter, (short) 0,
|
||||||
|
ivScratch, (short) 8, COUNTER_LEN);
|
||||||
|
|
||||||
|
try {
|
||||||
|
CryptoSingletons.getAliroGcm().decrypt(
|
||||||
|
stepUpSKReader, (short) 0,
|
||||||
|
ivScratch, (short) 0,
|
||||||
|
buf, dataOff, lc,
|
||||||
|
scratchPlaintext, (short) 0);
|
||||||
|
} catch (ISOException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
// Spec §8.3.1.9: reader_counter <- reader_counter + 1 after use.
|
||||||
|
incrementCounter(stepUpReaderCounter, (short) 0);
|
||||||
|
|
||||||
|
// Wipe the decrypted DeviceRequest -- M1 has no use for it. M2 will
|
||||||
|
// replace this with real mdoc parsing and a real DeviceResponse.
|
||||||
|
Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0);
|
||||||
|
|
||||||
|
// Build the canonical-CBOR empty map plaintext (single byte 0xA0,
|
||||||
|
// RFC 8949 major type 5 (map) with length 0). One byte total.
|
||||||
|
scratchPlaintext[0] = (byte) 0xA0;
|
||||||
|
|
||||||
|
// Build the device-side IV: 0x00*7 || 0x01 || device_counter
|
||||||
|
// (§8.3.1.6 -- 8-byte prefix ending in 0x01, then 4B BE counter).
|
||||||
|
Util.arrayFillNonAtomic(ivScratch, (short) 0, (short) 7, (byte) 0);
|
||||||
|
ivScratch[7] = (byte) 0x01;
|
||||||
|
Util.arrayCopyNonAtomic(stepUpDeviceCounter, (short) 0,
|
||||||
|
ivScratch, (short) 8, COUNTER_LEN);
|
||||||
|
|
||||||
|
// Encrypt into the APDU buffer at offset 0. Safe to overwrite the
|
||||||
|
// inbound command bytes here because we've finished reading them.
|
||||||
|
// Output length = 1 (ct) + 16 (tag) = 17 bytes; well under the 252-byte
|
||||||
|
// APDU buffer ceiling.
|
||||||
|
short ctLen = CryptoSingletons.getAliroGcm().encrypt(
|
||||||
|
stepUpSKDevice, (short) 0,
|
||||||
|
ivScratch, (short) 0,
|
||||||
|
scratchPlaintext, (short) 0, (short) 1,
|
||||||
|
buf, (short) 0);
|
||||||
|
// Spec §8.3.1.6: device_counter <- device_counter + 1 after use.
|
||||||
|
incrementCounter(stepUpDeviceCounter, (short) 0);
|
||||||
|
|
||||||
|
// Wipe the single plaintext byte (CLEAR_ON_DESELECT alone would
|
||||||
|
// leave 0xA0 sitting in transient until reader walks away).
|
||||||
|
scratchPlaintext[0] = 0;
|
||||||
|
|
||||||
|
apdu.setOutgoingAndSend((short) 0, ctLen);
|
||||||
|
}
|
||||||
|
|
||||||
/** 32-bit big-endian counter increment with carry across all 4 bytes.
|
/** 32-bit big-endian counter increment with carry across all 4 bytes.
|
||||||
* Wraps mod 2^32; spec §8.3.3.5.4 says the counter SHALL never reach
|
* Wraps mod 2^32; spec §8.3.3.5.4 says the counter SHALL never reach
|
||||||
* 0xFFFF before increment (note: spec uses 0xFFFF where 0xFFFFFFFF is
|
* 0xFFFF before increment (note: spec uses 0xFFFF where 0xFFFFFFFF is
|
||||||
|
|||||||
@@ -219,6 +219,103 @@ class StepUpAppletTest {
|
|||||||
"M1 EXCHANGE handler returns empty payload (decrypt-and-discard)");
|
"M1 EXCHANGE handler returns empty payload (decrypt-and-discard)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After SELECT-Step-Up the X-CUBE-ALIRO firmware also sends an ENVELOPE
|
||||||
|
* command (CLA=0x00 INS=0xC3) carrying the encrypted mdoc DeviceRequest
|
||||||
|
* (spec §8.4 + ISO 7816 ENVELOPE). Inbound encryption per §8.3.1.9
|
||||||
|
* (reader-side IV {@code 0x0000000000000000 || stepup_reader_counter},
|
||||||
|
* empty AAD); response encryption per §8.3.1.6 (device-side IV
|
||||||
|
* {@code 0x0000000000000001 || stepup_device_counter}, empty AAD).
|
||||||
|
*
|
||||||
|
* <p>For Milestone 1 we decrypt-and-discard the DeviceRequest, then
|
||||||
|
* return an encrypted single-byte CBOR empty map ({@code 0xA0}) so the
|
||||||
|
* X-CUBE-ALIRO firmware sees a spec-shape encrypted response. Total
|
||||||
|
* response bytes: 1 ct + 16 tag = 17.
|
||||||
|
*
|
||||||
|
* <p>This test also pins counter sequencing: ENVELOPE consumes
|
||||||
|
* stepup_reader_counter=1 (then increments) and emits with
|
||||||
|
* stepup_device_counter=1 (then increments). The reader-counter is
|
||||||
|
* shared with EXCHANGE, but EXCHANGE isn't sent in this test so we
|
||||||
|
* only see counter=1 on each side.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void envelopeAfterStepUpSelectDecryptsAndAcksWithEncryptedEmptyCborMap() 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.
|
||||||
|
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 both StepUpSK leg keys the card now holds.
|
||||||
|
byte[] stepUpSK = java.util.Arrays.copyOfRange(
|
||||||
|
reader.deriveExpeditedKeyMaterial(credentialEphemPubKey), 64, 96);
|
||||||
|
byte[] stepUpSKReader = hkdfStepUp(stepUpSK, "SKReader");
|
||||||
|
byte[] stepUpSKDevice = hkdfStepUp(stepUpSK, "SKDevice");
|
||||||
|
|
||||||
|
// SELECT the Step-Up AID -- arms StepUpApplet's session keys and
|
||||||
|
// initialises both counters 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 plaintext under the reader IV
|
||||||
|
// (00 00 00 00 00 00 00 00 || 00 00 00 01). The applet decrypts and
|
||||||
|
// discards; the actual plaintext is irrelevant for M1.
|
||||||
|
byte[] readerIv = new byte[12];
|
||||||
|
readerIv[11] = 0x01;
|
||||||
|
byte[] plaintext = new byte[] { 0x42, 0x42, 0x42, 0x42 };
|
||||||
|
Cipher gcmEnc = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
gcmEnc.init(Cipher.ENCRYPT_MODE,
|
||||||
|
new SecretKeySpec(stepUpSKReader, "AES"),
|
||||||
|
new GCMParameterSpec(128, readerIv));
|
||||||
|
byte[] envelopeBody = gcmEnc.doFinal(plaintext); // 4 + 16 = 20 bytes
|
||||||
|
assertEquals(20, envelopeBody.length);
|
||||||
|
|
||||||
|
// Send ENVELOPE: CLA=0x00 INS=0xC3 (ISO-class command per Table 8-14).
|
||||||
|
ResponseAPDU envelopeResp = sim.transmitCommand(
|
||||||
|
new CommandAPDU(0x00, 0xC3, 0x00, 0x00, envelopeBody, 256));
|
||||||
|
assertEquals(0x9000, envelopeResp.getSW(),
|
||||||
|
"ENVELOPE with valid GCM tag must return SW=9000");
|
||||||
|
|
||||||
|
byte[] respBody = envelopeResp.getData();
|
||||||
|
assertEquals(17, respBody.length,
|
||||||
|
"M1 ENVELOPE response = 1 ciphertext byte + 16 GCM tag");
|
||||||
|
|
||||||
|
// Device-side decrypt under IV = 00 00 00 00 00 00 00 01 || 00 00 00 01
|
||||||
|
// (device-prefix per §8.3.1.6 + device_counter=1).
|
||||||
|
byte[] deviceIv = new byte[12];
|
||||||
|
deviceIv[7] = 0x01;
|
||||||
|
deviceIv[11] = 0x01;
|
||||||
|
Cipher gcmDec = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
gcmDec.init(Cipher.DECRYPT_MODE,
|
||||||
|
new SecretKeySpec(stepUpSKDevice, "AES"),
|
||||||
|
new GCMParameterSpec(128, deviceIv));
|
||||||
|
byte[] decoded = gcmDec.doFinal(respBody);
|
||||||
|
assertArrayEquals(new byte[] { (byte) 0xA0 }, decoded,
|
||||||
|
"M1 ENVELOPE response plaintext = canonical CBOR empty map (0xA0)");
|
||||||
|
}
|
||||||
|
|
||||||
/** HKDF-SHA-256 with empty salt and single-block Expand (L=32). Mirrors
|
/** HKDF-SHA-256 with empty salt and single-block Expand (L=32). Mirrors
|
||||||
* AliroCrypto.deriveStepUpSessionKeys's spec-pinned HKDF computation. */
|
* AliroCrypto.deriveStepUpSessionKeys's spec-pinned HKDF computation. */
|
||||||
private static byte[] hkdfStepUp(byte[] ikm, String info) throws Exception {
|
private static byte[] hkdfStepUp(byte[] ikm, String info) throws Exception {
|
||||||
|
|||||||
Reference in New Issue
Block a user