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>
1049 lines
46 KiB
Java
1049 lines
46 KiB
Java
package com.dangerousthings.aliro;
|
|
|
|
import javacard.framework.APDU;
|
|
import javacard.framework.Applet;
|
|
import javacard.framework.ISO7816;
|
|
import javacard.framework.ISOException;
|
|
import javacard.framework.JCSystem;
|
|
import javacard.framework.Util;
|
|
import javacard.security.ECPrivateKey;
|
|
import javacard.security.ECPublicKey;
|
|
import javacard.security.KeyBuilder;
|
|
import javacard.security.KeyPair;
|
|
import javacard.security.MessageDigest;
|
|
import javacard.security.Signature;
|
|
|
|
/**
|
|
* Aliro v1.0 User Device applet.
|
|
*
|
|
* <p>Instantiated under {@link AliroAids#EXPEDITED} (and, once step-up is wired,
|
|
* also {@link AliroAids#STEP_UP}). A single class handles both phases; state
|
|
* is tracked internally.
|
|
*
|
|
* <p>Current scope:
|
|
* <ul>
|
|
* <li>SELECT returns a minimum-viable FCI template (spec §10.2.1).
|
|
* <li>AUTH0 (standard phase) generates a fresh credential ephemeral
|
|
* P-256 keypair and returns the public key as tag 0x86 (spec
|
|
* §8.3.3.2). Command input is not yet validated or stored.
|
|
* </ul>
|
|
*/
|
|
public class AliroApplet extends Applet {
|
|
|
|
private static final byte CLA_EXPEDITED = (byte) 0x80;
|
|
private static final byte INS_AUTH0 = (byte) 0x80;
|
|
private static final byte INS_AUTH1 = (byte) 0x81;
|
|
|
|
// 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;
|
|
private static final short OFF_READER_GROUP_SUB_ID = 81;
|
|
private static final short OFF_TRANSACTION_ID = 97;
|
|
private static final short OFF_COMMAND_PARAMETERS = 113;
|
|
private static final short OFF_AUTH_POLICY = 114;
|
|
private static final short OFF_AUTH1_CMD_PARAMS = 115;
|
|
private static final short SESSION_STATE_LEN = 116;
|
|
|
|
// Session flags (transient booleans).
|
|
private static final short FLAG_AUTH0_DONE = 0;
|
|
private static final short FLAGS_LEN = 1;
|
|
|
|
// AUTH0 command TLV tags, spec Table 8-4.
|
|
private static final byte TAG_COMMAND_PARAMETERS = (byte) 0x41;
|
|
private static final byte TAG_AUTHENTICATION_POLICY = (byte) 0x42;
|
|
private static final byte TAG_PROTOCOL_VERSION = (byte) 0x5C;
|
|
private static final byte TAG_READER_EPUBK = (byte) 0x87;
|
|
private static final byte TAG_TRANSACTION_ID = (byte) 0x4C;
|
|
private static final byte TAG_READER_IDENTIFIER = (byte) 0x4D;
|
|
|
|
// AUTH1 command TLV tags, spec Table 8-10.
|
|
private static final byte TAG_READER_SIGNATURE = (byte) 0x9E;
|
|
private static final byte TAG_READER_CERT = (byte) 0x90;
|
|
private static final short LEN_READER_SIGNATURE = 64;
|
|
|
|
private static final byte TAG_CREDENTIAL_EPUBK = (byte) 0x86;
|
|
|
|
// Bitmask of required AUTH1 tags.
|
|
private static final byte REQ_AUTH1_CMD_PARAMS = 0x01;
|
|
private static final byte REQ_AUTH1_READER_SIG = 0x02;
|
|
private static final byte REQ_ALL_AUTH1 = (byte) (REQ_AUTH1_CMD_PARAMS | REQ_AUTH1_READER_SIG);
|
|
|
|
// Expected lengths of the mandatory AUTH0 TLVs.
|
|
private static final short LEN_COMMAND_PARAMETERS = 1;
|
|
private static final short LEN_AUTHENTICATION_POLICY = 1;
|
|
private static final short LEN_PROTOCOL_VERSION = 2;
|
|
private static final short LEN_READER_EPUBK = 65;
|
|
private static final short LEN_TRANSACTION_ID = 16;
|
|
private static final short LEN_READER_IDENTIFIER = 32;
|
|
|
|
// Bitmask of required AUTH0 tags successfully parsed.
|
|
private static final byte REQ_CMD_PARAMS = 0x01;
|
|
private static final byte REQ_AUTH_POLICY = 0x02;
|
|
private static final byte REQ_PROTO_VER = 0x04;
|
|
private static final byte REQ_READER_EPUBK = 0x08;
|
|
private static final byte REQ_TXN_ID = 0x10;
|
|
private static final byte REQ_READER_ID = 0x20;
|
|
private static final byte REQ_ALL_AUTH0 = (byte) (REQ_CMD_PARAMS | REQ_AUTH_POLICY
|
|
| REQ_PROTO_VER | REQ_READER_EPUBK | REQ_TXN_ID | REQ_READER_ID);
|
|
|
|
/** ALIRO spec protocol version 0x0100 (major 1, minor 0). */
|
|
private static final short PROTOCOL_VERSION_1_0 = (short) 0x0100;
|
|
|
|
/** CSA application type, spec Table 10-4. */
|
|
private static final short APP_TYPE_CSA = (short) 0x0000;
|
|
|
|
/** Reusable ephemeral keypair — {@link KeyPair#genKeyPair()} refreshes it each AUTH0. */
|
|
private final KeyPair credentialEphemeralKeyPair;
|
|
|
|
/** Long-term Access Credential signing keypair. Curve params are set once at
|
|
* construction via {@code genKeyPair()}; the private scalar is overwritten
|
|
* per AUTH1 via {@link ECPrivateKey#setS} using bytes from {@link CredentialStore}. */
|
|
private final KeyPair credentialSigningKeyPair;
|
|
|
|
/** Holder for the provisioned reader long-term public key. Same underlying object across transactions; {@link ECPublicKey#setW} refreshes it per AUTH1. */
|
|
private final KeyPair readerPubKeyHolder;
|
|
|
|
/** Reusable ECDSA-SHA-256 verifier (reader signature) and signer (UD signature).
|
|
* Allocated lazily on first APDU — see {@link #ensureCryptoInitialized()}.
|
|
* Some real cards (notably J3R180) refuse {@code Cipher.getInstance(ALG_AES_GCM)}
|
|
* during {@code install()} even though it works at runtime; doing all
|
|
* crypto handle resolution lazily keeps install() trivial. */
|
|
private Signature ecdsaVerifier;
|
|
private Signature ecdsaSigner;
|
|
|
|
/** 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;
|
|
|
|
/** Low-level crypto primitives (ECDH, HKDF, Kdh, expedited key derivation). */
|
|
private AliroCrypto crypto;
|
|
|
|
/** Persistent (1B EEPROM) — set after {@link #ensureCryptoInitialized()} runs. */
|
|
private byte cryptoInitialized;
|
|
|
|
/** Transient buffers used during AUTH1 response construction. */
|
|
private final byte[] derivedKeys; // 160B: ExpeditedSKReader/Device, StepUpSK, BleSK, URSK
|
|
private final byte[] kdhBuf; // 32B
|
|
private final byte[] saltVolatile; // up to 200B
|
|
|
|
/** Transient session state. Cleared on deselect and on every SELECT. */
|
|
private final byte[] sessionState;
|
|
private final boolean[] sessionFlags;
|
|
|
|
/**
|
|
* Transient scratch buffer. Per-transaction layout:
|
|
* <pre>
|
|
* [ 0..128) Table 8-12 bytes (what gets signed)
|
|
* [128..200) DER-encoded reader signature for {@link Signature#verify}
|
|
* [200..265) uncompressed 65B reader pubkey for {@link ECPublicKey#setW}
|
|
* [265..329) raw 64B reader signature captured from AUTH1 input
|
|
* </pre>
|
|
*/
|
|
private final byte[] scratch;
|
|
private static final short SCRATCH_TABLE_812_OFF = 0;
|
|
private static final short SCRATCH_TABLE_812_LEN = 128;
|
|
private static final short SCRATCH_DER_SIG_OFF = 128;
|
|
private static final short SCRATCH_DER_SIG_CAPACITY = 72;
|
|
private static final short SCRATCH_READER_PUB_UNCOMP_OFF = 200;
|
|
private static final short SCRATCH_RAW_SIG_OFF = 265;
|
|
private static final short SCRATCH_LEN = 329;
|
|
|
|
private static final short DERIVED_KEYS_LEN = 160;
|
|
private static final short KDH_LEN = 32;
|
|
private static final short SALT_VOLATILE_CAPACITY = 200;
|
|
|
|
/** Offsets into {@link #derivedKeys} per spec §8.3.1.13. */
|
|
private static final short OFF_EXPEDITED_SK_READER = 0;
|
|
private static final short OFF_EXPEDITED_SK_DEVICE = 32;
|
|
private static final short OFF_STEP_UP_SK = 64;
|
|
|
|
/** Size of the 12-byte GCM IV (8B fixed prefix || 4B device_counter). */
|
|
private static final short GCM_IV_LEN = 12;
|
|
private static final short GCM_TAG_LEN = 16;
|
|
|
|
/** Aliro spec §8.3.1.13 salt_volatile constant strings. */
|
|
private static final byte[] SALT_VOLATILE_TAG = {
|
|
'V', 'o', 'l', 'a', 't', 'i', 'l', 'e', '*', '*', '*', '*'
|
|
};
|
|
|
|
// secp256r1 / NIST P-256 curve parameters per FIPS 186-4 / SEC2 §2.7.2.
|
|
// J3R180 doesn't ship a default P-256 parameter set on its EC keys, so
|
|
// calls into genKeyPair / setS / setW / Signature.init throw
|
|
// CryptoException.ILLEGAL_VALUE until we seed the curve explicitly.
|
|
private static final byte[] SECP256R1_P = {
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,0x00,0x00,0x00,0x01,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF
|
|
};
|
|
private static final byte[] SECP256R1_A = {
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,0x00,0x00,0x00,0x01,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFC
|
|
};
|
|
private static final byte[] SECP256R1_B = {
|
|
0x5A,(byte)0xC6,0x35,(byte)0xD8,(byte)0xAA,0x3A,(byte)0x93,(byte)0xE7,
|
|
(byte)0xB3,(byte)0xEB,(byte)0xBD,0x55,0x76,(byte)0x98,(byte)0x86,(byte)0xBC,
|
|
0x65,0x1D,0x06,(byte)0xB0,(byte)0xCC,0x53,(byte)0xB0,(byte)0xF6,
|
|
0x3B,(byte)0xCE,0x3C,0x3E,0x27,(byte)0xD2,0x60,0x4B
|
|
};
|
|
private static final byte[] SECP256R1_G = {
|
|
0x04,
|
|
0x6B,0x17,(byte)0xD1,(byte)0xF2,(byte)0xE1,0x2C,0x42,0x47,
|
|
(byte)0xF8,(byte)0xBC,(byte)0xE6,(byte)0xE5,0x63,(byte)0xA4,0x40,(byte)0xF2,
|
|
0x77,0x03,0x7D,(byte)0x81,0x2D,(byte)0xEB,0x33,(byte)0xA0,
|
|
(byte)0xF4,(byte)0xA1,0x39,0x45,(byte)0xD8,(byte)0x98,(byte)0xC2,(byte)0x96,
|
|
0x4F,(byte)0xE3,0x42,(byte)0xE2,(byte)0xFE,0x1A,0x7F,(byte)0x9B,
|
|
(byte)0x8E,(byte)0xE7,(byte)0xEB,0x4A,0x7C,0x0F,(byte)0x9E,0x16,
|
|
0x2B,(byte)0xCE,0x33,0x57,0x6B,0x31,0x5E,(byte)0xCE,
|
|
(byte)0xCB,(byte)0xB6,0x40,0x68,0x37,(byte)0xBF,0x51,(byte)0xF5
|
|
};
|
|
private static final byte[] SECP256R1_R = {
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,0x00,0x00,0x00,0x00,
|
|
(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,
|
|
(byte)0xBC,(byte)0xE6,(byte)0xFA,(byte)0xAD,(byte)0xA7,0x17,(byte)0x9E,(byte)0x84,
|
|
(byte)0xF3,(byte)0xB9,(byte)0xCA,(byte)0xC2,(byte)0xFC,0x63,0x25,0x51
|
|
};
|
|
/** NFC interface byte, spec §8.3.1.13. */
|
|
private static final byte INTERFACE_BYTE_NFC = (byte) 0x5E;
|
|
/** Table 8-13 usage constant for UD signature (spec §8.3.3.4.3). */
|
|
private static final byte[] UD_SIGN_USAGE = {
|
|
(byte) 0x4E, (byte) 0x88, (byte) 0x7B, (byte) 0x4C
|
|
};
|
|
/** Proprietary 0xA5 TLV contributed by the NFC SELECT FCI (see {@link #sendExpeditedFci}). */
|
|
private static final byte[] PROPRIETARY_A5_TLV = {
|
|
(byte) 0xA5, (byte) 0x08,
|
|
(byte) 0x80, (byte) 0x02, (byte) 0x00, (byte) 0x00,
|
|
(byte) 0x5C, (byte) 0x02, (byte) 0x01, (byte) 0x00
|
|
};
|
|
|
|
/** Persistent — set to 1 once {@link #credentialSigningKeyPair} has had
|
|
* its P-256 curve parameters seeded by {@code genKeyPair()}. We defer
|
|
* this from the constructor to first AUTH1 because EC keypair generation
|
|
* during applet install fails on some real cards (notably J3R180 returned
|
|
* 0x6F00 for INSTALL[for install and make selectable]). genKeyPair runs
|
|
* exactly once over the lifetime of the applet. */
|
|
private byte signingKeyParamsInitialized;
|
|
|
|
private AliroApplet() {
|
|
// Keep install() trivial: only the JC-required transient buffers and
|
|
// stateless persistent KeyPair holders. Anything that calls
|
|
// Signature.getInstance / Cipher.getInstance / MessageDigest.getInstance /
|
|
// KeyBuilder.buildKey / new AliroCrypto() runs in ensureCryptoInitialized()
|
|
// on the first APDU. This avoids 0x6F00 from cards that resolve
|
|
// crypto algorithms lazily and refuse them during install().
|
|
credentialEphemeralKeyPair = new KeyPair(KeyPair.ALG_EC_FP, KeyBuilder.LENGTH_EC_FP_256);
|
|
credentialSigningKeyPair = new KeyPair(KeyPair.ALG_EC_FP, KeyBuilder.LENGTH_EC_FP_256);
|
|
readerPubKeyHolder = new KeyPair(KeyPair.ALG_EC_FP, KeyBuilder.LENGTH_EC_FP_256);
|
|
|
|
sessionState = JCSystem.makeTransientByteArray(SESSION_STATE_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
sessionFlags = JCSystem.makeTransientBooleanArray(FLAGS_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
scratch = JCSystem.makeTransientByteArray(SCRATCH_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
derivedKeys = JCSystem.makeTransientByteArray(DERIVED_KEYS_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
kdhBuf = JCSystem.makeTransientByteArray(KDH_LEN, JCSystem.CLEAR_ON_DESELECT);
|
|
saltVolatile = JCSystem.makeTransientByteArray(SALT_VOLATILE_CAPACITY, JCSystem.CLEAR_ON_DESELECT);
|
|
}
|
|
|
|
/** First-APDU lazy initialization for crypto handles. Runs exactly once
|
|
* over the applet's lifetime (gated by persistent {@link #cryptoInitialized}).
|
|
*
|
|
* <p>Everything here has proven loadable at {@code install()} time on
|
|
* J3R180 except the AES-GCM {@link Cipher#getInstance} call — J3R180
|
|
* advertises JC 3.0.5 but does not implement {@code ALG_AES_GCM}. We
|
|
* substitute {@link AliroGcm}, which builds GCM in userland on top of
|
|
* {@code ALG_AES_BLOCK_128_ECB_NOPAD}. The try/catches around
|
|
* {@link AliroGcm} construction throw 0x6FA8 so a future regression
|
|
* (e.g. AES-ECB-NOPAD or AES-256-key allocation disappearing from a new
|
|
* ROM revision) surfaces with a specific, greppable status word. */
|
|
private void ensureCryptoInitialized() {
|
|
if (cryptoInitialized != 0) return;
|
|
ecdsaVerifier = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
|
|
ecdsaSigner = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
|
|
sha1 = MessageDigest.getInstance(MessageDigest.ALG_SHA, false);
|
|
try {
|
|
gcm = new AliroGcm();
|
|
} catch (ISOException e) { throw e; // preserve inner diagnostic SW
|
|
} catch (Throwable t) { ISOException.throwIt((short) 0x6FA8); }
|
|
try {
|
|
crypto = new AliroCrypto();
|
|
} catch (ISOException e) { throw e; // preserve inner diagnostic SW (0x6FC1-C5)
|
|
} catch (Throwable t) { ISOException.throwIt((short) 0x6FA6); }
|
|
// Seed P-256 curve params on every keypair before any genKeyPair / setS /
|
|
// setW / Signature.init runs. J3R180 ships ECC keys with no default
|
|
// domain parameters; without this, ILLEGAL_VALUE comes back at first
|
|
// crypto use (we hit 0x6FE1 from genKeyPair before adding this).
|
|
try { seedSecp256r1(credentialEphemeralKeyPair); }
|
|
catch (Throwable t) { ISOException.throwIt((short) 0x6FB0); }
|
|
try { seedSecp256r1(credentialSigningKeyPair); }
|
|
catch (Throwable t) { ISOException.throwIt((short) 0x6FB1); }
|
|
try { seedSecp256r1(readerPubKeyHolder); }
|
|
catch (Throwable t) { ISOException.throwIt((short) 0x6FB2); }
|
|
cryptoInitialized = 1;
|
|
}
|
|
|
|
/** Loads the secp256r1 / NIST P-256 curve parameters into both halves of
|
|
* {@code kp}. Must run before any {@code genKeyPair}, {@code setS},
|
|
* {@code setW}, or {@code Signature.init} on cards (like J3R180) that
|
|
* don't preset domain parameters on freshly-allocated EC keys. */
|
|
private static void seedSecp256r1(KeyPair kp) {
|
|
javacard.security.ECPublicKey pub = (javacard.security.ECPublicKey) kp.getPublic();
|
|
javacard.security.ECPrivateKey priv = (javacard.security.ECPrivateKey) kp.getPrivate();
|
|
pub.setFieldFP(SECP256R1_P, (short) 0, (short) SECP256R1_P.length);
|
|
pub.setA(SECP256R1_A, (short) 0, (short) SECP256R1_A.length);
|
|
pub.setB(SECP256R1_B, (short) 0, (short) SECP256R1_B.length);
|
|
pub.setG(SECP256R1_G, (short) 0, (short) SECP256R1_G.length);
|
|
pub.setR(SECP256R1_R, (short) 0, (short) SECP256R1_R.length);
|
|
pub.setK((short) 1);
|
|
priv.setFieldFP(SECP256R1_P, (short) 0, (short) SECP256R1_P.length);
|
|
priv.setA(SECP256R1_A, (short) 0, (short) SECP256R1_A.length);
|
|
priv.setB(SECP256R1_B, (short) 0, (short) SECP256R1_B.length);
|
|
priv.setG(SECP256R1_G, (short) 0, (short) SECP256R1_G.length);
|
|
priv.setR(SECP256R1_R, (short) 0, (short) SECP256R1_R.length);
|
|
priv.setK((short) 1);
|
|
}
|
|
|
|
@Override
|
|
public boolean select() {
|
|
resetSession();
|
|
return true;
|
|
}
|
|
|
|
private void resetSession() {
|
|
Util.arrayFillNonAtomic(sessionState, (short) 0, SESSION_STATE_LEN, (byte) 0);
|
|
sessionFlags[FLAG_AUTH0_DONE] = false;
|
|
// A fresh expedited transaction invalidates any prior step-up arming.
|
|
SessionContext.disarmStepUp();
|
|
}
|
|
|
|
public static void install(byte[] bArray, short bOffset, byte bLength) {
|
|
AliroApplet applet = new AliroApplet();
|
|
if (bArray == null || bLength == 0) {
|
|
applet.register();
|
|
} else {
|
|
applet.register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void process(APDU apdu) {
|
|
if (selectingApplet()) {
|
|
sendExpeditedFci(apdu);
|
|
return;
|
|
}
|
|
|
|
// First non-SELECT APDU: resolve all crypto handles. One-time cost.
|
|
ensureCryptoInitialized();
|
|
|
|
byte[] buf = apdu.getBuffer();
|
|
byte cla = buf[ISO7816.OFFSET_CLA];
|
|
byte ins = buf[ISO7816.OFFSET_INS];
|
|
|
|
if (cla != CLA_EXPEDITED) {
|
|
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
|
|
}
|
|
|
|
switch (ins) {
|
|
case INS_AUTH0:
|
|
processAuth0(apdu);
|
|
return;
|
|
case INS_AUTH1:
|
|
processAuth1(apdu);
|
|
return;
|
|
default:
|
|
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles AUTH1 (spec §8.3.3.4). Verifies the reader signature over
|
|
* Table 8-12, derives the Aliro session keys (§8.3.1.13), signs Table
|
|
* 8-13 with credential_PrivK, builds the Table 8-11 plaintext
|
|
* (key_slot or credential_PubK per command_parameters bit 0, UD sig,
|
|
* signaling_bitmap), and AES-256-GCM encrypts it with ExpeditedSKDevice.
|
|
* Response = encrypted_payload || authentication_tag.
|
|
*/
|
|
private void processAuth1(APDU apdu) {
|
|
if (!sessionFlags[FLAG_AUTH0_DONE]) {
|
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
}
|
|
// One-shot: a single AUTH1 consumes the AUTH0 state. Clearing here —
|
|
// before any crypto — means even a mid-pipeline exception leaves the
|
|
// state consumed, so a replay cannot reuse the session keys (and
|
|
// critically cannot reuse device_counter=1, which would produce IV
|
|
// collisions in AES-GCM). A fresh AUTH0 is required to try again.
|
|
sessionFlags[FLAG_AUTH0_DONE] = false;
|
|
|
|
CredentialStore store = CredentialStore.get();
|
|
if (!store.hasReaderPubKey()
|
|
|| !store.hasCredentialPrivKey()
|
|
|| !store.hasCredentialPubKey()) {
|
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
}
|
|
|
|
short lc = apdu.setIncomingAndReceive();
|
|
byte[] buf = apdu.getBuffer();
|
|
short dataOff = apdu.getOffsetCdata();
|
|
|
|
validateAuth1Data(buf, dataOff, lc);
|
|
verifyReaderSignature(store);
|
|
|
|
deriveSessionKeys(store);
|
|
loadCredentialSigningKey(store);
|
|
|
|
short udSigLen = signTable813AndGetRawSig(
|
|
scratch, SCRATCH_TABLE_812_OFF,
|
|
scratch, SCRATCH_DER_SIG_OFF,
|
|
scratch, SCRATCH_RAW_SIG_OFF);
|
|
|
|
short ptLen = buildTable811Plaintext(
|
|
sessionState[OFF_AUTH1_CMD_PARAMS], store,
|
|
scratch, SCRATCH_RAW_SIG_OFF, udSigLen,
|
|
buf, (short) 0);
|
|
|
|
short respLen = encryptResponseGcm(
|
|
buf, (short) 0, ptLen,
|
|
buf, (short) 0);
|
|
|
|
// AUTH1 succeeded; hand off StepUpSK (derived_keys[64..96]) to the
|
|
// cross-applet context so StepUpApplet can pick up the step-up flow.
|
|
SessionContext.armStepUp(derivedKeys, OFF_STEP_UP_SK);
|
|
|
|
apdu.setOutgoingAndSend((short) 0, respLen);
|
|
}
|
|
|
|
/**
|
|
* Reconstructs Table 8-12, loads the provisioned reader public key, and
|
|
* verifies the captured raw reader signature against the table. Throws
|
|
* SW_WRONG_DATA on any failure.
|
|
*/
|
|
private void verifyReaderSignature(CredentialStore store) {
|
|
short tblLen = buildTable812(scratch, SCRATCH_TABLE_812_OFF);
|
|
|
|
scratch[SCRATCH_READER_PUB_UNCOMP_OFF] = (byte) 0x04;
|
|
store.copyReaderPubKey(scratch, (short) (SCRATCH_READER_PUB_UNCOMP_OFF + 1));
|
|
ECPublicKey readerPub = (ECPublicKey) readerPubKeyHolder.getPublic();
|
|
readerPub.setW(scratch, SCRATCH_READER_PUB_UNCOMP_OFF, (short) 65);
|
|
|
|
short derLen = rawSigToDer(
|
|
scratch, SCRATCH_RAW_SIG_OFF,
|
|
scratch, SCRATCH_DER_SIG_OFF);
|
|
|
|
ecdsaVerifier.init(readerPub, Signature.MODE_VERIFY);
|
|
boolean ok = ecdsaVerifier.verify(
|
|
scratch, SCRATCH_TABLE_812_OFF, tblLen,
|
|
scratch, SCRATCH_DER_SIG_OFF, derLen);
|
|
if (!ok) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Derives Kdh and {@code derived_keys_volatile} per spec §8.3.1.13 for
|
|
* the expedited-standard flow. The 32-byte {@code ExpeditedSKDevice}
|
|
* lands at {@code derivedKeys[OFF_EXPEDITED_SK_DEVICE..+32)}, ready to
|
|
* be passed into {@link AliroGcm#encrypt} as the AES key.
|
|
*/
|
|
private void deriveSessionKeys(CredentialStore store) {
|
|
short saltLen = buildSaltVolatile(store, saltVolatile, (short) 0);
|
|
short infoLen = buildInfo(scratch, SCRATCH_READER_PUB_UNCOMP_OFF);
|
|
|
|
crypto.deriveKdh(
|
|
(ECPrivateKey) credentialEphemeralKeyPair.getPrivate(),
|
|
sessionState, OFF_READER_EPUBK,
|
|
sessionState, OFF_TRANSACTION_ID, (short) 16,
|
|
kdhBuf, (short) 0);
|
|
|
|
crypto.deriveExpeditedStandardKeys(
|
|
kdhBuf, (short) 0,
|
|
saltVolatile, (short) 0, saltLen,
|
|
scratch, SCRATCH_READER_PUB_UNCOMP_OFF, infoLen,
|
|
derivedKeys, (short) 0);
|
|
}
|
|
|
|
/**
|
|
* Builds salt_volatile per spec §8.3.1.13 into {@code out} starting at
|
|
* {@code outOff}. Returns the total length written.
|
|
*
|
|
* <pre>
|
|
* x(reader_group_id_key) 32B (from CredentialStore)
|
|
* "Volatile****" 12B
|
|
* reader_identifier 32B (group_id || group_sub_id from AUTH0)
|
|
* interface_byte 1B (0x5E for NFC)
|
|
* 0x5C 0x02 2B (literal per spec)
|
|
* protocol_version 2B (0x01 0x00)
|
|
* x(reader_ephem_pub_key) 32B
|
|
* transaction_identifier 16B
|
|
* flag 2B (auth0 cmd_params || authentication_policy)
|
|
* proprietary_A5_TLV 10B (from SELECT FCI)
|
|
* x(access_credential_pub_key) 32B (long-term; from CredentialStore)
|
|
* </pre>
|
|
*/
|
|
private short buildSaltVolatile(CredentialStore store, byte[] out, short outOff) {
|
|
short p = outOff;
|
|
|
|
// x(reader_group_identifier_key) — first 32 bytes of the 64B reader_PubK
|
|
store.copyReaderPubKeyX(out, p);
|
|
p += 32;
|
|
|
|
Util.arrayCopyNonAtomic(SALT_VOLATILE_TAG, (short) 0, out, p, (short) SALT_VOLATILE_TAG.length);
|
|
p += SALT_VOLATILE_TAG.length;
|
|
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_READER_GROUP_ID, out, p, (short) 16);
|
|
p += 16;
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_READER_GROUP_SUB_ID, out, p, (short) 16);
|
|
p += 16;
|
|
|
|
out[p++] = INTERFACE_BYTE_NFC;
|
|
out[p++] = (byte) 0x5C;
|
|
out[p++] = (byte) 0x02;
|
|
out[p++] = (byte) ((PROTOCOL_VERSION_1_0 >> 8) & 0xFF);
|
|
out[p++] = (byte) (PROTOCOL_VERSION_1_0 & 0xFF);
|
|
|
|
// reader_ephem_pub_key.x — sessionState holds full uncompressed (0x04 || x || y) at OFF_READER_EPUBK
|
|
Util.arrayCopyNonAtomic(sessionState, (short) (OFF_READER_EPUBK + 1), out, p, (short) 32);
|
|
p += 32;
|
|
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_TRANSACTION_ID, out, p, (short) 16);
|
|
p += 16;
|
|
|
|
// flag = command_parameters || authentication_policy (both 1 byte, from AUTH0)
|
|
out[p++] = sessionState[OFF_COMMAND_PARAMETERS];
|
|
out[p++] = sessionState[OFF_AUTH_POLICY];
|
|
|
|
Util.arrayCopyNonAtomic(PROPRIETARY_A5_TLV, (short) 0, out, p, (short) PROPRIETARY_A5_TLV.length);
|
|
p += PROPRIETARY_A5_TLV.length;
|
|
|
|
// x(access_credential_public_key) — first 32 bytes of the 64B stored credential_PubK
|
|
store.copyCredentialPubKeyX(out, p);
|
|
p += 32;
|
|
|
|
return (short) (p - outOff);
|
|
}
|
|
|
|
/**
|
|
* Builds {@code info} per §8.3.1.13: x(credential_ephemeral_public_key).
|
|
* In v1 we emit no AUTH0 vendor extensions and accept none, so info
|
|
* degenerates to the 32-byte x-coordinate of the ephemeral pubkey.
|
|
*/
|
|
private short buildInfo(byte[] out, short outOff) {
|
|
ECPublicKey ephemPub = (ECPublicKey) credentialEphemeralKeyPair.getPublic();
|
|
// Temp: first 65B of scratch — free at this point (phase 1 is done
|
|
// and phase 3 hasn't started).
|
|
short n = ephemPub.getW(scratch, (short) 0);
|
|
if (n != 65 || scratch[0] != 0x04) {
|
|
ISOException.throwIt(ISO7816.SW_UNKNOWN); // should be unreachable on P-256
|
|
}
|
|
Util.arrayCopyNonAtomic(scratch, (short) 1, out, outOff, (short) 32);
|
|
return (short) 32;
|
|
}
|
|
|
|
/** Overwrites {@link #credentialSigningKeyPair}'s private scalar with credential_PrivK.
|
|
* On first call, also seeds the keypair's P-256 curve parameters via genKeyPair()
|
|
* (the random scalar it produces is immediately discarded by setS below). */
|
|
private void loadCredentialSigningKey(CredentialStore store) {
|
|
if (signingKeyParamsInitialized == 0) {
|
|
credentialSigningKeyPair.genKeyPair();
|
|
signingKeyParamsInitialized = 1;
|
|
}
|
|
short off = SCRATCH_DER_SIG_OFF; // borrow scratch region; reader sig is no longer needed here
|
|
store.copyCredentialPrivKey(scratch, off);
|
|
((ECPrivateKey) credentialSigningKeyPair.getPrivate()).setS(
|
|
scratch, off, CredentialStore.CRED_PRIV_KEY_LEN);
|
|
}
|
|
|
|
/**
|
|
* Builds Table 8-13 into {@code t813Buf[t813Off..]}, signs it with
|
|
* {@code credential_PrivK}, and writes the raw 64-byte r||s signature to
|
|
* {@code rawSigBuf[rawSigOff..]}. Returns the raw sig length (always 64).
|
|
* The DER work buffer is only used internally.
|
|
*/
|
|
private short signTable813AndGetRawSig(
|
|
byte[] t813Buf, short t813Off,
|
|
byte[] derSigBuf, short derSigOff,
|
|
byte[] rawSigBuf, short rawSigOff) {
|
|
short t813Len = buildTable813(t813Buf, t813Off);
|
|
|
|
ecdsaSigner.init(credentialSigningKeyPair.getPrivate(), Signature.MODE_SIGN);
|
|
short derLen = ecdsaSigner.sign(t813Buf, t813Off, t813Len, derSigBuf, derSigOff);
|
|
derSigToRaw(derSigBuf, derSigOff, derLen, rawSigBuf, rawSigOff);
|
|
return (short) 64;
|
|
}
|
|
|
|
/**
|
|
* Builds Table 8-13 (UD-side authentication data, spec §8.3.3.4.3) into
|
|
* {@code dst[dstOff..]}. Identical to Table 8-12 except the 4-byte
|
|
* usage constant (0x4E887B4C instead of 0x415D9569).
|
|
*/
|
|
private short buildTable813(byte[] dst, short dstOff) {
|
|
short p = dstOff;
|
|
|
|
dst[p++] = (byte) 0x4D;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_READER_GROUP_ID, dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
ECPublicKey credPub = (ECPublicKey) credentialEphemeralKeyPair.getPublic();
|
|
credPub.getW(scratch, SCRATCH_READER_PUB_UNCOMP_OFF);
|
|
dst[p++] = (byte) 0x86;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(scratch, (short) (SCRATCH_READER_PUB_UNCOMP_OFF + 1), dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
dst[p++] = (byte) 0x87;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(sessionState, (short) (OFF_READER_EPUBK + 1), dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
dst[p++] = (byte) 0x4C;
|
|
dst[p++] = (byte) 0x10;
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_TRANSACTION_ID, dst, p, (short) 16);
|
|
p += 16;
|
|
|
|
dst[p++] = (byte) 0x93;
|
|
dst[p++] = (byte) 0x04;
|
|
Util.arrayCopyNonAtomic(UD_SIGN_USAGE, (short) 0, dst, p, (short) UD_SIGN_USAGE.length);
|
|
p += UD_SIGN_USAGE.length;
|
|
|
|
return (short) (p - dstOff);
|
|
}
|
|
|
|
/**
|
|
* Converts a DER-encoded ECDSA P-256 signature into raw r||s (32+32)
|
|
* form. Strips the leading 0x00 that DER inserts when the high bit of
|
|
* r or s is set, and left-zero-pads shorter integers to 32 bytes.
|
|
*/
|
|
private static void derSigToRaw(
|
|
byte[] der, short derOff, short derLen,
|
|
byte[] raw, short rawOff) {
|
|
Util.arrayFillNonAtomic(raw, rawOff, (short) 64, (byte) 0);
|
|
short p = (short) (derOff + 2); // skip SEQUENCE tag + short-form length
|
|
|
|
if (der[p++] != 0x02) ISOException.throwIt(ISO7816.SW_UNKNOWN);
|
|
short rDerLen = (short) (der[p++] & 0xFF);
|
|
short rValStart = p;
|
|
short rValLen = rDerLen;
|
|
if (rValLen > 32 && der[rValStart] == 0x00) { rValStart++; rValLen--; }
|
|
Util.arrayCopyNonAtomic(der, rValStart, raw, (short) (rawOff + 32 - rValLen), rValLen);
|
|
p = (short) (p + rDerLen);
|
|
|
|
if (der[p++] != 0x02) ISOException.throwIt(ISO7816.SW_UNKNOWN);
|
|
short sDerLen = (short) (der[p++] & 0xFF);
|
|
short sValStart = p;
|
|
short sValLen = sDerLen;
|
|
if (sValLen > 32 && der[sValStart] == 0x00) { sValStart++; sValLen--; }
|
|
Util.arrayCopyNonAtomic(der, sValStart, raw, (short) (rawOff + 64 - sValLen), sValLen);
|
|
}
|
|
|
|
/**
|
|
* Builds Table 8-11 plaintext into {@code out[outOff..]}. Emits either
|
|
* {@code 0x4E key_slot} (first 8 bytes of SHA-1(uncompressed
|
|
* credential_PubK), per §8.3.3.4.2 + ref [12]) when command_parameters
|
|
* bit 0 = 0, or {@code 0x5A credential_PubK} (full uncompressed 65B)
|
|
* when bit 0 = 1. Then {@code 0x9E UD_signature} and a 2-byte
|
|
* all-zero {@code 0x5E signaling_bitmap}.
|
|
*/
|
|
private short buildTable811Plaintext(
|
|
byte auth1CmdParams, CredentialStore store,
|
|
byte[] rawSig, short rawSigOff, short rawSigLen,
|
|
byte[] out, short outOff) {
|
|
short p = outOff;
|
|
|
|
if ((auth1CmdParams & 0x01) == 0) {
|
|
// 0x4E 0x08 [key_slot] — first 8 bytes of SHA-1(uncompressed credential_PubK)
|
|
out[p++] = (byte) 0x4E;
|
|
out[p++] = (byte) 0x08;
|
|
p = appendKeySlot(store, out, p);
|
|
} else {
|
|
// 0x5A 0x41 [0x04 || x || y] — 65-byte uncompressed credential long-term pubkey
|
|
out[p++] = (byte) 0x5A;
|
|
out[p++] = (byte) 0x41;
|
|
out[p++] = (byte) 0x04;
|
|
store.copyCredentialPubKey(out, p);
|
|
p += CredentialStore.CRED_PUBK_LEN;
|
|
}
|
|
|
|
// 0x9E 0x40 [raw r||s UD signature]
|
|
out[p++] = (byte) 0x9E;
|
|
out[p++] = (byte) 0x40;
|
|
Util.arrayCopyNonAtomic(rawSig, rawSigOff, out, p, rawSigLen);
|
|
p += rawSigLen;
|
|
|
|
// 0x5E 0x02 [signaling_bitmap] — 16-bit big-endian. Bit 0: Access
|
|
// Document retrievable. Bit 2: retrieval requires step-up AID SELECT
|
|
// (applicable on NFC). Other bits unused in v1 (no mailbox/notify).
|
|
short bitmap = 0;
|
|
if (store.hasAccessDocument()) {
|
|
bitmap |= 0x0001; // bit 0
|
|
bitmap |= 0x0004; // bit 2 — NFC requires step-up AID to fetch AD
|
|
}
|
|
out[p++] = (byte) 0x5E;
|
|
out[p++] = (byte) 0x02;
|
|
out[p++] = (byte) ((bitmap >> 8) & 0xFF);
|
|
out[p++] = (byte) (bitmap & 0xFF);
|
|
|
|
return (short) (p - outOff);
|
|
}
|
|
|
|
/**
|
|
* Computes key_slot = first 8 bytes of SHA-1(uncompressed credential_PubK)
|
|
* and writes it to {@code out[p..p+8)}. Returns {@code p + 8}.
|
|
*
|
|
* <p>Uses scratch for both the 65-byte SHA-1 input and the 20-byte SHA-1
|
|
* output — the expensive phases (reader-sig verify, key derivation) have
|
|
* already finished by the time this runs, so scratch can be reused.
|
|
*/
|
|
private short appendKeySlot(CredentialStore store, byte[] out, short p) {
|
|
// Build uncompressed credential_PubK at scratch[0..65).
|
|
scratch[0] = (byte) 0x04;
|
|
store.copyCredentialPubKey(scratch, (short) 1);
|
|
// Hash into scratch[65..85).
|
|
sha1.reset();
|
|
sha1.doFinal(scratch, (short) 0, (short) 65, scratch, (short) 65);
|
|
Util.arrayCopyNonAtomic(scratch, (short) 65, out, p, (short) 8);
|
|
return (short) (p + 8);
|
|
}
|
|
|
|
/**
|
|
* AES-256-GCM encrypts {@code plaintext} with {@code ExpeditedSKDevice}
|
|
* (from {@link #derivedKeys}) and IV =
|
|
* {@code 0x0000000000000001 || expedited_device_counter (4B BE)},
|
|
* where the device_counter starts at 1 (spec §8.3.1.13) and is only
|
|
* incremented once a new phase begins. For the first AUTH1 response the
|
|
* counter is 1. Writes {@code ciphertext || tag} into {@code out[outOff..]}
|
|
* and returns {@code plaintextLen + 16}.
|
|
*
|
|
* <p>Uses {@link AliroGcm} — a userland GCM built on AES-ECB — because
|
|
* the target card (J3R180) does not expose {@code AEADCipher.ALG_AES_GCM}.
|
|
*/
|
|
private short encryptResponseGcm(
|
|
byte[] plaintext, short ptOff, short ptLen,
|
|
byte[] out, short outOff) {
|
|
// Build 12-byte IV in a small scratch region.
|
|
short ivOff = SCRATCH_RAW_SIG_OFF; // borrow scratch — sig already copied into plaintext
|
|
Util.arrayFillNonAtomic(scratch, ivOff, GCM_IV_LEN, (byte) 0);
|
|
scratch[(short) (ivOff + 7)] = (byte) 0x01; // fixed prefix low byte
|
|
// device_counter big-endian in the last 4 bytes; first AUTH1 = 1
|
|
scratch[(short) (ivOff + 11)] = (byte) 0x01;
|
|
|
|
return gcm.encrypt(
|
|
derivedKeys, OFF_EXPEDITED_SK_DEVICE,
|
|
scratch, ivOff,
|
|
plaintext, ptOff, ptLen,
|
|
out, outOff);
|
|
}
|
|
|
|
/**
|
|
* Builds Table 8-12 (the bytes the reader signed) into the given buffer.
|
|
* Requires AUTH0 state to be captured and the credential ephemeral
|
|
* keypair to be fresh (both true by the time we get here).
|
|
* Returns the total number of bytes written.
|
|
*/
|
|
private short buildTable812(byte[] dst, short dstOff) {
|
|
short p = dstOff;
|
|
|
|
// reader_identifier (tag 0x4D, 32 bytes: group_id || group_sub_id)
|
|
dst[p++] = (byte) 0x4D;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_READER_GROUP_ID, dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
// credential_ePubK.x (tag 0x86, 32 bytes)
|
|
// Write full uncompressed point to a later scratch region, copy only the x part.
|
|
ECPublicKey credPub = (ECPublicKey) credentialEphemeralKeyPair.getPublic();
|
|
short credPubOff = SCRATCH_READER_PUB_UNCOMP_OFF; // reuse — we haven't written reader pub yet
|
|
credPub.getW(scratch, credPubOff);
|
|
dst[p++] = (byte) 0x86;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(scratch, (short) (credPubOff + 1), dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
// reader_ePubK.x (tag 0x87, 32 bytes) — sessionState stores full uncompressed at OFF_READER_EPUBK
|
|
dst[p++] = (byte) 0x87;
|
|
dst[p++] = (byte) 0x20;
|
|
Util.arrayCopyNonAtomic(sessionState, (short) (OFF_READER_EPUBK + 1), dst, p, (short) 32);
|
|
p += 32;
|
|
|
|
// transaction_identifier (tag 0x4C, 16 bytes)
|
|
dst[p++] = (byte) 0x4C;
|
|
dst[p++] = (byte) 0x10;
|
|
Util.arrayCopyNonAtomic(sessionState, OFF_TRANSACTION_ID, dst, p, (short) 16);
|
|
p += 16;
|
|
|
|
// usage (tag 0x93, 4 bytes: 0x41 5D 95 69)
|
|
dst[p++] = (byte) 0x93;
|
|
dst[p++] = (byte) 0x04;
|
|
dst[p++] = (byte) 0x41;
|
|
dst[p++] = (byte) 0x5D;
|
|
dst[p++] = (byte) 0x95;
|
|
dst[p++] = (byte) 0x69;
|
|
|
|
return (short) (p - dstOff);
|
|
}
|
|
|
|
/**
|
|
* Converts a 64-byte raw ECDSA (r||s) signature into ASN.1 DER:
|
|
* {@code SEQUENCE { INTEGER r, INTEGER s }}. Both r and s are written
|
|
* without leading-zero stripping, and a 0x00 is prepended if the high
|
|
* bit of the first byte is set (to keep the INTEGER positive).
|
|
* Returns the number of DER bytes written.
|
|
*/
|
|
private static short rawSigToDer(byte[] raw, short rawOff, byte[] out, short outOff) {
|
|
boolean rPad = (raw[rawOff] & 0x80) != 0;
|
|
boolean sPad = (raw[(short) (rawOff + 32)] & 0x80) != 0;
|
|
short rLen = rPad ? (short) 33 : (short) 32;
|
|
short sLen = sPad ? (short) 33 : (short) 32;
|
|
short contentLen = (short) (2 + rLen + 2 + sLen);
|
|
|
|
short p = outOff;
|
|
out[p++] = (byte) 0x30;
|
|
out[p++] = (byte) contentLen;
|
|
|
|
out[p++] = (byte) 0x02;
|
|
out[p++] = (byte) rLen;
|
|
if (rPad) out[p++] = (byte) 0x00;
|
|
Util.arrayCopyNonAtomic(raw, rawOff, out, p, (short) 32);
|
|
p += 32;
|
|
|
|
out[p++] = (byte) 0x02;
|
|
out[p++] = (byte) sLen;
|
|
if (sPad) out[p++] = (byte) 0x00;
|
|
Util.arrayCopyNonAtomic(raw, (short) (rawOff + 32), out, p, (short) 32);
|
|
p += 32;
|
|
|
|
return (short) (p - outOff);
|
|
}
|
|
|
|
/**
|
|
* Walks AUTH1 command data, verifies the two mandatory tags (0x41 and
|
|
* 0x9E) with their expected lengths, rejects tag 0x90 (reader_Cert,
|
|
* unsupported in v1), and silently accepts unknown tags per spec
|
|
* §1.4.3.
|
|
*/
|
|
private void validateAuth1Data(byte[] buf, short dataOff, short dataLen) {
|
|
if (dataLen <= 0) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
|
|
byte seen = 0;
|
|
short i = dataOff;
|
|
short end = (short) (dataOff + dataLen);
|
|
|
|
while (i < end) {
|
|
if ((short) (i + 2) > end) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
byte tag = buf[i];
|
|
short len = (short) (buf[(short) (i + 1)] & 0xFF);
|
|
short valueOff = (short) (i + 2);
|
|
if ((short) (valueOff + len) > end) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
|
|
switch (tag) {
|
|
case TAG_COMMAND_PARAMETERS:
|
|
requireLen(len, LEN_COMMAND_PARAMETERS);
|
|
sessionState[OFF_AUTH1_CMD_PARAMS] = buf[valueOff];
|
|
seen |= REQ_AUTH1_CMD_PARAMS;
|
|
break;
|
|
case TAG_READER_SIGNATURE:
|
|
requireLen(len, LEN_READER_SIGNATURE);
|
|
// Capture the sig for later use by verifyReaderSignature().
|
|
Util.arrayCopyNonAtomic(buf, valueOff, scratch, SCRATCH_RAW_SIG_OFF, len);
|
|
seen |= REQ_AUTH1_READER_SIG;
|
|
break;
|
|
case TAG_READER_CERT:
|
|
// v1 scope: certificates are out of scope. Reject explicitly.
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
break;
|
|
default:
|
|
// Unknown TLV — spec §1.4.3 says accept silently.
|
|
break;
|
|
}
|
|
i = (short) (valueOff + len);
|
|
}
|
|
|
|
if ((byte) (seen & REQ_ALL_AUTH1) != REQ_ALL_AUTH1) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds and sends the FCI template for a successful SELECT of the
|
|
* expedited-phase AID, per spec §10.2.1.
|
|
*
|
|
* <pre>
|
|
* 6F L1
|
|
* 84 09 [expedited AID]
|
|
* A5 L2
|
|
* 80 02 00 00 ; type = CSA application
|
|
* 5C 02 01 00 ; expedited protocol versions = {0x0100}
|
|
* </pre>
|
|
*/
|
|
private void sendExpeditedFci(APDU apdu) {
|
|
byte[] buf = apdu.getBuffer();
|
|
short off = 0;
|
|
|
|
buf[off++] = 0x6F;
|
|
short outerLenPos = off++;
|
|
|
|
buf[off++] = (byte) 0x84;
|
|
buf[off++] = (byte) AliroAids.EXPEDITED.length;
|
|
off = Util.arrayCopyNonAtomic(AliroAids.EXPEDITED, (short) 0,
|
|
buf, off, (short) AliroAids.EXPEDITED.length);
|
|
|
|
buf[off++] = (byte) 0xA5;
|
|
short propLenPos = off++;
|
|
short propStart = off;
|
|
|
|
buf[off++] = (byte) 0x80;
|
|
buf[off++] = (byte) 0x02;
|
|
buf[off++] = (byte) ((APP_TYPE_CSA >> 8) & 0xFF);
|
|
buf[off++] = (byte) (APP_TYPE_CSA & 0xFF);
|
|
|
|
buf[off++] = (byte) 0x5C;
|
|
buf[off++] = (byte) 0x02;
|
|
buf[off++] = (byte) ((PROTOCOL_VERSION_1_0 >> 8) & 0xFF);
|
|
buf[off++] = (byte) (PROTOCOL_VERSION_1_0 & 0xFF);
|
|
|
|
buf[propLenPos] = (byte) (off - propStart);
|
|
buf[outerLenPos] = (byte) (off - 2);
|
|
|
|
apdu.setOutgoingAndSend((short) 0, off);
|
|
}
|
|
|
|
/**
|
|
* Handles AUTH0 (spec §8.3.3.2). Validates mandatory input TLVs,
|
|
* captures session state for AUTH1 (reader ephemeral pubkey,
|
|
* reader_group_id, reader_group_sub_id, transaction_identifier,
|
|
* command_parameters), generates a fresh credential ephemeral P-256
|
|
* keypair, and returns the ephemeral public key tagged 0x86.
|
|
*/
|
|
private void processAuth0(APDU apdu) {
|
|
// A new AUTH0 starts a new transaction; disarm any prior step-up
|
|
// state so a mid-pipeline failure later cannot leave stale StepUpSK
|
|
// reachable by SELECT STEP_UP.
|
|
SessionContext.disarmStepUp();
|
|
|
|
short lc = apdu.setIncomingAndReceive();
|
|
byte[] buf = apdu.getBuffer();
|
|
short dataOff = apdu.getOffsetCdata();
|
|
|
|
validateAuth0Data(buf, dataOff, lc);
|
|
// Diagnostic ladder for J3R180: EC keypair generation has historically
|
|
// been brittle on this card (see signingKeyParamsInitialized comment).
|
|
// Surface the CryptoException reason in the low nibble of the SW so we
|
|
// can tell ILLEGAL_VALUE/UNINITIALIZED_KEY/NO_SUCH_ALGORITHM apart on
|
|
// the bench. 0x6FEF = non-CryptoException Throwable.
|
|
try {
|
|
credentialEphemeralKeyPair.genKeyPair();
|
|
} catch (javacard.security.CryptoException e) {
|
|
ISOException.throwIt((short) (0x6FE0 | (e.getReason() & 0x000F)));
|
|
} catch (Throwable t) {
|
|
ISOException.throwIt((short) 0x6FEF);
|
|
}
|
|
|
|
ECPublicKey pub = (ECPublicKey) credentialEphemeralKeyPair.getPublic();
|
|
|
|
short off = 0;
|
|
buf[off++] = TAG_CREDENTIAL_EPUBK;
|
|
short lenPos = off++;
|
|
short keyLen = pub.getW(buf, off);
|
|
buf[lenPos] = (byte) keyLen;
|
|
off += keyLen;
|
|
|
|
sessionFlags[FLAG_AUTH0_DONE] = true;
|
|
apdu.setOutgoingAndSend((short) 0, off);
|
|
}
|
|
|
|
/**
|
|
* Walks the AUTH0 command data, verifies every mandatory TLV (tags,
|
|
* lengths, and protocol version value) is present, and rejects the
|
|
* command with SW=6A80 if anything required is missing or invalid.
|
|
* Unknown tags are silently skipped (spec §1.4.3).
|
|
*/
|
|
private void validateAuth0Data(byte[] buf, short dataOff, short dataLen) {
|
|
if (dataLen <= 0) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
|
|
byte seen = 0;
|
|
short i = dataOff;
|
|
short end = (short) (dataOff + dataLen);
|
|
|
|
while (i < end) {
|
|
if ((short) (i + 2) > end) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
byte tag = buf[i];
|
|
short len = (short) (buf[(short) (i + 1)] & 0xFF);
|
|
short valueOff = (short) (i + 2);
|
|
if ((short) (valueOff + len) > end) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
|
|
switch (tag) {
|
|
case TAG_COMMAND_PARAMETERS:
|
|
requireLen(len, LEN_COMMAND_PARAMETERS);
|
|
sessionState[OFF_COMMAND_PARAMETERS] = buf[valueOff];
|
|
seen |= REQ_CMD_PARAMS;
|
|
break;
|
|
case TAG_AUTHENTICATION_POLICY:
|
|
requireLen(len, LEN_AUTHENTICATION_POLICY);
|
|
sessionState[OFF_AUTH_POLICY] = buf[valueOff];
|
|
seen |= REQ_AUTH_POLICY;
|
|
break;
|
|
case TAG_PROTOCOL_VERSION:
|
|
requireLen(len, LEN_PROTOCOL_VERSION);
|
|
short version = Util.getShort(buf, valueOff);
|
|
if (version != PROTOCOL_VERSION_1_0) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
seen |= REQ_PROTO_VER;
|
|
break;
|
|
case TAG_READER_EPUBK:
|
|
requireLen(len, LEN_READER_EPUBK);
|
|
if (buf[valueOff] != (byte) 0x04) {
|
|
// Must be uncompressed point per spec Table 8-4.
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
Util.arrayCopyNonAtomic(buf, valueOff, sessionState, OFF_READER_EPUBK, len);
|
|
seen |= REQ_READER_EPUBK;
|
|
break;
|
|
case TAG_TRANSACTION_ID:
|
|
requireLen(len, LEN_TRANSACTION_ID);
|
|
Util.arrayCopyNonAtomic(buf, valueOff, sessionState, OFF_TRANSACTION_ID, len);
|
|
seen |= REQ_TXN_ID;
|
|
break;
|
|
case TAG_READER_IDENTIFIER:
|
|
requireLen(len, LEN_READER_IDENTIFIER);
|
|
Util.arrayCopyNonAtomic(buf, valueOff, sessionState, OFF_READER_GROUP_ID, (short) 16);
|
|
Util.arrayCopyNonAtomic(buf, (short) (valueOff + 16), sessionState, OFF_READER_GROUP_SUB_ID, (short) 16);
|
|
seen |= REQ_READER_ID;
|
|
break;
|
|
default:
|
|
// Unknown TLV — spec §1.4.3 says accept silently.
|
|
break;
|
|
}
|
|
i = (short) (valueOff + len);
|
|
}
|
|
|
|
if ((byte) (seen & REQ_ALL_AUTH0) != REQ_ALL_AUTH0) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
}
|
|
|
|
private static void requireLen(short actual, short expected) {
|
|
if (actual != expected) {
|
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
|
}
|
|
}
|
|
}
|