Initial snapshot: Aliro applet, harness, Nucleo NFC10A1 reader port

Three components, all bench-validated to varying depths:

- applet/: CSA Aliro v1.0 Java Card applet for J3R180. AUTH0 + AUTH1
  expedited-standard flow end-to-end green via PC/SC bench reader
  (aliro-bench-test). Userland AES-256-GCM and HMAC-SHA-256 layered
  on top of J3R180's primitives because the card lacks both natively.
  P-256 curve params seeded explicitly per J3R180's quirk.

- harness/: Python orchestrator (aliro-trustgen, aliro-personalize,
  aliro-bench-test) for trust-bundle generation, card personalization
  via PersonalizationApplet, and PC/SC AUTH0+AUTH1 transactions. 126
  pytest cases passing.

- reader/STM32CubeExpansion_ALIRO_V1_0_0/: ST X-CUBE-ALIRO V1.0.0
  with our NFC10A1 port (NUCLEO-U545RE-Q + X-NUCLEO-NFC10A1, ST25R200
  shared with NFC09A1). nfc10-only/ project, NFC10A1 BSP shim,
  ALIRO_TRUST_OVERRIDE include into vendor's provisioning.c, and an
  ALIRO_APDU_TRACE wrapper around demoTransceiveBlocking. Boots,
  detects the J3R180, completes SELECT + AUTH0; AUTH1 currently fails
  with RFAL ERR_PROTO (0xB) — under investigation, see
  docs/plans/2026-04-20-nucleo-nfc10a1-port.md and bench-notes/.

