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

@@ -134,13 +134,12 @@ public class AliroApplet extends Applet {
/** SHA-1 for key_slot = first 8 bytes of SHA-1(uncompressed credential_PubK). */
private MessageDigest sha1;
/** Userland AES-256-GCM (built on AES-ECB-NOPAD + AES-256 key). The
* target card (NXP J3R180) does not expose {@code AEADCipher.ALG_AES_GCM}
* despite advertising JC 3.0.5, so we implement GCM in userland.
* {@link AliroGcm} owns its own persistent {@link AESKey} and
* {@link javacardx.crypto.Cipher} internally — this class no longer
* needs a separate {@code expeditedSKDeviceKey} field. */
private AliroGcm gcm;
// Userland AES-256-GCM (built on AES-ECB-NOPAD + AES-256 key) is pulled
// from CryptoSingletons.getAliroGcm(). The target card (NXP J3R180) does
// not expose AEADCipher.ALG_AES_GCM despite advertising JC 3.0.5, so we
// implement GCM in userland. Sharing with StepUpApplet via the singleton
// saves ~370 B of transient/EEPROM footprint that a per-applet duplicate
// would otherwise pay. See encryptResponseGcm().
// Low-level crypto primitives (ECDH, HKDF, Kdh, expedited key derivation)
// are pulled from CryptoSingletons.getAliroCrypto() so both AliroApplet
@@ -291,7 +290,10 @@ public class AliroApplet extends Applet {
ecdsaSigner = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
sha1 = MessageDigest.getInstance(MessageDigest.ALG_SHA, false);
try {
gcm = new AliroGcm();
// Force lazy alloc of the shared AliroGcm so any install-time
// failure (Cipher.getInstance / KeyBuilder.buildKey rejection)
// surfaces with the same greppable diagnostic SW as before.
CryptoSingletons.getAliroGcm();
} catch (ISOException e) { throw e; // preserve inner diagnostic SW
} catch (Throwable t) { ISOException.throwIt((short) 0x6FA8); }
try {
@@ -457,6 +459,7 @@ public class AliroApplet extends Applet {
case INS_DIAG_GCM: {
// Plaintext: 137 bytes anywhere in buf past the output region.
// Contents don't affect timing.
AliroGcm gcm = CryptoSingletons.getAliroGcm();
for (short i = 0; i < n; i++) {
gcm.encrypt(
DIAG_KEY_32, (short) 0,
@@ -964,7 +967,7 @@ public class AliroApplet extends Applet {
// device_counter big-endian in the last 4 bytes; first AUTH1 = 1
scratch[(short) (ivOff + 11)] = (byte) 0x01;
return gcm.encrypt(
return CryptoSingletons.getAliroGcm().encrypt(
derivedKeys, OFF_EXPEDITED_SK_DEVICE,
scratch, ivOff,
plaintext, ptOff, ptLen,