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:
michael
2026-06-11 10:45:22 -07:00
parent 8e999b770d
commit 96816bc232
5 changed files with 398 additions and 11 deletions

View File

@@ -15,6 +15,7 @@ package com.dangerousthings.aliro;
final class CryptoSingletons {
private static AliroCrypto aliroCrypto;
private static AliroGcm aliroGcm;
private CryptoSingletons() { }
@@ -26,4 +27,19 @@ final class CryptoSingletons {
}
return aliroCrypto;
}
/** Returns the process-wide {@link AliroGcm} instance. Same Java-Card
* {@code <clinit>}-cannot-{@code new} rationale as {@link #getAliroCrypto()}:
* lazy-allocate on first call so both {@link AliroApplet} and
* {@link StepUpApplet} share one userland-GCM machine. Each shared
* instance saves ~370 B of transient/EEPROM footprint that a per-applet
* duplicate would otherwise pay. The two applets are never selected
* simultaneously and the JCRE serializes APDU dispatch, so the shared
* scratch and {@code AESKey} slot don't race. */
static AliroGcm getAliroGcm() {
if (aliroGcm == null) {
aliroGcm = new AliroGcm();
}
return aliroGcm;
}
}