package com.dangerousthings.aliro;

import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.ECPublicKey;
import javacard.security.KeyBuilder;
import javacard.security.Signature;

/**
 * Verifies an Aliro IssuerAuth COSE_Sign1 (RFC 9052) over ECDSA-P256 +
 * SHA-256. Single use case: at personalization we verify the Access
 * Document signature ONCE, cache the verdict (M2A.1's
 * {@code accessDocumentVerified} flag), and trust thereafter.
 *
 * <p>COSE_Sign1 wire format (RFC 9052 §3):
 * <pre>
 *   [ protected_bstr, unprotected_map, payload_bstr, signature_bstr ]
 * </pre>
 * Sig_structure that was signed (RFC 9052 §4.4):
 * <pre>
 *   [ "Signature1", body_protected, external_aad, payload ]
 * </pre>
 * Signature is raw {@code r || s} (64 bytes for P-256), not DER —
 * the JC {@code Signature.ALG_ECDSA_SHA_256} primitive expects DER, so
 * we transcode raw → DER before {@link Signature#verify}.
 *
 * <p>The CBOR walk uses {@link StructuralCbor#decodeHeader} +
 * {@link StructuralCbor#elementSpan} for the 4-element COSE_Sign1 array:
 * decode the outer array header, decode each bstr header at elements 0/2/3
 * (protected / payload / signature) to capture value offsets, and span-skip
 * the unprotected map at element 1. Any malformed input throws
 * {@code ISOException(SW_DATA_INVALID)} which the outer try/catch collapses
 * to "not verified".
 *
 * <p>The verifier owns a reusable {@link ECPublicKey} slot, seeded with
 * P-256 curve params at construction. Each call calls {@code setW} with
 * the caller-supplied uncompressed (0x04 || X || Y) issuer key — no
 * per-call allocation.
 */
final class CoseVerifier {

    /** Maximum supported COSE_Sign1 payload length. The Aliro Access
     *  Document fits comfortably under this; we size the Sig_structure
     *  working buffer for it. */
    static final short MAX_PAYLOAD = (short) 512;

    /** Maximum supported protected bstr length. Aliro IssuerAuth uses a
     *  single {alg: ES256} map (3 bytes), but we cap at 32 for slack. */
    static final short MAX_PROTECTED = (short) 32;

    /** Length of a raw P-256 ECDSA signature: r(32) || s(32). */
    private static final short RAW_SIG_LEN = (short) 64;

    /** Length of an uncompressed SEC1 P-256 public key: 0x04 || X(32) || Y(32). */
    private static final short UNCOMP_PUB_LEN = (short) 65;

    /**
     * Sig_structure working buffer layout:
     * <pre>
     *   0x84              array(4)
     *   0x6A              tstr(10)        — "Signature1"
     *   53 69 67 6E 61    "Signature1" (10 bytes)
     *   74 75 72 65 31
     *   [protected_bstr CBOR header + bytes]
     *   0x40              bstr(0)         — external_aad (empty)
     *   [payload_bstr CBOR header + bytes]
     * </pre>
     * "Signature1" prefix is fixed; cache it.
     */
    private static final byte[] SIG_STRUCT_PREFIX = {
        (byte) 0x84,                                             // array(4)
        (byte) 0x6A,                                             // tstr(10)
        (byte) 0x53, (byte) 0x69, (byte) 0x67, (byte) 0x6E,      // "Sign"
        (byte) 0x61, (byte) 0x74, (byte) 0x75, (byte) 0x72,      // "atur"
        (byte) 0x65, (byte) 0x31                                 // "e1"
    };

    private final ECPublicKey issuerPubKey;
    private final Signature ecdsaVerifier;
    /** Transient scratch for building the Sig_structure and DER signature.
     *  Sized for MAX_PAYLOAD + MAX_PROTECTED + headers + DER overhead. */
    private final byte[] scratch;

    CoseVerifier() {
        issuerPubKey = (ECPublicKey) KeyBuilder.buildKey(
            KeyBuilder.TYPE_EC_FP_PUBLIC,
            KeyBuilder.LENGTH_EC_FP_256,
            false);
        Secp256r1Params.seedPublic(issuerPubKey);
        ecdsaVerifier = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
        // Sig_structure size = prefix(12) + prot_hdr(<=3) + prot_bytes(<=32)
        //                    + ext_aad(1) + pl_hdr(<=3) + pl_bytes(<=512)
        // DER sig max = 2 + 2 + 33 + 2 + 33 = 72.
        // Round up to keep arithmetic simple.
        scratch = JCSystem.makeTransientByteArray(
            (short) 768, JCSystem.CLEAR_ON_DESELECT);
    }

    /**
     * Verifies a COSE_Sign1 IssuerAuth blob against the supplied issuer
     * public key.
     *
     * @param coseSign1        buffer holding the CBOR-encoded COSE_Sign1
     * @param coseOff          offset of the outer array tag (0x84)
     * @param coseLen          total length of the COSE_Sign1 blob
     * @param issuerPubUncomp  buffer holding the 65-byte uncompressed SEC1
     *                         pubkey (0x04 || X || Y)
     * @param pubOff           offset of the 0x04 tag
     * @return {@code true} iff the signature verifies; {@code false} on any
     *         malformed input, mismatched key, or invalid signature.
     */
    boolean verifyCoseSign1(
            byte[] coseSign1, short coseOff, short coseLen,
            byte[] issuerPubUncomp, short pubOff) {
        try {
            return verifyInternal(coseSign1, coseOff, coseLen, issuerPubUncomp, pubOff);
        } catch (Throwable t) {
            // Any malformed CBOR, length overrun, crypto exception, etc.
            // collapses to "not verified". The personalization caller will
            // refuse to install the access document.
            return false;
        }
    }

