445 lines
21 KiB
Java
445 lines
21 KiB
Java
package com.dangerousthings.aliro;
|
|
|
|
import javacard.framework.APDU;
|
|
import javacard.framework.Applet;
|
|
import javacard.framework.ISO7816;
|
|
import javacard.framework.ISOException;
|
|
import javacard.framework.JCSystem;
|
|
import javacard.framework.Util;
|
|
|
|
/**
|
|
* Scaffold for the Aliro step-up phase (spec §8.4). Lives under
|
|
* {@link AliroAids#STEP_UP}.
|
|
*
|
|
* <p>This first pass only answers SELECT with a minimal FCI and rejects
|
|
* unknown commands cleanly. The actual step-up protocol — mdoc
|
|
* SessionData framing (DeviceRequest/DeviceResponse CBOR with
|
|
* Aliro-remapped integer keys per Tables 8-21/8-22), ENVELOPE/GET
|
|
* RESPONSE command chaining, and a fresh AES-256-GCM session keyed by
|
|
* StepUpSKDevice/StepUpSKReader derived per spec §8.4.3 — comes in
|
|
* follow-up iterations.
|
|
*
|
|
* <p>When that work lands, this applet will need access to the
|
|
* {@code StepUpSK} (a 32-byte session-bound value produced by
|
|
* {@link AliroApplet}'s derivation in §8.3.1.13). Since Java Card
|
|
* installs distinct instances per AID, that state will be exchanged via
|
|
* a shared package-private holder (not yet implemented).
|
|
*
|
|
* <p><b>Implementation notes for the Step-Up milestone (see
|
|
* {@code docs/plans/2026-06-07-step-up-implementation.md}):</b>
|
|
* <ol>
|
|
* <li><b>Opt 1 — Reuse AliroGcm.</b> Don't allocate a second
|
|
* {@link AliroGcm} for Step-Up; reuse {@link AliroApplet}'s instance
|
|
* via {@code aesKey.setKey(stepUpSKDevice)} between phases. Saves
|
|
* ~368 B transient. ExpeditedSK and StepUpSK are distinct so the
|
|
* key/IV uniqueness invariant holds across rekey.</li>
|
|
* <li><b>Opt 2 — Structural CBOR only.</b> The Access Document is opaque
|
|
* transport bytes to us; the reader does field-level parsing. We only
|
|
* need a structural CBOR codec (major type + length boundaries) for
|
|
* the SessionData wrapper and to extract {@code IssuerAuth} for
|
|
* verification. Do not implement field-level mdoc parsing — wastes
|
|
* bytecode and RAM. CBOR encoding MUST be deterministic per RFC 8949
|
|
* §4.2.1 anywhere we emit.</li>
|
|
* <li><b>Opt 3 — Stream-encrypt during ENVELOPE emit.</b> Don't buffer
|
|
* the entire encrypted DeviceResponse then chunk it; pipe plaintext
|
|
* through GCM as we emit ENVELOPE response chunks. Reduces transient
|
|
* footprint AND minimizes plaintext residence in RAM.</li>
|
|
* <li><b>Opt 4 — IssuerAuth verify cached at personalization.</b> See
|
|
* {@link CredentialStore#finalizeAccessDocument(short)} TODO — the
|
|
* COSE_Sign1 verify happens once at write time, the persistent
|
|
* {@code accessDocumentVerified} flag is checked here at read time.
|
|
* Saves ~100 ms per transaction. Trusts that the Credential Issuer
|
|
* trust anchor is fixed for the card's lifetime — true for DT's
|
|
* implantable target but document the limitation.</li>
|
|
* </ol>
|
|
*
|
|
* <p>The {@code AliroApplet.INS_EXCHANGE} stub was retired once StepUpApplet
|
|
* landed handling for 0xC9 directly — spec-conformant readers (X-CUBE-ALIRO
|
|
* included, once the upstream crypto interop is right) route EXCHANGE here
|
|
* after the step-up AID SELECT per §10.2.
|
|
*/
|
|
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;
|
|
/** 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
|
|
* active applet. */
|
|
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). */
|
|
private static final short STEP_UP_SK_LEN = 32;
|
|
|
|
/** 12-byte AES-256-GCM IV layout (§8.3.1.8/9): 8B prefix + 4B counter. */
|
|
private static final short GCM_IV_LEN = 12;
|
|
private static final short GCM_TAG_LEN = 16;
|
|
private static final short COUNTER_LEN = 4;
|
|
|
|
/** {@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;
|
|
|
|
/** Session-bound {@code StepUp_reader_counter} per §8.4.3 + mdoc [6]
|
|
* §9.1.1.5: 32-bit big-endian counter starting at {@code 0x00000001} the
|
|
* first time the reader→UD direction is used in this Step-Up session,
|
|
* incremented after each successful decrypt. CLEAR_ON_DESELECT so each
|
|
* Step-Up phase entry starts fresh -- the matching SELECT re-initialises
|
|
* 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;
|
|
|
|
/** 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
|
|
* per §8.3.1.8. Rebuilt per EXCHANGE; CLEAR_ON_DESELECT. */
|
|
private final byte[] ivScratch;
|
|
|
|
/** Persistent EXCHANGE plaintext sink for the decrypt-and-discard path.
|
|
* Sized to the largest reasonable Reader Status sub-event we'd see
|
|
* during M1 (X-CUBE-ALIRO observed values are well under 64 B); we'll
|
|
* resize when the real Reader Status payload size lands. CLEAR_ON_DESELECT
|
|
* so post-deselect there's no plaintext residue. */
|
|
private final byte[] scratchPlaintext;
|
|
private static final short SCRATCH_PLAINTEXT_LEN = 256;
|
|
|
|
/** Transient single-slot flag: 1 once {@link #select()} successfully
|
|
* derived the Step-Up session keys (i.e. the SELECT found
|
|
* {@link SessionContext} armed). EXCHANGE / ENVELOPE handlers refuse to
|
|
* run if this is 0. CLEAR_ON_DESELECT, alongside the keys themselves. */
|
|
private final byte[] sessionFlags;
|
|
private static final short FLAG_KEYS_READY = 0;
|
|
private static final short FLAGS_LEN = 1;
|
|
|
|
public static void install(byte[] bArray, short bOffset, byte bLength) {
|
|
StepUpApplet applet = new StepUpApplet();
|
|
if (bArray == null || bLength == 0) {
|
|
applet.register();
|
|
} else {
|
|
applet.register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
|
}
|
|
}
|
|
|
|
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);
|
|
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);
|
|
scratchPlaintext = JCSystem.makeTransientByteArray(SCRATCH_PLAINTEXT_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
sessionFlags = JCSystem.makeTransientByteArray(FLAGS_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);
|
|
// Spec §8.4.3 -> mdoc [6] §9.1.1.5: session-bound counters
|
|
// initialized to 0x00000001 on session entry, one per direction.
|
|
// CLEAR_ON_DESELECT already zeroes them on each fresh select;
|
|
// rewrite explicitly so a Step-Up SELECT mid-session (without a
|
|
// deselect in between) also starts both counters at 1.
|
|
Util.arrayFillNonAtomic(stepUpReaderCounter, (short) 0, COUNTER_LEN, (byte) 0);
|
|
stepUpReaderCounter[3] = (byte) 0x01;
|
|
Util.arrayFillNonAtomic(stepUpDeviceCounter, (short) 0, COUNTER_LEN, (byte) 0);
|
|
stepUpDeviceCounter[3] = (byte) 0x01;
|
|
sessionFlags[FLAG_KEYS_READY] = (byte) 1;
|
|
} else {
|
|
sessionFlags[FLAG_KEYS_READY] = (byte) 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void process(APDU apdu) {
|
|
if (selectingApplet()) {
|
|
sendStepUpFci(apdu);
|
|
return;
|
|
}
|
|
|
|
byte[] buf = apdu.getBuffer();
|
|
byte cla = buf[ISO7816.OFFSET_CLA];
|
|
byte ins = buf[ISO7816.OFFSET_INS];
|
|
|
|
// 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);
|
|
return;
|
|
}
|
|
if (cla != CLA_ISO && cla != CLA_PROPRIETARY) {
|
|
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
|
|
}
|
|
// GET RESPONSE handler plugs in here in follow-up milestones.
|
|
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
|
}
|
|
|
|
/**
|
|
* EXCHANGE (CLA=0x80, INS=0xC9) handler — Milestone 1 decrypt-and-discard.
|
|
*
|
|
* <p>Spec §8.3.3.5 / Table 8-14: the reader sends
|
|
* {@code encrypted_payload || authentication_tag} encrypted with
|
|
* {@code StepUpSKReader} per §8.3.1.8, IV layout
|
|
* {@code 0x0000000000000000 || stepup_reader_counter (4B BE)} and empty
|
|
* AAD. M1 only needs to verify the tag (proves matching session keys)
|
|
* then ACK with 9000 + empty payload so the X-CUBE-ALIRO firmware marks
|
|
* "DOOR OPERATION SUCCEEDED" and moves on. The real Reader Status
|
|
* response sub-event (encrypted with StepUpSKDevice) lands in M2.
|
|
*/
|
|
private void processExchange(APDU apdu) {
|
|
if (sessionFlags[FLAG_KEYS_READY] == 0) {
|
|
// SELECT hit StepUpApplet without an armed SessionContext (i.e.
|
|
// no successful AUTH1 ran on AliroApplet first). Spec §8.4 says
|
|
// the Step-Up phase is only entered 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.
|
|
if (lc < GCM_TAG_LEN) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
|
}
|
|
// M1 sink is fixed-size; reject payloads that wouldn't fit. The real
|
|
// EXCHANGE payload during M1 ack flow is tiny (X-CUBE-ALIRO sends a
|
|
// few bytes of CBOR), so this bound is comfortable.
|
|
short ptLen = (short) (lc - GCM_TAG_LEN);
|
|
if (ptLen > SCRATCH_PLAINTEXT_LEN) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
|
}
|
|
|
|
// Build the IV: 8 zero bytes (reader→device prefix per §8.3.1.8) +
|
|
// stepup_reader_counter, big-endian, in the trailing 4 bytes.
|
|
Util.arrayFillNonAtomic(ivScratch, (short) 0, (short) 8, (byte) 0);
|
|
Util.arrayCopyNonAtomic(stepUpReaderCounter, (short) 0,
|
|
ivScratch, (short) 8, COUNTER_LEN);
|
|
|
|
// Decrypt-and-discard. AliroGcm.decrypt throws CryptoException on
|
|
// tag mismatch; remap to a security SW so an attacker can't tell
|
|
// tag-mismatch from any other failure mode.
|
|
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);
|
|
}
|
|
|
|
// Wipe the discarded plaintext immediately -- M1 has no use for it,
|
|
// and CLEAR_ON_DESELECT alone would leave it sitting around until the
|
|
// reader walks away.
|
|
Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0);
|
|
|
|
// Spec §8.3.1.8: reader_counter <- reader_counter + 1 after use.
|
|
incrementCounter(stepUpReaderCounter, (short) 0);
|
|
|
|
// Ack with SW=9000 and empty payload. If field testing on real
|
|
// X-CUBE-ALIRO firmware shows the reader rejects an empty payload,
|
|
// M1E iteration escalates this to "9000 + encrypted-empty-CBOR-map"
|
|
// per the implementation plan.
|
|
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.
|
|
* 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
|
|
* clearly meant -- 4-byte BE counter), so wrap is unreachable in
|
|
* practice during normal protocol flow. */
|
|
private static void incrementCounter(byte[] buf, short off) {
|
|
for (short i = (short) (off + 3); i >= off; i--) {
|
|
buf[i]++;
|
|
if (buf[i] != 0) return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minimal FCI for step-up SELECT. The spec (§10.2.1.2) allows the UD to
|
|
* advertise supported APDU command/response sizes here; those get added
|
|
* when we implement ENVELOPE/GET RESPONSE.
|
|
*
|
|
* <pre>
|
|
* 6F L
|
|
* 84 09 [step-up AID]
|
|
* </pre>
|
|
*/
|
|
private void sendStepUpFci(APDU apdu) {
|
|
byte[] buf = apdu.getBuffer();
|
|
short off = 0;
|
|
buf[off++] = 0x6F;
|
|
short outerLenPos = off++;
|
|
|
|
buf[off++] = (byte) 0x84;
|
|
buf[off++] = (byte) AliroAids.STEP_UP.length;
|
|
off = Util.arrayCopyNonAtomic(AliroAids.STEP_UP, (short) 0,
|
|
buf, off, (short) AliroAids.STEP_UP.length);
|
|
|
|
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);
|
|
}
|
|
}
|