Excluded: x-cube-aliro.zip vendor archive, harness/.venv, build dirs,
generated aliro_trust.h (contains private reader scalar), all PEMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 10:17:46 -07:00
commit 782074f6ae
8786 changed files with 2902373 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
package com.dangerousthings.aliro;
/**
* CSA Aliro v1.0 AIDs, spec &sect;10.2.1.
*/
public final class AliroAids {
/** Expedited Phase AID: A0 00 00 09 09 AC CE 55 01 */
public static final byte[] EXPEDITED = {
(byte)0xA0, (byte)0x00, (byte)0x00, (byte)0x09, (byte)0x09,
(byte)0xAC, (byte)0xCE, (byte)0x55, (byte)0x01
};
/** Step-up Phase AID: A0 00 00 09 09 AC CE 55 02 */
public static final byte[] STEP_UP = {
(byte)0xA0, (byte)0x00, (byte)0x00, (byte)0x09, (byte)0x09,
(byte)0xAC, (byte)0xCE, (byte)0x55, (byte)0x02
};
/**
* DT-internal provisioning AID. Lives under the CSA RID so this applet
* can ship in the same CAP as the spec-defined Aliro applets (Java Card
* requires all applets in a package share the package's RID). The PIX
* tail (ACCE 55 99 01) is non-spec — Aliro v1 uses 0x01/0x02 as the
* PIX-low byte; 0x99 stays clear of any future allocation.
*
* <p>Used by the PersonalizationApplet to receive Access Credential +
* reader keys + Access Document before the main Aliro flow runs. The
* applet locks itself after COMMIT.
*/
public static final byte[] PROVISIONING = {
(byte)0xA0, (byte)0x00, (byte)0x00, (byte)0x09, (byte)0x09,
(byte)0xAC, (byte)0xCE, (byte)0x55, (byte)0x99, (byte)0x01
};
private AliroAids() {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,265 @@
package com.dangerousthings.aliro;
import javacard.security.ECPrivateKey;
import javacard.security.KeyAgreement;
/**
* Low-level Aliro cryptographic primitives, factored out so they can be
* unit-tested independently of the APDU dispatch in {@link AliroApplet}.
*
* <p>Intended to be instantiated once per applet (or test) and reused —
* the bundled {@link KeyAgreement} and {@link AliroHmac} objects are
* expensive to allocate.
*
* <p>HMAC-SHA-256 is supplied by {@link AliroHmac}, our userland
* implementation. The target card (NXP J3R180) does not expose
* {@code KeyBuilder.TYPE_HMAC} at any tested key size nor
* {@code Signature.ALG_HMAC_SHA_256}, so we build HMAC on top of the card's
* raw SHA-256 primitive per RFC 2104.
*/
final class AliroCrypto {
private static final short HASH_LEN = 32;
/** Maximum expanded-input buffer size for HKDF-Expand: one T(i-1) block
* (32B) + info (up to ~200B, defensively) + one counter byte. Rounded up
* to 256B. The largest info we currently pass is the 32-byte salt_volatile
* info for expedited-standard derivation; step-up uses 8-byte ASCII info. */
private static final short EXPAND_SCRATCH_LEN = 256;
private KeyAgreement ecdhPlain;
private AliroHmac aliroHmac;
/** Reusable scratch for one HMAC output (T(i)) and one counter byte. */
private byte[] hkdfPrevT;
/** Scratch for assembling the HMAC message for HKDF-Expand: T(i-1) || info || counter. */
private byte[] expandScratch;
/** Scratch for {@link #deriveKdh}: 32B for Z_AB then 32B for PRK. */
private byte[] kdfWorkbuf;
private static final short KDF_WORKBUF_LEN = 64;
/** 32 zero bytes — RFC 5869 §2.2 fallback when {@link #hkdfExtract} is
* called with an empty salt. Allocated in the constructor; Java Card
* bans {@code new} in static initializers, so we can't make this static-final. */
private byte[] emptySaltZeros;
AliroCrypto() {
try {
ecdhPlain = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
} catch (Throwable t) {
javacard.framework.ISOException.throwIt((short) 0x6FC1);
}
// Userland HMAC-SHA-256 on top of MessageDigest.ALG_SHA_256. Diagnostic
// status word 0x6FC7 gives us one regression cycle of visibility on the
// real card before we strip it.
try {
aliroHmac = new AliroHmac();
} catch (Throwable t) {
javacard.framework.ISOException.throwIt((short) 0x6FC7);
}
try {
hkdfPrevT = javacard.framework.JCSystem.makeTransientByteArray(
HASH_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
expandScratch = javacard.framework.JCSystem.makeTransientByteArray(
EXPAND_SCRATCH_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
kdfWorkbuf = javacard.framework.JCSystem.makeTransientByteArray(
KDF_WORKBUF_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
} catch (Throwable t) {
javacard.framework.ISOException.throwIt((short) 0x6FC4);
}
try {
emptySaltZeros = new byte[32]; // persistent, defaults to all zeros
} catch (Throwable t) {
javacard.framework.ISOException.throwIt((short) 0x6FC5);
}
}
/**
* Computes the x-coordinate of the ECDH shared point, given a local
* P-256 private key and the peer's uncompressed public point
* (0x04 || x || y, 65 bytes). Returns 32 (the x-coordinate length).
*/
short computeEcdhSharedX(
ECPrivateKey priv,
byte[] peerPubUncomp, short peerPubOff,
byte[] out, short outOff) {
ecdhPlain.init(priv);
return ecdhPlain.generateSecret(peerPubUncomp, peerPubOff, (short) 65, out, outOff);
}
/**
* HKDF-Extract per RFC 5869 §2.2 with HMAC-SHA-256:
* {@code PRK = HMAC(salt, IKM)}.
* Returns 32 (HashLen).
*/
short hkdfExtract(
byte[] salt, short saltOff, short saltLen,
byte[] ikm, short ikmOff, short ikmLen,
byte[] prk, short prkOff) {
// RFC 5869 §2.2: empty salt is treated as HashLen zero bytes.
if (saltLen == 0) {
return aliroHmac.compute(
emptySaltZeros, (short) 0, HASH_LEN,
ikm, ikmOff, ikmLen,
prk, prkOff);
}
return aliroHmac.compute(
salt, saltOff, saltLen,
ikm, ikmOff, ikmLen,
prk, prkOff);
}
/**
* HKDF-Expand per RFC 5869 §2.3 with HMAC-SHA-256. Generates {@code L}
* bytes of output keying material, where {@code L} must be ≤ 255 *
* HashLen (8160 bytes — far above any Aliro need). The info region must
* be at most {@code EXPAND_SCRATCH_LEN - HASH_LEN - 1} bytes long
* (= 223 bytes with the current buffer sizing).
*/
short hkdfExpand(
byte[] prk, short prkOff, short prkLen,
byte[] info, short infoOff, short infoLen,
short L,
byte[] out, short outOff) {
// T(0) = empty; T(i) = HMAC(PRK, T(i-1) || info || i). Concatenate
// T(1)..T(N) and truncate to L bytes.
short produced = 0;
short prevTLen = 0;
byte counter = (byte) 1;
while (produced < L) {
// Assemble the HMAC message as T(i-1) || info || counter in expandScratch.
short msgLen = 0;
if (prevTLen != 0) {
javacard.framework.Util.arrayCopyNonAtomic(
hkdfPrevT, (short) 0, expandScratch, msgLen, prevTLen);
msgLen += prevTLen;
}
if (infoLen != 0) {
javacard.framework.Util.arrayCopyNonAtomic(
info, infoOff, expandScratch, msgLen, infoLen);
msgLen += infoLen;
}
expandScratch[msgLen] = counter;
msgLen++;
aliroHmac.compute(
prk, prkOff, prkLen,
expandScratch, (short) 0, msgLen,
hkdfPrevT, (short) 0);
prevTLen = HASH_LEN;
short remaining = (short) (L - produced);
short toCopy = remaining < HASH_LEN ? remaining : HASH_LEN;
javacard.framework.Util.arrayCopyNonAtomic(
hkdfPrevT, (short) 0, out, (short) (outOff + produced), toCopy);
produced += toCopy;
counter++;
}
// Wipe the assembled message scratch — it held T(i-1) values (derived
// from the HKDF PRK) and the info bytes. CLEAR_ON_DESELECT cleans up
// at deselect, but multi-APDU sessions would otherwise see stale T(i).
javacard.framework.Util.arrayFillNonAtomic(
expandScratch, (short) 0, EXPAND_SCRATCH_LEN, (byte) 0);
return produced;
}
/**
* Derives Kdh per Aliro §8.3.1.4 (with the §8.3.1.5 HKDF substitution
* noted in the spec): {@code Kdh = HKDF(IKM=ECDH_x(priv, peerPub),
* salt=txnId, info=∅, L=32)}. Writes 32 bytes to {@code out[outOff..]}
* and returns 32.
*/
short deriveKdh(
ECPrivateKey priv,
byte[] peerPubUncomp, short peerPubOff,
byte[] txnId, short txnIdOff, short txnIdLen,
byte[] out, short outOff) {
computeEcdhSharedX(priv, peerPubUncomp, peerPubOff, kdfWorkbuf, (short) 0);
hkdfExtract(
txnId, txnIdOff, txnIdLen,
kdfWorkbuf, (short) 0, HASH_LEN,
kdfWorkbuf, HASH_LEN);
hkdfExpand(
kdfWorkbuf, HASH_LEN, HASH_LEN,
kdfWorkbuf, (short) 0, (short) 0,
HASH_LEN,
out, outOff);
return HASH_LEN;
}
/** ASCII info strings for step-up session key derivation, per mdoc 9.1.1.5. */
private static final byte[] STEP_UP_INFO_SK_DEVICE = {
(byte) 'S', (byte) 'K', (byte) 'D', (byte) 'e',
(byte) 'v', (byte) 'i', (byte) 'c', (byte) 'e'
};
private static final byte[] STEP_UP_INFO_SK_READER = {
(byte) 'S', (byte) 'K', (byte) 'R', (byte) 'e',
(byte) 'a', (byte) 'd', (byte) 'e', (byte) 'r'
};
/**
* Derives {@code StepUpSKDevice} and {@code StepUpSKReader} per Aliro
* §8.4.3, which points at mdoc [6] clause 9.1.1.5 with two Aliro-specific
* changes: IKM is the {@code StepUpSK} computed in §8.3.1.13 and the salt
* is empty. Info remains the ASCII bytes of {@code "SKDevice"} and
* {@code "SKReader"}.
*
* <p>Writes 32 bytes each to {@code skDeviceOut[skDeviceOff..]} and
* {@code skReaderOut[skReaderOff..]}.
*/
short deriveStepUpSessionKeys(
byte[] stepUpSK, short stepUpSKOff,
byte[] skDeviceOut, short skDeviceOff,
byte[] skReaderOut, short skReaderOff) {
// HKDF-Extract once (shared across the two Expand calls): empty salt.
hkdfExtract(
kdfWorkbuf, (short) 0, (short) 0, // empty salt — use first 0 bytes of anything
stepUpSK, stepUpSKOff, HASH_LEN,
kdfWorkbuf, HASH_LEN); // PRK at [32..64)
hkdfExpand(
kdfWorkbuf, HASH_LEN, HASH_LEN,
STEP_UP_INFO_SK_DEVICE, (short) 0, (short) STEP_UP_INFO_SK_DEVICE.length,
HASH_LEN,
skDeviceOut, skDeviceOff);
hkdfExpand(
kdfWorkbuf, HASH_LEN, HASH_LEN,
STEP_UP_INFO_SK_READER, (short) 0, (short) STEP_UP_INFO_SK_READER.length,
HASH_LEN,
skReaderOut, skReaderOff);
return HASH_LEN;
}
/**
* Derives the 160-byte {@code derived_keys_volatile} for the
* expedited-standard phase per Aliro §8.3.1.13. Output is laid out as
* ExpeditedSKReader[0..32), ExpeditedSKDevice[32..64), StepUpSK[64..96),
* BleSK[96..128), URSK[128..160). For NFC-only operation only the
* first 96 bytes are load-bearing, but the spec mandates generating all
* 160 so the caller can discard what it doesn't need.
*/
short deriveExpeditedStandardKeys(
byte[] kdh, short kdhOff,
byte[] saltVolatile, short saltOff, short saltLen,
byte[] info, short infoOff, short infoLen,
byte[] out, short outOff) {
hkdfExtract(
saltVolatile, saltOff, saltLen,
kdh, kdhOff, HASH_LEN,
kdfWorkbuf, (short) 0);
hkdfExpand(
kdfWorkbuf, (short) 0, HASH_LEN,
info, infoOff, infoLen,
(short) 160,
out, outOff);
return (short) 160;
}
}

View File

@@ -0,0 +1,286 @@
package com.dangerousthings.aliro;
import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.AESKey;
import javacard.security.KeyBuilder;
import javacardx.crypto.Cipher;
/**
* Userland AES-256-GCM, built on top of the JC 3.0.5 single-block AES-ECB
* primitive. Needed because the target card (NXP J3R180) does not expose
* {@code AEADCipher.ALG_AES_GCM} despite advertising JC 3.0.5 support, while
* {@code Cipher.ALG_AES_BLOCK_128_ECB_NOPAD} and AES-256 keys are both
* present and functional.
*
* <p>Tailored to the Aliro AUTH1 response use case (spec &sect;8.3.1.6):
* AES-256 with a 12-byte IV, empty AAD, 16-byte tag, and plaintext on the
* order of 137 bytes. The NIST SP 800-38D canonical 96-bit-IV shortcut is
* used (IV is taken as {@code IV || 0x00000001} for J<sub>0</sub>, no IV
* hashing). This is the only IV length we ever produce — see
* {@link AliroApplet#encryptResponseGcm}.
*
* <p>GHASH follows NIST SP 800-38D &sect;6.4's bit-reflected convention:
* byte 0 bit 7 (MSB) is the highest-degree polynomial coefficient; the
* reduction polynomial R = x^128 + x^7 + x^2 + x + 1 is represented as the
* 16-byte array {0xE1, 0x00, ..., 0x00}; the GHASH shift-right shifts toward
* higher byte indices (lower polynomial degrees).
*
* <p>Not constant-time — the GF(2^128) multiply branches on bits of the
* authentication tag partial state. On a secure JC platform this is
* acceptable (the key material is smartcard-protected), but callers should
* not pass attacker-chosen GHASH state.
*/
final class AliroGcm {
private static final short BLOCK_LEN = 16;
private static final short TAG_LEN = 16;
private static final short IV_LEN = 12; // 96-bit IV shortcut (NIST SP 800-38D §7.1)
private static final short AES_256_KEY_BYTES = 32;
// Scratch layout — all 16-byte slots, single transient allocation.
private static final short OFF_H = 0; // hash subkey = AES_K(0^128)
private static final short OFF_J0 = 16; // pre-counter block
private static final short OFF_CB = 32; // running counter block
private static final short OFF_GHASH = 48; // accumulated GHASH state
private static final short OFF_ECB_OUT = 64; // AES keystream block (also used for J0 encryption)
private static final short OFF_TMP = 80; // misc: GF multiply workspace (V)
private static final short OFF_LEN_BLK = 96; // length block for GHASH finalization
private static final short SCRATCH_LEN = 112;
/** Persistent ECB cipher (AES-128/192/256 single-block-NOPAD). AES-256
* is driven by the size of the {@link AESKey} passed to {@link Cipher#init}. */
private final Cipher aesEcb;
/** Reusable 256-bit AES key slot. {@link AESKey#setKey} is called per encrypt. */
private final AESKey aesKey;
/** Transient scratch — see OFF_* constants above. */
private final byte[] scratch;
AliroGcm() {
aesEcb = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false);
aesKey = (AESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false);
scratch = JCSystem.makeTransientByteArray(SCRATCH_LEN, JCSystem.CLEAR_ON_DESELECT);
}
/**
* AES-256-GCM encrypt with the Aliro parameters: 32-byte key, 12-byte IV,
* empty AAD, 16-byte tag appended. Writes {@code ptLen} ciphertext bytes
* followed by 16 tag bytes to {@code out[outOff..]} and returns
* {@code ptLen + 16}.
*
* <p>The {@code out} buffer may overlap {@code pt} (same buffer, same
* offset is supported — we encrypt block-by-block left-to-right, the
* GHASH pass reads from {@code out}, and the tag lands past the end of
* the ciphertext).
*
* @param key 32-byte AES-256 key, at {@code key[keyOff..keyOff+32)}
* @param iv 12-byte IV, at {@code iv[ivOff..ivOff+12)}
* @param pt plaintext bytes
* @param ptOff, ptLen plaintext region
* @param out output buffer, must have at least {@code ptLen + 16} bytes
* available at {@code outOff}
* @param outOff output start offset
* @return {@code ptLen + 16}
*/
short encrypt(
byte[] key, short keyOff,
byte[] iv, short ivOff,
byte[] pt, short ptOff, short ptLen,
byte[] out, short outOff) {
aesKey.setKey(key, keyOff);
aesEcb.init(aesKey, Cipher.MODE_ENCRYPT);
// H = AES_K(0^128). Zero scratch[OFF_H..OFF_H+16) and encrypt in place.
Util.arrayFillNonAtomic(scratch, OFF_H, BLOCK_LEN, (byte) 0);
aesEcb.doFinal(scratch, OFF_H, BLOCK_LEN, scratch, OFF_H);
// J0 = IV || 0x00000001 (12B IV + 4B counter). 96-bit IV canonical case.
Util.arrayCopyNonAtomic(iv, ivOff, scratch, OFF_J0, IV_LEN);
scratch[(short) (OFF_J0 + 12)] = 0x00;
scratch[(short) (OFF_J0 + 13)] = 0x00;
scratch[(short) (OFF_J0 + 14)] = 0x00;
scratch[(short) (OFF_J0 + 15)] = 0x01;
// cb = INC32(J0) — first counter block for the plaintext stream.
Util.arrayCopyNonAtomic(scratch, OFF_J0, scratch, OFF_CB, BLOCK_LEN);
inc32(scratch, OFF_CB);
// GCTR loop: stream plaintext through AES-CTR, writing ciphertext to out.
short produced = 0;
while (produced < ptLen) {
short blockLen = (short) (ptLen - produced);
if (blockLen > BLOCK_LEN) blockLen = BLOCK_LEN;
// keystream = AES_K(cb)
aesEcb.doFinal(scratch, OFF_CB, BLOCK_LEN, scratch, OFF_ECB_OUT);
// ct[i] = pt[i] XOR keystream[i] — partial-block safe.
for (short i = 0; i < blockLen; i++) {
out[(short) (outOff + produced + i)] =
(byte) (pt[(short) (ptOff + produced + i)]
^ scratch[(short) (OFF_ECB_OUT + i)]);
}
inc32(scratch, OFF_CB);
produced += blockLen;
}
// GHASH over (AAD=empty || ciphertext || len_block).
// state = 0^128
Util.arrayFillNonAtomic(scratch, OFF_GHASH, BLOCK_LEN, (byte) 0);
// Fold ciphertext blocks into GHASH, zero-padding the final partial block.
short consumed = 0;
while (consumed < ptLen) {
short chunk = (short) (ptLen - consumed);
if (chunk >= BLOCK_LEN) {
// XOR full 16-byte block directly from the output buffer.
for (short i = 0; i < BLOCK_LEN; i++) {
scratch[(short) (OFF_GHASH + i)] ^= out[(short) (outOff + consumed + i)];
}
consumed += BLOCK_LEN;
} else {
// Partial block: XOR the available bytes, implicit zero-pad for the rest.
for (short i = 0; i < chunk; i++) {
scratch[(short) (OFF_GHASH + i)] ^= out[(short) (outOff + consumed + i)];
}
consumed += chunk;
}
gfMul(scratch, OFF_GHASH, scratch, OFF_H);
}
// len_block = (8*|AAD|)^64 || (8*|C|)^64, big-endian. AAD is always empty
// for this use case; ptLen fits in 16 bits, so only the low 2 bytes of
// the 8-byte plaintext-bit-length field can be non-zero.
Util.arrayFillNonAtomic(scratch, OFF_LEN_BLK, BLOCK_LEN, (byte) 0);
// ptLen·8 in bits, big-endian, into the last 4 bytes of the 16-byte
// length block (bits 0-95 are zero — AAD-bits=0 + high bytes of CT-bits).
// Java Card 3.0.5 forbids `int` literals, so we compute as `short` and
// split the byte-shift into two stages to dodge the implicit `int`
// promotion that the converter rejects ("unsupported int type constant").
// ptLen ≤ 8191 ⇒ ptLen·8 ≤ 65528, fits in unsigned 16 bits. The top two
// bytes of the length field stay zero.
short ctBitsLo = (short) (ptLen << 3); // low 16 bits of bit-count
short ctBitsHi = (short) (((short)(ptLen >>> 13)) & 0x07); // bits 16-18 of bit-count
scratch[(short) (OFF_LEN_BLK + 13)] = (byte) (ctBitsHi & 0xFF);
scratch[(short) (OFF_LEN_BLK + 14)] = (byte) ((ctBitsLo >>> 8) & 0xFF);
scratch[(short) (OFF_LEN_BLK + 15)] = (byte) (ctBitsLo & 0xFF);
for (short i = 0; i < BLOCK_LEN; i++) {
scratch[(short) (OFF_GHASH + i)] ^= scratch[(short) (OFF_LEN_BLK + i)];
}
gfMul(scratch, OFF_GHASH, scratch, OFF_H);
// T = AES_K(J0) XOR GHASH
aesEcb.doFinal(scratch, OFF_J0, BLOCK_LEN, scratch, OFF_ECB_OUT);
short tagOut = (short) (outOff + ptLen);
for (short i = 0; i < TAG_LEN; i++) {
out[(short) (tagOut + i)] =
(byte) (scratch[(short) (OFF_ECB_OUT + i)]
^ scratch[(short) (OFF_GHASH + i)]);
}
// Wipe scratch: H, J0 (with IV bytes), GHASH state, and the keystream
// remnant in OFF_ECB_OUT all touched the AES key directly. CLEAR_ON_DESELECT
// would clean up at deselect, but multi-APDU sessions could otherwise
// see stale H/keystream until then.
Util.arrayFillNonAtomic(scratch, (short) 0, SCRATCH_LEN, (byte) 0);
return (short) (ptLen + TAG_LEN);
}
/**
* INC32 per NIST SP 800-38D §6.2: increments the last 4 bytes of the
* 16-byte block, big-endian, modulo 2^32.
*/
private static void inc32(byte[] buf, short off) {
short i = (short) (off + 15);
// Carry propagation across 4 bytes.
if ((byte) (++buf[i]) != (byte) 0) return;
i--;
if ((byte) (++buf[i]) != (byte) 0) return;
i--;
if ((byte) (++buf[i]) != (byte) 0) return;
i--;
buf[i]++;
}
/**
* In-place GF(2^128) multiplication per NIST SP 800-38D §6.3, storing
* X ·_GF Y into X. Uses the bit-reflected convention:
* <ul>
* <li>byte 0 MSB = x^127 (highest-degree polynomial coefficient)
* <li>byte 15 LSB = x^0
* <li>reduction polynomial R = x^128 + x^7 + x^2 + x + 1 represented
* as {0xE1, 0x00, ..., 0x00}
* <li>"shift right" means shift toward higher byte indices
* </ul>
*
* <p>Algorithm 1 from NIST SP 800-38D:
* <pre>
* Z = 0; V = Y
* for i = 0..127:
* if bit i of X is 1: Z ^= V
* if lowest bit of V (byte 15 bit 0) is 1: V = (V >> 1) ^ R
* else: V = V >> 1
* return Z
* </pre>
*
* <p>This is the simple, portable NIST shift-XOR form. A 4-bit-table
* variant would be ~4x faster but adds 32 bytes of per-H precomputation
* and a more intricate shift; we encrypt ~137B per AUTH1, so the extra
* cycles are acceptable versus the complexity cost.
*/
private void gfMul(byte[] xBuf, short xOff, byte[] yBuf, short yOff) {
// V = Y (copied into OFF_TMP scratch region; gets shifted in place)
Util.arrayCopyNonAtomic(yBuf, yOff, scratch, OFF_TMP, BLOCK_LEN);
// Z = 0 (we'll accumulate in a second 16-byte region — reuse OFF_LEN_BLK,
// which is free until after the main GHASH loop finishes). gfMul is
// called both during GHASH accumulation AND during final-length mixing;
// at final-length mixing OFF_LEN_BLK has already been consumed, so it's
// safe to overwrite. But to avoid an aliasing bug if OFF_LEN_BLK is
// ever reused differently, we stage Z in OFF_ECB_OUT instead.
//
// Wait — OFF_ECB_OUT holds the AES keystream we need later for the
// tag (T = AES_K(J0) XOR GHASH). However, gfMul is never called
// between producing OFF_ECB_OUT (the AES_K(J0) output) and consuming
// it for the tag XOR. The last gfMul is for (state ^ len_block) ·_GF H,
// and only AFTER that do we compute AES_K(J0) into OFF_ECB_OUT. So
// using OFF_ECB_OUT as the Z accumulator is safe.
Util.arrayFillNonAtomic(scratch, OFF_ECB_OUT, BLOCK_LEN, (byte) 0);
for (short byteIdx = 0; byteIdx < BLOCK_LEN; byteIdx++) {
byte xByte = xBuf[(short) (xOff + byteIdx)];
for (short bit = 7; bit >= 0; bit--) {
// bit of X at polynomial position (byteIdx*8 + (7-bit)): byte-high bit first.
if (((xByte >> bit) & 0x01) != 0) {
for (short k = 0; k < BLOCK_LEN; k++) {
scratch[(short) (OFF_ECB_OUT + k)] ^= scratch[(short) (OFF_TMP + k)];
}
}
// V = V >> 1; if the bit that fell off (byte 15 low bit BEFORE shift) is 1,
// also XOR the reduction polynomial {0xE1, 0x00, ..., 0x00}.
boolean lsb = (scratch[(short) (OFF_TMP + 15)] & 0x01) != 0;
// Shift V right by 1 bit (toward higher byte indices).
short carry = 0;
for (short k = 0; k < BLOCK_LEN; k++) {
short cur = (short) (scratch[(short) (OFF_TMP + k)] & 0xFF);
short newCur = (short) ((cur >>> 1) | carry);
carry = (short) ((cur & 0x01) << 7);
scratch[(short) (OFF_TMP + k)] = (byte) newCur;
}
if (lsb) {
scratch[OFF_TMP] ^= (byte) 0xE1;
}
}
}
// Copy Z back into X in place.
Util.arrayCopyNonAtomic(scratch, OFF_ECB_OUT, xBuf, xOff, BLOCK_LEN);
}
}

View File

@@ -0,0 +1,110 @@
package com.dangerousthings.aliro;
import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.MessageDigest;
/**
* Userland HMAC-SHA-256 per RFC 2104, built on top of the JC
* {@link MessageDigest#ALG_SHA_256} primitive. Needed because the target card
* (NXP J3R180) does not expose {@code KeyBuilder.TYPE_HMAC} at any tested key
* size (512/256/160/128 bits) nor {@code Signature.ALG_HMAC_SHA_256}, while
* raw SHA-256 is present and functional.
*
* <p>Each {@link #compute} call reads a key and a message, zero-pads the key
* to the SHA-256 block size (64 bytes) — first hashing the key down with
* SHA-256 if it exceeds the block size — then computes:
* <pre>
* HMAC(K, M) = SHA-256((K_pad XOR opad) || SHA-256((K_pad XOR ipad) || M))
* </pre>
* with {@code ipad = 0x36 × 64} and {@code opad = 0x5C × 64}. Output is
* always 32 bytes.
*
* <p>Intended to be instantiated once per applet (or test) and reused — the
* underlying {@link MessageDigest} is expensive to allocate.
*/
final class AliroHmac {
private static final short BLOCK_LEN = 64; // SHA-256 block size
private static final short HASH_LEN = 32; // SHA-256 output size
private static final byte IPAD_BYTE = (byte) 0x36;
private static final byte OPAD_BYTE = (byte) 0x5C;
// Scratch layout: 64B pad block || 32B inner digest = 96B total.
private static final short OFF_PAD = 0;
private static final short OFF_INNER = 64;
private static final short SCRATCH_LEN = 96;
/** Persistent SHA-256 digest. Always reset before reuse — JC
* {@code MessageDigest} does not auto-reset after {@link MessageDigest#doFinal}. */
private final MessageDigest sha256;
/** Transient scratch — see OFF_* constants above. */
private final byte[] scratch;
AliroHmac() {
sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
scratch = JCSystem.makeTransientByteArray(SCRATCH_LEN, JCSystem.CLEAR_ON_DESELECT);
}
/**
* HMAC-SHA-256(key, msg). Writes 32 bytes to {@code out[outOff..outOff+32)}
* and returns 32. The key is copied into internal scratch before the first
* hash, and the message is consumed during the inner SHA-256 pass before
* anything is written to {@code out}, so the {@code key}, {@code msg}, and
* {@code out} buffers may alias each other freely.
*/
short compute(
byte[] key, short keyOff, short keyLen,
byte[] msg, short msgOff, short msgLen,
byte[] out, short outOff) {
// --- Build K_pad into scratch[OFF_PAD..OFF_PAD+64) ---
// Zero the 64-byte pad region first so any unused key-tail bytes stay 0.
Util.arrayFillNonAtomic(scratch, OFF_PAD, BLOCK_LEN, (byte) 0);
if (keyLen > BLOCK_LEN) {
// K' = SHA-256(K), 32 bytes; the remaining 32 bytes stay zero.
sha256.reset();
sha256.doFinal(key, keyOff, keyLen, scratch, OFF_PAD);
} else if (keyLen > 0) {
Util.arrayCopyNonAtomic(key, keyOff, scratch, OFF_PAD, keyLen);
}
// else keyLen == 0: scratch[OFF_PAD..OFF_PAD+64) stays all zeros.
// --- Inner: SHA-256((K_pad XOR ipad) || M) into scratch[OFF_INNER..) ---
for (short i = 0; i < BLOCK_LEN; i++) {
scratch[(short) (OFF_PAD + i)] ^= IPAD_BYTE;
}
sha256.reset();
sha256.update(scratch, OFF_PAD, BLOCK_LEN);
if (msgLen > 0) {
sha256.doFinal(msg, msgOff, msgLen, scratch, OFF_INNER);
} else {
// doFinal with length 0 — pass the same buffer as a harmless source.
sha256.doFinal(scratch, OFF_PAD, (short) 0, scratch, OFF_INNER);
}
// --- Recover K_pad from (K_pad XOR ipad), then XOR opad. ---
// (K_pad XOR ipad) XOR (ipad XOR opad) = K_pad XOR opad, so one pass
// over the block suffices. ipad XOR opad = 0x36 XOR 0x5C = 0x6A.
for (short i = 0; i < BLOCK_LEN; i++) {
scratch[(short) (OFF_PAD + i)] ^= (byte) 0x6A;
}
// --- Outer: SHA-256((K_pad XOR opad) || inner) into out[outOff..) ---
sha256.reset();
sha256.update(scratch, OFF_PAD, BLOCK_LEN);
sha256.doFinal(scratch, OFF_INNER, HASH_LEN, out, outOff);
// Wipe scratch — it held K_pad (derived from the HMAC key) and the
// inner digest, both of which would leak key-dependent state to a
// later HMAC invocation on a different key otherwise. CLEAR_ON_DESELECT
// cleans up at deselect, but multi-APDU sessions could see stale
// state until then.
Util.arrayFillNonAtomic(scratch, (short) 0, SCRATCH_LEN, (byte) 0);
return HASH_LEN;
}
}

View File

@@ -0,0 +1,190 @@
package com.dangerousthings.aliro;
/**
* Shared, package-private provisioning store. Holds the Access Credential
* private key and (eventually) reader public keys. The
* {@link PersonalizationApplet} writes into it; {@link AliroApplet} reads
* from it when it needs long-term keys for AUTH1 / step-up.
*
* <p>Two applets in the same CAP file share this class's static state
* directly — no {@code Shareable} interface needed.
*
* <p>After {@link #commit()} is called, further writes via the
* personalization interface are refused. The only way to re-unlock is
* a factory reset, which is not yet implemented.
*/
final class CredentialStore {
static final short CRED_PRIV_KEY_LEN = 32;
static final short CRED_PUBK_LEN = 64;
static final short READER_PUBK_LEN = 64;
/** Max Access Document blob size (serialized COSE_Sign1 bytes). 1 KB
* accommodates a typical Aliro Access Document with room to spare. */
static final short ACCESS_DOC_MAX_LEN = 1024;
/** Lazily initialized in {@link #get()}. Java Card bans {@code new} in
* static initializers, so we can't declare {@code = new CredentialStore()}. */
private static CredentialStore INSTANCE;
private final byte[] credentialPrivKey;
private boolean credentialPrivKeySet;
private final byte[] credentialPubKey;
private boolean credentialPubKeySet;
private final byte[] readerPubKey;
private boolean readerPubKeySet;
private final byte[] accessDocument;
private short accessDocumentLen;
private boolean accessDocumentFinalized;
private boolean committed;
private CredentialStore() {
credentialPrivKey = new byte[CRED_PRIV_KEY_LEN];
credentialPubKey = new byte[CRED_PUBK_LEN];
readerPubKey = new byte[READER_PUBK_LEN];
accessDocument = new byte[ACCESS_DOC_MAX_LEN];
}
static CredentialStore get() {
if (INSTANCE == null) {
INSTANCE = new CredentialStore();
}
return INSTANCE;
}
boolean isCommitted() {
return committed;
}
void commit() {
committed = true;
}
/**
* Returns true iff a credential private key has been written. Callers
* that need the actual bytes should use {@link #copyCredentialPrivKey}.
*/
boolean hasCredentialPrivKey() {
return credentialPrivKeySet;
}
void setCredentialPrivKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN);
credentialPrivKeySet = true;
}
short copyCredentialPrivKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPrivKey, (short) 0, dst, dstOff, CRED_PRIV_KEY_LEN);
return CRED_PRIV_KEY_LEN;
}
boolean hasCredentialPubKey() {
return credentialPubKeySet;
}
void setCredentialPubKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, credentialPubKey, (short) 0, CRED_PUBK_LEN);
credentialPubKeySet = true;
}
short copyCredentialPubKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPubKey, (short) 0, dst, dstOff, CRED_PUBK_LEN);
return CRED_PUBK_LEN;
}
/** Copies only the 32-byte x-coordinate of credential_PubK (spec uses x-only in salt_volatile). */
short copyCredentialPubKeyX(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPubKey, (short) 0, dst, dstOff, (short) 32);
return (short) 32;
}
boolean hasReaderPubKey() {
return readerPubKeySet;
}
void setReaderPubKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, readerPubKey, (short) 0, READER_PUBK_LEN);
readerPubKeySet = true;
}
short copyReaderPubKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
readerPubKey, (short) 0, dst, dstOff, READER_PUBK_LEN);
return READER_PUBK_LEN;
}
/** Copies only the 32-byte x-coordinate of the reader_PubK. */
short copyReaderPubKeyX(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
readerPubKey, (short) 0, dst, dstOff, (short) 32);
return (short) 32;
}
/**
* Stores an Access Document chunk at the given destination offset.
* Returns true iff the write stayed within {@link #ACCESS_DOC_MAX_LEN};
* otherwise the store is unchanged.
*/
boolean writeAccessDocumentChunk(byte[] src, short srcOff, short dstOff, short len) {
if ((short) (dstOff + len) > ACCESS_DOC_MAX_LEN || dstOff < 0 || len < 0) {
return false;
}
javacard.framework.Util.arrayCopyNonAtomic(src, srcOff, accessDocument, dstOff, len);
return true;
}
/**
* Marks the Access Document as provisioned with {@code totalLen} bytes
* of valid content starting at offset 0. Returns false (and does not
* mutate state) if {@code totalLen} is outside {@code [0, ACCESS_DOC_MAX_LEN]}.
*/
boolean finalizeAccessDocument(short totalLen) {
if (totalLen < 0 || totalLen > ACCESS_DOC_MAX_LEN) {
return false;
}
accessDocumentLen = totalLen;
accessDocumentFinalized = true;
return true;
}
boolean hasAccessDocument() {
return accessDocumentFinalized;
}
short getAccessDocumentLen() {
return accessDocumentLen;
}
short copyAccessDocument(byte[] dst, short dstOff, short srcOff, short len) {
javacard.framework.Util.arrayCopyNonAtomic(accessDocument, srcOff, dst, dstOff, len);
return len;
}
/**
* Test-only hook. The store is a static singleton, which makes unit
* tests interfere with each other unless each test starts from a
* known-blank slate. Package-private and named for clarity.
*/
void resetForTesting() {
javacard.framework.Util.arrayFillNonAtomic(credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(credentialPubKey, (short) 0, CRED_PUBK_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(readerPubKey, (short) 0, READER_PUBK_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(accessDocument, (short) 0, ACCESS_DOC_MAX_LEN, (byte) 0);
credentialPrivKeySet = false;
credentialPubKeySet = false;
readerPubKeySet = false;
accessDocumentLen = 0;
accessDocumentFinalized = false;
committed = false;
}
}

View File

@@ -0,0 +1,148 @@
package com.dangerousthings.aliro;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
/**
* Receives Access Credential long-term private key, reader public keys,
* and (later) the pre-signed Access Document bytes. Accepts writes only
* until {@link CredentialStore#commit()}; afterwards every write returns
* SW_CONDITIONS_NOT_SATISFIED.
*
* <p>This applet lives under {@link AliroAids#PROVISIONING}, a
* DangerousThings-proprietary AID — it is not part of the Aliro spec
* surface.
*/
public class PersonalizationApplet extends Applet {
private static final byte CLA = (byte) 0x80;
private static final byte INS_SET_CREDENTIAL_PRIV_KEY = (byte) 0x20;
private static final byte INS_SET_CREDENTIAL_PUBK = (byte) 0x21;
private static final byte INS_SET_READER_PUBK = (byte) 0x22;
private static final byte INS_WRITE_ACCESS_DOC = (byte) 0x23;
private static final byte INS_FINALIZE_ACCESS_DOC = (byte) 0x24;
private static final byte INS_COMMIT = (byte) 0x2C;
public static void install(byte[] bArray, short bOffset, byte bLength) {
PersonalizationApplet applet = new PersonalizationApplet();
if (bArray == null || bLength == 0) {
applet.register();
} else {
applet.register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
}
@Override
public void process(APDU apdu) {
if (selectingApplet()) {
return;
}
byte[] buf = apdu.getBuffer();
if (buf[ISO7816.OFFSET_CLA] != CLA) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
CredentialStore store = CredentialStore.get();
byte ins = buf[ISO7816.OFFSET_INS];
switch (ins) {
case INS_SET_CREDENTIAL_PRIV_KEY:
requireUnlocked(store);
setCredentialPrivKey(apdu, store);
return;
case INS_SET_CREDENTIAL_PUBK:
requireUnlocked(store);
setCredentialPubKey(apdu, store);
return;
case INS_SET_READER_PUBK:
requireUnlocked(store);
setReaderPubKey(apdu, store);
return;
case INS_WRITE_ACCESS_DOC:
requireUnlocked(store);
writeAccessDocChunk(apdu, store);
return;
case INS_FINALIZE_ACCESS_DOC:
requireUnlocked(store);
finalizeAccessDoc(apdu, store);
return;
case INS_COMMIT:
requireUnlocked(store);
store.commit();
return;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
private static void requireUnlocked(CredentialStore store) {
if (store.isCommitted()) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
private void setCredentialPrivKey(APDU apdu, CredentialStore store) {
short lc = apdu.setIncomingAndReceive();
if (lc != CredentialStore.CRED_PRIV_KEY_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte[] buf = apdu.getBuffer();
store.setCredentialPrivKey(buf, apdu.getOffsetCdata());
}
private void setCredentialPubKey(APDU apdu, CredentialStore store) {
short lc = apdu.setIncomingAndReceive();
if (lc != CredentialStore.CRED_PUBK_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte[] buf = apdu.getBuffer();
store.setCredentialPubKey(buf, apdu.getOffsetCdata());
}
private void setReaderPubKey(APDU apdu, CredentialStore store) {
short lc = apdu.setIncomingAndReceive();
if (lc != CredentialStore.READER_PUBK_LEN) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte[] buf = apdu.getBuffer();
store.setReaderPubKey(buf, apdu.getOffsetCdata());
}
/**
* Writes an Access Document chunk. P1|P2 is the destination offset
* within the stored Access Document (big-endian unsigned 16-bit); the
* command data field is the chunk bytes.
*/
private void writeAccessDocChunk(APDU apdu, CredentialStore store) {
short lc = apdu.setIncomingAndReceive();
byte[] buf = apdu.getBuffer();
short dstOff = (short) (((buf[ISO7816.OFFSET_P1] & 0xFF) << 8)
| (buf[ISO7816.OFFSET_P2] & 0xFF));
boolean ok = store.writeAccessDocumentChunk(
buf, apdu.getOffsetCdata(), dstOff, lc);
if (!ok) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
}
/**
* Marks the Access Document as provisioned. P1|P2 is the total length
* (big-endian unsigned 16-bit). Lc must be 0.
*/
private void finalizeAccessDoc(APDU apdu, CredentialStore store) {
short lc = apdu.setIncomingAndReceive();
if (lc != 0) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte[] buf = apdu.getBuffer();
short totalLen = (short) (((buf[ISO7816.OFFSET_P1] & 0xFF) << 8)
| (buf[ISO7816.OFFSET_P2] & 0xFF));
if (!store.finalizeAccessDocument(totalLen)) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
}
}

View File

@@ -0,0 +1,78 @@
package com.dangerousthings.aliro;
import javacard.framework.JCSystem;
import javacard.framework.Util;
/**
* Cross-applet session state that survives an AID change (EXPEDITED →
* STEP_UP) but not a card reset. Since Java Card installs distinct
* applet instances per AID, {@link AliroApplet} and {@link StepUpApplet}
* need a shared holder to hand off {@code StepUpSK}, the session-bound
* 32-byte key derived during AUTH1 and consumed in the step-up phase.
*
* <p>Storage is a {@link JCSystem#CLEAR_ON_RESET} transient array,
* lazily allocated on first access so that neither applet's constructor
* assumes a particular install order. The reference is a static field;
* both applets see the same underlying array.
*
* <p>State machine:
* <ul>
* <li>Initial / post-reset: disarmed, {@code StepUpSK} zeroed.
* <li>{@link AliroApplet} SELECT or failure path: calls
* {@link #disarmStepUp()} to clear any stale state.
* <li>{@link AliroApplet} AUTH1 success: calls
* {@link #armStepUp(byte[], short)} to load the freshly-derived
* StepUpSK and flip the armed flag.
* <li>{@link StepUpApplet}: reads via {@link #isStepUpArmed()} + (future)
* an internal {@code copyStepUpSK} call to derive session keys.
* </ul>
*/
final class SessionContext {
static final short STEP_UP_SK_LEN = 32;
private static byte[] stepUpSK;
private static boolean[] armedFlag; // length 1 — boolean transient arrays are per-JC convention
private SessionContext() {}
/** Allocates the transient storage on first call. Subsequent calls are no-ops. */
static void ensureInitialized() {
if (stepUpSK == null) {
stepUpSK = JCSystem.makeTransientByteArray(STEP_UP_SK_LEN, JCSystem.CLEAR_ON_RESET);
armedFlag = JCSystem.makeTransientBooleanArray((short) 1, JCSystem.CLEAR_ON_RESET);
}
}
static boolean isStepUpArmed() {
return armedFlag != null && armedFlag[0];
}
/** Loads {@code StepUpSK} from {@code src[srcOff..srcOff+32)} and arms the context. */
static void armStepUp(byte[] src, short srcOff) {
ensureInitialized();
Util.arrayCopyNonAtomic(src, srcOff, stepUpSK, (short) 0, STEP_UP_SK_LEN);
armedFlag[0] = true;
}
/** Zeroes {@code StepUpSK} and flips the armed flag off. */
static void disarmStepUp() {
if (stepUpSK != null) {
Util.arrayFillNonAtomic(stepUpSK, (short) 0, STEP_UP_SK_LEN, (byte) 0);
}
if (armedFlag != null) {
armedFlag[0] = false;
}
}
/** Copies the armed StepUpSK into {@code dst[dstOff..]}. Only legal if {@link #isStepUpArmed()}. */
static short copyStepUpSK(byte[] dst, short dstOff) {
Util.arrayCopyNonAtomic(stepUpSK, (short) 0, dst, dstOff, STEP_UP_SK_LEN);
return STEP_UP_SK_LEN;
}
/** Test-only accessor that skips the armed-flag precondition. */
static short copyStepUpSKForTesting(byte[] dst, short dstOff) {
return copyStepUpSK(dst, dstOff);
}
}

View File

@@ -0,0 +1,86 @@
package com.dangerousthings.aliro;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
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).
*/
public class StepUpApplet extends Applet {
private static final byte CLA_PROPRIETARY = (byte) 0x80;
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]);
}
}
@Override
public boolean select() {
return true;
}
@Override
public void process(APDU apdu) {
if (selectingApplet()) {
sendStepUpFci(apdu);
return;
}
byte[] buf = apdu.getBuffer();
if (buf[ISO7816.OFFSET_CLA] != CLA_PROPRIETARY) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
// Until the mdoc DeviceRequest pipeline lands, every proprietary INS
// is unrecognised. ENVELOPE + GET RESPONSE handlers plug in here.
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
/**
* 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);
}
}