    private boolean verifyInternal(
            byte[] coseSign1, short coseOff, short coseLen,
            byte[] issuerPubUncomp, short pubOff) {
        short p = coseOff;
        short remaining = coseLen;
        // 4-byte scratch shared across decodeHeader / elementSpan calls.
        // Reuse the end of `scratch` so we don't allocate.
        final short argOff = (short) (scratch.length - 4);

        // Outer array(4): expect major type 4, argument == 4.
        short hdr = StructuralCbor.decodeHeader(
            coseSign1, p, remaining, scratch, argOff);
        if ((short) ((hdr >> 8) & 0x07) != 4) return false;
        if (readArg(scratch, argOff) != 4) return false;
        short consumed = (short) (hdr & 0xFF);
        p += consumed;
        remaining -= consumed;

        // Element 0: protected_bstr — decode header, capture value off/len.
        hdr = StructuralCbor.decodeHeader(
            coseSign1, p, remaining, scratch, argOff);
        if ((short) ((hdr >> 8) & 0x07) != 2) return false;
        short protLen = readArg(scratch, argOff);
        if (protLen > MAX_PROTECTED) return false;
        consumed = (short) (hdr & 0xFF);
        short protValOff = (short) (p + consumed);
        if ((short) (consumed + protLen) > remaining) return false;
        p += (short) (consumed + protLen);
        remaining -= (short) (consumed + protLen);

        // Element 1: unprotected_map — span-skip, contents don't enter Sig_structure.
        short mapSpan = StructuralCbor.elementSpan(
            coseSign1, p, remaining, scratch, argOff);
        p += mapSpan;
        remaining -= mapSpan;

        // Element 2: payload_bstr — decode header, capture value off/len.
        hdr = StructuralCbor.decodeHeader(
            coseSign1, p, remaining, scratch, argOff);
        if ((short) ((hdr >> 8) & 0x07) != 2) return false;
        short payloadLen = readArg(scratch, argOff);
        if (payloadLen > MAX_PAYLOAD) return false;
        consumed = (short) (hdr & 0xFF);
        short payloadValOff = (short) (p + consumed);
        if ((short) (consumed + payloadLen) > remaining) return false;
        p += (short) (consumed + payloadLen);
        remaining -= (short) (consumed + payloadLen);

        // Element 3: signature_bstr — raw 64-byte P-256 ECDSA (r||s).
        hdr = StructuralCbor.decodeHeader(
            coseSign1, p, remaining, scratch, argOff);
        if ((short) ((hdr >> 8) & 0x07) != 2) return false;
        short sigLen = readArg(scratch, argOff);
        if (sigLen != RAW_SIG_LEN) return false;
        consumed = (short) (hdr & 0xFF);
        short sigValOff = (short) (p + consumed);
        if ((short) (consumed + sigLen) > remaining) return false;

        // Build Sig_structure into scratch.
        short s = (short) 0;
        Util.arrayCopyNonAtomic(SIG_STRUCT_PREFIX, (short) 0,
            scratch, s, (short) SIG_STRUCT_PREFIX.length);
        s += (short) SIG_STRUCT_PREFIX.length;
        // protected_bstr (re-emit header so it matches input verbatim).
        s = writeBstr(coseSign1, protValOff, protLen, scratch, s);
        // external_aad = empty bstr (h''): single byte 0x40.
        scratch[s++] = (byte) 0x40;
        // payload bstr.
        s = writeBstr(coseSign1, payloadValOff, payloadLen, scratch, s);

        // Place the DER-encoded signature after the Sig_structure.
        short derLen = rawSigToDer(coseSign1, sigValOff, scratch, s);
        short derOff = s;

        // Load issuer pubkey.
        issuerPubKey.setW(issuerPubUncomp, pubOff, UNCOMP_PUB_LEN);
        ecdsaVerifier.init(issuerPubKey, Signature.MODE_VERIFY);
        return ecdsaVerifier.verify(
            scratch, (short) 0, s,
            scratch, derOff, derLen);
    }

    /** Reads the 4-byte big-endian argument that
     *  {@link StructuralCbor#decodeHeader} wrote into the scratch as a short.
     *  StructuralCbor itself caps argument values at {@code Short.MAX_VALUE}
     *  before they can reach us, so the high half is guaranteed zero. */
    private static short readArg(byte[] buf, short off) {
        return (short) (((buf[(short) (off + 2)] & 0xFF) << 8)
                      | (buf[(short) (off + 3)] & 0xFF));
    }

    /** Copies {@code len} bytes from {@code src[srcOff..]} into {@code dst}
     *  prefixed by a freshly-emitted CBOR bstr header. Returns the post-write
     *  offset into {@code dst}. */
    private static short writeBstr(byte[] src, short srcOff, short len,
                                   byte[] dst, short dstOff) {
        if (len <= 23) {
            dst[dstOff++] = (byte) (0x40 | len);
        } else if (len <= 0xFF) {
            dst[dstOff++] = (byte) 0x58;
            dst[dstOff++] = (byte) len;
        } else {
            dst[dstOff++] = (byte) 0x59;
            dst[dstOff++] = (byte) ((len >> 8) & 0xFF);
            dst[dstOff++] = (byte) (len & 0xFF);
        }
        Util.arrayCopyNonAtomic(src, srcOff, dst, dstOff, len);
        return (short) (dstOff + len);
    }

    /**
     * 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).
     *
     * <p>Duplicates {@code AliroApplet.rawSigToDer} verbatim — both
     * callers are private and Java Card 1.7 has no facility for a shared
     * package-level helper without a separate utility class.
     */
    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);
    }
}
