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 &sect;10.2.1).
 *   <li>AUTH0 (standard phase) generates a fresh credential ephemeral
 *       P-256 keypair and returns the public key as tag 0x86 (spec
 *       &sect;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;

    // 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 = false;

    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;
    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) is pulled
    // from CryptoSingletons.getAliroGcm(). The target card (NXP J3R180) does
    // not expose AEADCipher.ALG_AES_GCM despite advertising JC 3.0.5, so we
    // implement GCM in userland. Sharing with StepUpApplet via the singleton
    // saves ~370 B of transient/EEPROM footprint that a per-applet duplicate
    // would otherwise pay. See encryptResponseGcm().

    // Low-level crypto primitives (ECDH, HKDF, Kdh, expedited key derivation)
    // are pulled from CryptoSingletons.getAliroCrypto() so both AliroApplet
    // and StepUpApplet share one instance — saves ~352 B transient versus a
    // per-applet duplicate. Local refs in deriveSessionKeys / processDiag.

    /** 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 live in {@link Secp256r1Params}
    // — shared with {@link CoseVerifier}. J3R180 doesn't ship a default P-256
    // parameter set on its EC keys, so seedSecp256r1() must run before any
    // genKeyPair / setS / setW / Signature.init.
    /** 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 {
            // Force lazy alloc of the shared AliroGcm so any install-time
            // failure (Cipher.getInstance / KeyBuilder.buildKey rejection)
            // surfaces with the same greppable diagnostic SW as before.
            CryptoSingletons.getAliroGcm();
        } catch (ISOException e) { throw e;  // preserve inner diagnostic SW
        } catch (Throwable t) { ISOException.throwIt((short) 0x6FA8); }
        try {
            // Force lazy alloc of the shared AliroCrypto so the same install-
            // time failure ladder applies if the constructor throws.
            CryptoSingletons.getAliroCrypto();
        } 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.
     *
     *  <p>Thin wrapper over {@link Secp256r1Params#seed(KeyPair)} kept for
     *  call-site readability; the byte arrays live in the shared class. */
    private static void seedSecp256r1(KeyPair kp) {
        Secp256r1Params.seed(kp);
    }

    // --- 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: {
                AliroCrypto cryptoHmac = CryptoSingletons.getAliroCrypto();
                for (short i = 0; i < n; i++) {
                    cryptoHmac.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();
                AliroCrypto cryptoEcdh = CryptoSingletons.getAliroCrypto();
                for (short i = 0; i < n; i++) {
                    cryptoEcdh.computeEcdhSharedX(priv,
                        Secp256r1Params.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.
                AliroGcm gcm = CryptoSingletons.getAliroGcm();
                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();
        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;
            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);
        }
    }

    /**
     * Handles AUTH1 (spec &sect;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);

        AliroCrypto crypto = CryptoSingletons.getAliroCrypto();
        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)
     * </pre>
     *
     * <p>Spec §8.3.1.13 salt_volatile ends at the 0xA5 proprietary TLV. We
     * previously appended x(access_credential_pub_key) here too -- that was
     * an misread of the spec text (it belongs in {@code info}, not
     * salt_volatile, and {@code info} uses the EPHEMERAL credential key, not
     * the long-term one). The misread was symmetric between this applet and
     * our PC/SC reader, so AUTH1 succeeded against our own host-side reader
     * but failed against ST's X-CUBE-ALIRO with
     * {@code ACWG_Error_Crypto_EncryptDecrypt}. Removed 2026-06-11.
     */
    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 (1 byte each).
        // command_parameters comes from the AUTH1 command -- "from the command
        // data field" in §8.3.1.13 refers to the AUTH1 command being processed
        // when keys are derived (the AUTH0 cmd_params still gets used for the
        // EXPEDITED-FAST path's salt_persistent per §8.3.1.12, but for the
        // EXPEDITED-STANDARD §8.3.1.13 path AUTH1 is the active request).
        // authentication_policy only exists in AUTH0, so it's pulled from
        // the AUTH0 state we saved earlier.
        // Empirical confirmation: with AUTH0's cmd_params the X-CUBE-ALIRO
        // vendor library failed AES-GCM tag verify on AUTH1 response with
        // ACWG_Error_Crypto_EncryptDecrypt; AUTH1's cmd_params unblocks it.
        out[p++] = sessionState[OFF_AUTH1_CMD_PARAMS];
        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;

        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
     * {@code 0x5E signaling_bitmap} reflecting step-up capability and
     * AD retrievability (see inline comment at the bitmap emit).
     */
    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 from this credential. Bit 2: retrieval
        // requires step-up AID SELECT (applicable on NFC). Other bits unused
        // in v1 (no mailbox/notify).
        //
        // Bit 2 is always set: our architecture always uses split AID
        // (5501 expedited + 5502 step-up), so any AD retrieval will go
        // through SELECT ACCE5502. We advertise the step-up channel even
        // when nothing's there yet — readers that don't speak step-up just
        // won't try.
        //
        // Bit 0 reflects whether we can actually serve an AD: it requires
        // both hasAccessDocument() (finalized) AND isAccessDocumentVerified()
        // (IssuerAuth check passed). finalizeAccessDocument() commits both
        // flags atomically, so the "finalized but not verified" branch is
        // defensive.
        //
        // Note: the closed-source ACWG_processAUTH1ResponsePayload() in
        // X-CUBE-ALIRO's Aliro.a errors out when bits 0 + 2 are clear and AD
        // is provisioned. With bit 2 now always set we still keep that
        // library happy, and bit 0 only makes a promise we can actually keep.
        byte bitmapLo = 0x04;  // bit 2 — split-AID step-up architecture
        if (store.hasAccessDocument() && store.isAccessDocumentVerified()) {
            bitmapLo |= 0x01;  // bit 0 — AD retrievable
        }
        out[p++] = (byte) 0x5E;
        out[p++] = (byte) 0x02;
        out[p++] = (byte) 0x00;  // high byte unused in v1
        out[p++] = bitmapLo;

        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 CryptoSingletons.getAliroGcm().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
     * &sect;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 &sect;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 &sect;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 &sect;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);
        }
    }
}
