feat: diagnostic INSes for AUTH1 sub-op profiling (compile-gated)
Adds INS_DIAG_HMAC (0xD0), INS_DIAG_ECDH (0xD1), INS_DIAG_ECDSA_SIGN (0xD2), and INS_DIAG_GCM (0xD3) to AliroApplet, gated by a DIAGNOSTICS_ENABLED compile-time flag so they're trivially strippable for production builds. Each INS runs its primitive N times (N in P3 byte) against hardcoded card-side test vectors and returns the result. Output lands in the APDU buffer at offset 16, never in scratch -- the AUTH session state stays intact even if a diag INS runs mid-session. The HMAC INS routes through a new AliroCrypto.diagHmac() wrapper that exposes the internal AliroHmac instance without leaking it through the class boundary. ECDH/ECDSA use a dedicated diagKeyPair (separate from the protocol's credentialEphemeralKeyPair) so a diagnostic call can never disturb a real AUTH flow. These INSes are what aliro-bench-profile drives over PC/SC -- they let us measure each AUTH1 primitive's cost in isolation and reconstruct the AUTH1 budget against the bench-test wall-clock. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,25 @@ public class AliroApplet extends Applet {
|
||||
private static final byte INS_AUTH0 = (byte) 0x80;
|
||||
private static final byte INS_AUTH1 = (byte) 0x81;
|
||||
|
||||
// Diagnostic INSes (CLA=0x80) for profiling AUTH1 sub-operations.
|
||||
// DEV / PERFORMANCE-DEBUG ONLY. Set DIAGNOSTICS_ENABLED = false for any
|
||||
// production CAP -- the JC converter dead-code-eliminates the disabled
|
||||
// branches, so production binaries have zero attack surface from these.
|
||||
//
|
||||
// When enabled, each INS takes Lc=1 byte = iteration count N (1-255),
|
||||
// runs the operation N times against hardcoded test vectors with sizes
|
||||
// that match what AUTH1 actually does, then returns SW=9000. Output is
|
||||
// written to the APDU buffer (per-APDU, transient -- never aliases the
|
||||
// AUTH session scratch). The diagnostic keypair is allocated separately
|
||||
// from the protocol's ephemeral keypair, so calling diag mid-transaction
|
||||
// cannot clobber an in-flight AUTH0/AUTH1.
|
||||
private static final boolean DIAGNOSTICS_ENABLED = true;
|
||||
|
||||
private static final byte INS_DIAG_HMAC = (byte) 0xD0;
|
||||
private static final byte INS_DIAG_ECDH = (byte) 0xD1;
|
||||
private static final byte INS_DIAG_ECDSA_SIGN = (byte) 0xD2;
|
||||
private static final byte INS_DIAG_GCM = (byte) 0xD3;
|
||||
|
||||
// Session state layout (transient, CLEAR_ON_DESELECT + reset on SELECT).
|
||||
private static final short OFF_READER_EPUBK = 0;
|
||||
private static final short OFF_READER_GROUP_ID = 65;
|
||||
@@ -310,6 +329,141 @@ public class AliroApplet extends Applet {
|
||||
priv.setK((short) 1);
|
||||
}
|
||||
|
||||
// --- Diagnostic test vectors -------------------------------------------
|
||||
// Hardcoded inputs for the INS_DIAG_* operations. Sizes match what AUTH1
|
||||
// exercises: 32B HMAC key (matches HKDF PRK), 64B HMAC message (matches
|
||||
// SHA-256 block size), 12B AES-GCM IV, 137B plaintext (matches the actual
|
||||
// Table 8-11 plaintext length when cmd_params bit 0 = 1).
|
||||
private static final byte[] DIAG_KEY_32 = {
|
||||
(byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03,
|
||||
(byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
|
||||
(byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B,
|
||||
(byte) 0x0C, (byte) 0x0D, (byte) 0x0E, (byte) 0x0F,
|
||||
(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13,
|
||||
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17,
|
||||
(byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x1B,
|
||||
(byte) 0x1C, (byte) 0x1D, (byte) 0x1E, (byte) 0x1F
|
||||
};
|
||||
private static final byte[] DIAG_MSG_64 = {
|
||||
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27,
|
||||
(byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x2B, (byte) 0x2C, (byte) 0x2D, (byte) 0x2E, (byte) 0x2F,
|
||||
(byte) 0x30, (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37,
|
||||
(byte) 0x38, (byte) 0x39, (byte) 0x3A, (byte) 0x3B, (byte) 0x3C, (byte) 0x3D, (byte) 0x3E, (byte) 0x3F,
|
||||
(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
|
||||
(byte) 0x48, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F,
|
||||
(byte) 0x50, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57,
|
||||
(byte) 0x58, (byte) 0x59, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F
|
||||
};
|
||||
private static final byte[] DIAG_IV_12 = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01
|
||||
};
|
||||
private static final short DIAG_GCM_PT_LEN = (short) 137;
|
||||
|
||||
/** Dedicated diagnostic keypair, allocated lazily on first INS_DIAG_* and
|
||||
* used ONLY by the diag dispatcher. Kept separate from
|
||||
* {@link #credentialEphemeralKeyPair} so a diagnostic call cannot
|
||||
* clobber an in-flight AUTH0/AUTH1 transaction's ephemeral key. */
|
||||
private javacard.security.KeyPair diagKeyPair;
|
||||
|
||||
/** Persistent flag: 1 once {@link #diagKeyPair} has been allocated and
|
||||
* seeded with a random P-256 scalar via genKeyPair(). */
|
||||
private byte diagInitialized;
|
||||
|
||||
private void ensureDiagInitialized() {
|
||||
if (diagInitialized != 0) return;
|
||||
diagKeyPair = new javacard.security.KeyPair(
|
||||
javacard.security.KeyPair.ALG_EC_FP,
|
||||
javacard.security.KeyBuilder.LENGTH_EC_FP_256);
|
||||
seedSecp256r1(diagKeyPair);
|
||||
diagKeyPair.genKeyPair();
|
||||
diagInitialized = 1;
|
||||
}
|
||||
|
||||
/** Dispatch for the four INS_DIAG_* profiling operations. Output is
|
||||
* written into the APDU buffer (which is per-APDU and never aliases the
|
||||
* AUTH session's {@link #scratch}), and all crypto runs against the
|
||||
* dedicated {@link #diagKeyPair}, never the protocol's ephemeral key.
|
||||
* Caller already gated on {@link #DIAGNOSTICS_ENABLED}. */
|
||||
private void processDiag(APDU apdu, byte ins) {
|
||||
ensureDiagInitialized();
|
||||
short lc = apdu.setIncomingAndReceive();
|
||||
byte[] buf = apdu.getBuffer();
|
||||
if (lc != (short) 1) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
||||
}
|
||||
short n = (short) (buf[apdu.getOffsetCdata()] & 0xFF);
|
||||
if (n == (short) 0) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
// Diagnostic output lands in the APDU buffer past the Lc/data header
|
||||
// (offset 16 leaves space for HMAC/ECDH 32B output, ECDSA DER sigs up
|
||||
// to 72B, and AES-GCM 153B ciphertext+tag -- all fit in the standard
|
||||
// 261-byte APDU buffer). Never reuses scratch -- AUTH session state
|
||||
// stays intact.
|
||||
final short DIAG_OUT_OFF = (short) 16;
|
||||
// AES-GCM: encrypt in place at offset 16. pt/out overlap at the same
|
||||
// offset is supported by AliroGcm.encrypt. Output is 137B ciphertext
|
||||
// + 16B tag = 153B, ending at offset 169 -- well inside the 261B
|
||||
// APDU buffer. Old code tried pt at offset 176 (16 + 160 reserved
|
||||
// for the CT region) but that ran the 137B read off the end of the
|
||||
// buffer at offset 313, throwing ArrayIndexOutOfBoundsException.
|
||||
final short DIAG_GCM_PT_OFF = (short) 16;
|
||||
final short DIAG_GCM_OUT_OFF = (short) 16;
|
||||
|
||||
switch (ins) {
|
||||
case INS_DIAG_HMAC: {
|
||||
for (short i = 0; i < n; i++) {
|
||||
crypto.diagHmac(
|
||||
DIAG_KEY_32, (short) 0, (short) DIAG_KEY_32.length,
|
||||
DIAG_MSG_64, (short) 0, (short) DIAG_MSG_64.length,
|
||||
buf, DIAG_OUT_OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case INS_DIAG_ECDH: {
|
||||
javacard.security.ECPrivateKey priv =
|
||||
(javacard.security.ECPrivateKey) diagKeyPair.getPrivate();
|
||||
for (short i = 0; i < n; i++) {
|
||||
crypto.computeEcdhSharedX(priv,
|
||||
SECP256R1_G, (short) 0,
|
||||
buf, DIAG_OUT_OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case INS_DIAG_ECDSA_SIGN: {
|
||||
// Sign with the DEDICATED diag keypair, never the protocol's
|
||||
// credential or ephemeral keys. The signed message is also a
|
||||
// fixed test vector, so this cannot be coerced into signing
|
||||
// attacker-chosen data.
|
||||
ecdsaSigner.init(diagKeyPair.getPrivate(),
|
||||
Signature.MODE_SIGN);
|
||||
for (short i = 0; i < n; i++) {
|
||||
ecdsaSigner.sign(
|
||||
DIAG_MSG_64, (short) 0, (short) DIAG_MSG_64.length,
|
||||
buf, DIAG_OUT_OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case INS_DIAG_GCM: {
|
||||
// Plaintext: 137 bytes anywhere in buf past the output region.
|
||||
// Contents don't affect timing.
|
||||
for (short i = 0; i < n; i++) {
|
||||
gcm.encrypt(
|
||||
DIAG_KEY_32, (short) 0,
|
||||
DIAG_IV_12, (short) 0,
|
||||
buf, DIAG_GCM_PT_OFF, DIAG_GCM_PT_LEN,
|
||||
buf, DIAG_GCM_OUT_OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean select() {
|
||||
resetSession();
|
||||
@@ -357,6 +511,20 @@ public class AliroApplet extends Applet {
|
||||
case INS_AUTH1:
|
||||
processAuth1(apdu);
|
||||
return;
|
||||
case INS_DIAG_HMAC:
|
||||
case INS_DIAG_ECDH:
|
||||
case INS_DIAG_ECDSA_SIGN:
|
||||
case INS_DIAG_GCM:
|
||||
// Compile-time gate: with DIAGNOSTICS_ENABLED=false the JC
|
||||
// converter dead-code-eliminates the processDiag call, so a
|
||||
// production CAP rejects these INSes the same way it rejects
|
||||
// any other unsupported INS -- zero residual surface.
|
||||
if (DIAGNOSTICS_ENABLED) {
|
||||
processDiag(apdu, ins);
|
||||
return;
|
||||
}
|
||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||
return;
|
||||
default:
|
||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user