From b03c781b926f5bafdcd0535bdf4129f323513aba Mon Sep 17 00:00:00 2001 From: michael Date: Sat, 6 Jun 2026 15:53:10 -0700 Subject: [PATCH] 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 --- .../dangerousthings/aliro/AliroApplet.java | 168 ++++++++++++++++++ .../dangerousthings/aliro/AliroCrypto.java | 13 ++ 2 files changed, 181 insertions(+) diff --git a/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java b/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java index b92f49f..39d533d 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java +++ b/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java @@ -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); } diff --git a/applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java b/applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java index 8b65cf6..234d8bb 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java +++ b/applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java @@ -89,6 +89,19 @@ final class AliroCrypto { return ecdhPlain.generateSecret(peerPubUncomp, peerPubOff, (short) 65, out, outOff); } + /** + * Diagnostic-only: exposes the underlying HMAC-SHA-256 primitive so + * AliroApplet's INS_DIAG_HMAC handler can profile per-call cost. Not + * used by production AUTH0/AUTH1 paths (those go through hkdfExtract + * and hkdfExpand). + */ + short diagHmac( + byte[] key, short keyOff, short keyLen, + byte[] msg, short msgOff, short msgLen, + byte[] out, short outOff) { + return aliroHmac.compute(key, keyOff, keyLen, msg, msgOff, msgLen, out, outOff); + } + /** * HKDF-Extract per RFC 5869 ยง2.2 with HMAC-SHA-256: * {@code PRK = HMAC(salt, IKM)}.