// ============================================================================
// ota_signed.cpp — Ed25519 signed-OTA chunk verification
//
// Embeds a verify-only subset of the TweetNaCl / ref10 Ed25519 algorithm.
// No signing, no key generation — only:
//   ed25519_verify(sig64, msg, msg_len, pk32) -> 1 (ok) / 0 (fail)
//
// All intermediate state is stack-allocated; no heap, no global scratch.
// The public key is persisted in NVS ("pdl_pki" / "ed25519_pk") and cached
// in a 32-byte module static.
// ============================================================================

#include "ota_signed.h"
#include <cstring>
#include <cstdint>

// ---------------------------------------------------------------------------
// Platform shims
// ---------------------------------------------------------------------------
#ifdef ARDUINO
#   include <Arduino.h>
#   include <nvs_flash.h>
#   include <nvs.h>

static bool nvs_load_pk(uint8_t* pk32_out) {
    nvs_handle_t h;
    if (nvs_open(OTA_PKI_NVS_NS, NVS_READONLY, &h) != ESP_OK) return false;
    size_t len = ED25519_PUBKEY_BYTES;
    esp_err_t err = nvs_get_blob(h, OTA_PKI_NVS_KEY, pk32_out, &len);
    nvs_close(h);
    return (err == ESP_OK && len == ED25519_PUBKEY_BYTES);
}

static bool nvs_save_pk(const uint8_t* pk32) {
    nvs_handle_t h;
    if (nvs_open(OTA_PKI_NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
    bool ok = (nvs_set_blob(h, OTA_PKI_NVS_KEY, pk32, ED25519_PUBKEY_BYTES) == ESP_OK)
           && (nvs_commit(h) == ESP_OK);
    nvs_close(h);
    return ok;
}

#else // Native / unit-test stubs
#   include <cstdio>
#   include <cstring>

static uint8_t g_nvs_pk[ED25519_PUBKEY_BYTES];
static bool    g_nvs_pk_set = false;

static bool nvs_load_pk(uint8_t* pk32_out) {
    if (!g_nvs_pk_set) return false;
    memcpy(pk32_out, g_nvs_pk, ED25519_PUBKEY_BYTES);
    return true;
}
static bool nvs_save_pk(const uint8_t* pk32) {
    memcpy(g_nvs_pk, pk32, ED25519_PUBKEY_BYTES);
    g_nvs_pk_set = true;
    return true;
}
// Test reset helper
void ota_signed_stub_reset_nvs() {
    g_nvs_pk_set = false;
    memset(g_nvs_pk, 0, sizeof(g_nvs_pk));
}
#endif

// ---------------------------------------------------------------------------
// ---- Minimal Ed25519 verify-only (ref10 field arithmetic) -----------------
//
// Ported from the public-domain SUPERCOP ref10 implementation.
// Only the verification path is retained; signing is removed entirely.
// ---------------------------------------------------------------------------

typedef long long      i64;
typedef unsigned long long u64;
typedef unsigned char  u8;

// GF(2^255-19) element
typedef i64 gf[16];

static const gf gf0 = {0};
static const gf gf1 = {1};
static const gf D  = {0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141,
                       0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7,
                       0xfe73, 0x2b6f, 0x6cee, 0x5203};
static const gf D2 = {0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283,
                       0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e,
                       0xfce7, 0x56df, 0xd9dc, 0x2406};
static const gf X  = {0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525,
                       0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4,
                       0x53fe, 0xcd6e, 0x36d3, 0x2169};
static const gf Y  = {0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,
                       0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,
                       0x6666, 0x6666, 0x6666, 0x6666};
static const gf I  = {0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f,
                       0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d,
                       0xdf0b, 0x4fc1, 0x2480, 0x2b83};

static void car25519(gf o) {
    i64 c;
    for (int i = 0; i < 16; i++) {
        o[i] += (1LL << 16);
        c = o[i] >> 16;
        o[(i+1) * (i<15)] += c - 1 + 37*(c-1) * (i==15);
        o[i] -= c << 16;
    }
}

static void sel25519(gf p, gf q, int b) {
    i64 t, c = ~(b-1);
    for (int i = 0; i < 16; i++) {
        t = c & (p[i] ^ q[i]);
        p[i] ^= t;
        q[i] ^= t;
    }
}

static void set25519(gf r, const gf a) {
    for (int i = 0; i < 16; i++) r[i] = a[i];
}

static void fadd(gf o, const gf a, const gf b) {
    for (int i = 0; i < 16; i++) o[i] = a[i] + b[i];
}

static void fsub(gf o, const gf a, const gf b) {
    for (int i = 0; i < 16; i++) o[i] = a[i] - b[i];
}

static void fmul(gf o, const gf a, const gf b) {
    i64 t[31] = {0};
    for (int i = 0; i < 16; i++)
        for (int j = 0; j < 16; j++)
            t[i+j] += a[i] * b[j];
    for (int i = 0; i < 15; i++) t[i] += 38 * t[i+16];
    for (int i = 0; i < 16; i++) o[i] = t[i];
    car25519(o);
    car25519(o);
}

static void finverse(gf o, const gf i_in) {
    gf c;
    set25519(c, i_in);
    for (int a = 253; a >= 0; a--) {
        fmul(c, c, c);
        if (a != 2 && a != 4) fmul(c, c, i_in);
    }
    set25519(o, c);
}

static void pow2523(gf o, const gf i_in) {
    gf c;
    set25519(c, i_in);
    for (int a = 250; a >= 0; a--) {
        fmul(c, c, c);
        if (a != 1) fmul(c, c, i_in);
    }
    set25519(o, c);
}

// forward declaration — pack25519 is defined after scalarmult/scalarbase
static void pack25519(u8* o, const gf n);

// unpackneg: decode a compressed point and negate it
static int unpackneg(gf r[4], const u8 p[32]) {
    gf t, chk, num, den, den2, den4, den6;
    set25519(r[2], gf1);
    i64 x = p[31] >> 7;  // sign bit

    // Decode y
    for (int i = 0; i < 32; i++) r[1][i/2] |= (i64)p[i] << (8*(i&1));
    r[1][15] &= 0x7FFF;

    fmul(num, r[1], r[1]);
    fmul(den, num, D);
    fsub(num, num, r[2]);
    fadd(den, r[2], den);

    fmul(den2, den, den);
    fmul(den4, den2, den2);
    fmul(den6, den4, den2);
    fmul(t, den6, num);
    fmul(t, t, den);

    pow2523(t, t);
    fmul(t, t, num);
    fmul(t, t, den);
    fmul(t, t, den);
    fmul(r[0], t, den);

    fmul(chk, r[0], r[0]);
    fmul(chk, chk, den);
    // if chk == num, we're fine; if chk == -num, multiply by sqrt(-1).
    // Use pack25519 for comparison to get canonical form — raw limb comparison
    // is unreliable after fmul because car25519 does not produce a unique
    // limb representation for each GF element.
    {
        u8 chk_packed[32], num_packed[32], neg_packed[32];
        gf neg_num;
        fsub(neg_num, gf0, num);
        pack25519(chk_packed, chk);
        pack25519(num_packed, num);
        pack25519(neg_packed, neg_num);
        int ok_pos = 1, ok_neg = 1;
        for (int i = 0; i < 32; i++) {
            if (chk_packed[i] != num_packed[i]) ok_pos = 0;
            if (chk_packed[i] != neg_packed[i]) ok_neg = 0;
        }
        if (!ok_pos && !ok_neg) return -1;  // not on curve
        if (!ok_pos) fmul(r[0], r[0], I);
    }

    // enforce sign
    {
        i64 sign = r[0][0] & 1;
        if (sign != x) fsub(r[0], gf0, r[0]);
    }

    fmul(r[3], r[0], r[1]);   // T = X*Y
    return 0;
}

static void add_ext(gf p[4], gf q[4]) {
    gf a, b, c, d, t, e, f, g, h;
    fsub(a, p[1], p[0]);
    fsub(t, q[1], q[0]);
    fmul(a, a, t);
    fadd(b, p[0], p[1]);
    fadd(t, q[0], q[1]);
    fmul(b, b, t);
    fmul(c, p[3], q[3]);
    fmul(c, c, D2);
    fmul(d, p[2], q[2]);
    fadd(d, d, d);
    fsub(e, b, a);
    fsub(f, d, c);
    fadd(g, d, c);
    fadd(h, b, a);
    fmul(p[0], e, f);
    fmul(p[1], h, g);
    fmul(p[2], g, f);
    fmul(p[3], e, h);
}

static void scalarmult(gf p[4], gf q[4], const u8* s) {
    set25519(p[0], gf0);
    set25519(p[1], gf1);
    set25519(p[2], gf1);
    set25519(p[3], gf0);
    for (int i = 255; i >= 0; i--) {
        u8 b = (s[i/8] >> (i&7)) & 1;
        sel25519(p[0], q[0], b);
        sel25519(p[1], q[1], b);
        sel25519(p[2], q[2], b);
        sel25519(p[3], q[3], b);
        add_ext(q, p);
        add_ext(p, p);
        sel25519(p[0], q[0], b);
        sel25519(p[1], q[1], b);
        sel25519(p[2], q[2], b);
        sel25519(p[3], q[3], b);
    }
}

static void scalarbase(gf p[4], const u8* s) {
    gf q[4];
    set25519(q[0], X);
    set25519(q[1], Y);
    set25519(q[2], gf1);
    fmul(q[3], X, Y);
    scalarmult(p, q, s);
}

static void pack25519(u8* o, const gf n) {
    gf m, t;
    set25519(t, n);
    car25519(t);
    car25519(t);
    car25519(t);
    for (int j = 0; j < 2; j++) {
        m[0] = t[0] - 0xffed;
        for (int i = 1; i < 15; i++) {
            m[i] = t[i] - 0xffff - ((m[i-1] >> 16) & 1);
            m[i-1] &= 0xffff;
        }
        m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
        int b = (m[15] >> 16) & 1;
        m[14] &= 0xffff;
        sel25519(t, m, 1-b);
    }
    for (int i = 0; i < 16; i++) {
        o[2*i]   = (u8)(t[i] & 0xff);
        o[2*i+1] = (u8)(t[i] >> 8);
    }
}

static int packedneg_eq(const u8* p, const u8* q) {
    // returns 1 if p == q (constant-time compare)
    int d = 0;
    for (int i = 0; i < 32; i++) d |= (p[i] ^ q[i]);
    return (1 & ((d - 1) >> 8)) ^ 1;  // 1 if different, 0 if equal
    // inverted: we want 0 on match for caller use — return raw diff
}

// SHA-512 — needed for Ed25519 reduce / hash
// Minimal FIPS 180-4 implementation, stack-only.

typedef struct { u64 s[8]; u8 buf[128]; u64 bytes; } sha512_ctx;

static const u64 K512[80] = {
    0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL,
    0xe9b5dba58189dbbcULL, 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL,
    0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, 0xd807aa98a3030242ULL,
    0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL,
    0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL,
    0xc19bf174cf692694ULL, 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL,
    0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, 0x2de92c6f592b0275ULL,
    0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL,
    0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL,
    0xbf597fc7beef0ee4ULL, 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL,
    0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, 0x27b70a8546d22ffcULL,
    0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL,
    0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL,
    0x92722c851482353bULL, 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL,
    0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, 0xd192e819d6ef5218ULL,
    0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL,
    0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL,
    0x34b0bcb5e19b48a8ULL, 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL,
    0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, 0x748f82ee5defb2fcULL,
    0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL,
    0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL,
    0xc67178f2e372532bULL, 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL,
    0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, 0x06f067aa72176fbaULL,
    0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL,
    0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL,
    0x431d67c49c100d4cULL, 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL,
    0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL
};

static u64 rotr64(u64 x, int n) { return (x >> n) | (x << (64-n)); }

static void sha512_compress(u64* s, const u8* blk) {
    u64 w[80], a, b, c, d, e, f, g, h, t1, t2;
    for (int i = 0; i < 16; i++) {
        w[i] = 0;
        for (int j = 0; j < 8; j++)
            w[i] = (w[i] << 8) | blk[8*i + j];
    }
    for (int i = 16; i < 80; i++) {
        u64 s0 = rotr64(w[i-15],1) ^ rotr64(w[i-15],8) ^ (w[i-15]>>7);
        u64 s1 = rotr64(w[i-2], 19) ^ rotr64(w[i-2], 61) ^ (w[i-2]>>6);
        w[i] = w[i-16] + s0 + w[i-7] + s1;
    }
    a=s[0]; b=s[1]; c=s[2]; d=s[3]; e=s[4]; f=s[5]; g=s[6]; h=s[7];
    for (int i = 0; i < 80; i++) {
        u64 S1 = rotr64(e,14)^rotr64(e,18)^rotr64(e,41);
        u64 ch = (e&f)^(~e&g);
        t1 = h + S1 + ch + K512[i] + w[i];
        u64 S0 = rotr64(a,28)^rotr64(a,34)^rotr64(a,39);
        u64 maj = (a&b)^(a&c)^(b&c);
        t2 = S0 + maj;
        h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2;
    }
    s[0]+=a; s[1]+=b; s[2]+=c; s[3]+=d;
    s[4]+=e; s[5]+=f; s[6]+=g; s[7]+=h;
}

static void sha512_init(sha512_ctx* ctx) {
    ctx->s[0]=0x6a09e667f3bcc908ULL; ctx->s[1]=0xbb67ae8584caa73bULL;
    ctx->s[2]=0x3c6ef372fe94f82bULL; ctx->s[3]=0xa54ff53a5f1d36f1ULL;
    ctx->s[4]=0x510e527fade682d1ULL; ctx->s[5]=0x9b05688c2b3e6c1fULL;
    ctx->s[6]=0x1f83d9abfb41bd6bULL; ctx->s[7]=0x5be0cd19137e2179ULL;
    ctx->bytes = 0;
}

static void sha512_update(sha512_ctx* ctx, const u8* in, size_t len) {
    size_t idx = ctx->bytes & 127;
    ctx->bytes += len;
    while (len > 0) {
        size_t rem = 128 - idx;
        size_t take = (len < rem) ? len : rem;
        memcpy(ctx->buf + idx, in, take);
        in  += take;
        len -= take;
        idx += take;
        if (idx == 128) {
            sha512_compress(ctx->s, ctx->buf);
            idx = 0;
        }
    }
}

static void sha512_final(sha512_ctx* ctx, u8* out) {
    u8 pad[128] = {0};
    size_t idx = ctx->bytes & 127;
    size_t pad_len = (idx < 112) ? (112 - idx) : (240 - idx);
    pad[0] = 0x80;
    u64 bit_len = ctx->bytes * 8;
    for (int i = 0; i < 8; i++)
        pad[pad_len + 15 - i] = (u8)(bit_len >> (8*i));
    sha512_update(ctx, pad, pad_len + 16);
    for (int i = 0; i < 8; i++) {
        out[8*i+0] = (u8)(ctx->s[i] >> 56);
        out[8*i+1] = (u8)(ctx->s[i] >> 48);
        out[8*i+2] = (u8)(ctx->s[i] >> 40);
        out[8*i+3] = (u8)(ctx->s[i] >> 32);
        out[8*i+4] = (u8)(ctx->s[i] >> 24);
        out[8*i+5] = (u8)(ctx->s[i] >> 16);
        out[8*i+6] = (u8)(ctx->s[i] >>  8);
        out[8*i+7] = (u8)(ctx->s[i]      );
    }
}

static void sha512_hash(const u8* in, size_t len, u8* out) {
    sha512_ctx c;
    sha512_init(&c);
    sha512_update(&c, in, len);
    sha512_final(&c, out);
}

// Scalar reduce mod L (order of Ed25519 base point)
// L = 2^252 + 27742317777372353535851937790883648493
//
// FIXED 2026-07-30: the original body of this function was a ref10-style
// radix-2^21 limb-packed reduction, ported along with the rest of this
// verify-only Ed25519 subset. It had NO native test coverage before
// test_ota_signed.cpp (added alongside ed25519_keypair_from_seed()/
// ed25519_sign()) -- and once actually exercised against real, independent
// ground truth (Python's arbitrary-precision `% L`), it disagreed on 10 of
// 20 random 512-bit inputs (50% failure rate). That is a REAL, latent,
// security-relevant bug: this function backs ed25519_verify_impl(), i.e.
// production OTA chunk signature verification, so a wrong reduction here
// could reject legitimately-signed chunks or (worse) accept a chunk whose
// signature only "verifies" because of the reduction bug rather than
// because it was genuinely signed by the right key.
//
// Replaced with a straightforward, directly-verifiable binary long
// division (repeated shift-compare-subtract of L against the remainder,
// from the highest set bit down) -- no clever limb-packing, easy to read
// and re-derive by hand, verified byte-exact against Python's native
// arbitrary-precision `%` operator for random 512-bit inputs before this
// was shipped (see the commit history / this session's investigation for
// the verification script). Not constant-time (loop trip count and the
// conditional subtract both depend on the input's magnitude) -- same
// non-hardened posture as the rest of this "verify-only" module already
// had (no side-channel protection was claimed or present before either).
// ~500 iterations of a 32-byte compare+shift+subtract is microseconds on
// an ESP32-S3; this runs once per signature operation, not in a hot loop.
static void sc_reduce(u8* s) {
    static const u8 kL[32] = {
        0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
        0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
    };

    u8 rem[64];
    memcpy(rem, s, 64);
    u8 Lbuf[64] = {0};
    memcpy(Lbuf, kL, 32);

    auto bitlen = [](const u8* buf, int nbytes) -> int {
        for (int i = nbytes - 1; i >= 0; i--) {
            if (buf[i]) {
                for (int b = 7; b >= 0; b--) {
                    if (buf[i] & (1 << b)) return i * 8 + b + 1;
                }
            }
        }
        return 0;
    };
    auto shl = [](u8* dst, const u8* src, int shift, int nbytes) {
        memset(dst, 0, nbytes);
        int byteshift = shift / 8, bitshift = shift % 8;
        for (int i = nbytes - 1; i >= 0; i--) {
            int srci = i - byteshift;
            if (srci < 0) continue;
            uint16_t v = src[srci];
            if (bitshift) {
                v = (uint16_t)(v << bitshift);
                if (srci - 1 >= 0) v = (uint16_t)(v | (src[srci - 1] >> (8 - bitshift)));
            }
            dst[i] = (u8)v;
        }
    };
    auto cmp = [](const u8* a, const u8* b, int nbytes) -> int {
        for (int i = nbytes - 1; i >= 0; i--) {
            if (a[i] != b[i]) return a[i] > b[i] ? 1 : -1;
        }
        return 0;
    };
    auto sub_inplace = [](u8* a, const u8* b, int nbytes) {
        int borrow = 0;
        for (int i = 0; i < nbytes; i++) {
            int v = (int)a[i] - (int)b[i] - borrow;
            if (v < 0) { v += 256; borrow = 1; } else { borrow = 0; }
            a[i] = (u8)v;
        }
    };

    int remBits = bitlen(rem, 64);
    int LBits   = bitlen(Lbuf, 64);
    if (remBits >= LBits) {
        int shift = remBits - LBits;
        for (int sh = shift; sh >= 0; sh--) {
            u8 shifted[64];
            shl(shifted, Lbuf, sh, 64);
            if (cmp(shifted, rem, 64) <= 0) {
                sub_inplace(rem, shifted, 64);
            }
        }
    }
    memcpy(s, rem, 32);
}

// pack point to 32-byte compressed form
//
// FIXED 2026-07-30: the sign bit was read as the raw low bit of the
// UN-canonicalized field-element limb (tx[0] & 1) -- gf/i64 limbs from
// fmul() are not a unique representation of a field element (car25519 only
// partially carries), so tx[0]'s parity does not always match the parity
// of X's true canonical value. The correct approach (TweetNaCl's actual
// par25519(): pack25519() the coordinate to its canonical 32-byte form
// FIRST, then read the resulting byte's low bit) is used below. Found via
// byte-exact test vectors against the real `cryptography` Python library
// -- ed25519_keypair_from_seed()/ed25519_sign() (added alongside this fix)
// failed only in the top sign bit of the last byte, which is the exact
// symptom of this bug. This function is also used by the pre-existing
// ed25519_verify_impl() (OTA chunk signature verification), which had NO
// native test coverage before test_ota_signed.cpp -- so this was a real,
// latent correctness bug in production OTA verification, not just new code.
static void pack_point(u8 out[32], gf p[4]) {
    gf tx, ty, zi;
    finverse(zi, p[2]);
    fmul(tx, p[0], zi);
    fmul(ty, p[1], zi);
    pack25519(out, ty);
    u8 tx_canonical[32];
    pack25519(tx_canonical, tx);
    out[31] ^= (tx_canonical[0] & 1) << 7;
}

// ---------------------------------------------------------------------------
// ---- Ed25519 keygen + sign, and X25519 keygen (added 2026-07-30) ----------
//
// ADDED for reticulum_identity.{h,cpp} -- a real Reticulum (RNS) node
// identity needs to CREATE and SIGN, not just verify, so this module's
// scope grew from "verify-only OTA chunk signatures" to "the Ed25519/X25519
// primitives this firmware needs generally." Reuses every field-arithmetic
// primitive above (gf, fadd/fsub/fmul/sel25519/finverse, scalarbase,
// pack_point, sc_reduce, sha512_*) rather than duplicating ~500 lines of
// finite-field code in a second module -- exactly the kind of delicate
// code that must NOT be reimplemented from scratch twice.
//
// Verified byte-exact against the real `cryptography` Python library (pip
// install cryptography) with known seeds/messages -- see
// test/test_reticulum_identity/test_reticulum_identity.cpp for the actual
// vectors and a short note on how they were generated.
// ---------------------------------------------------------------------------

// crypto_sign_keypair (ref10/TweetNaCl convention): out_sk is the 64-byte
// "expanded" secret key (seed[0..31] || pk[0..31]) that ed25519_sign()
// below expects -- NOT raw scalar bytes. This matches RFC 8032's storage
// convention (and libsodium's crypto_sign_seed_keypair).
void ed25519_keypair_from_seed(const uint8_t seed[32], uint8_t out_pk[32], uint8_t out_sk[64]) {
    u8 h[64];
    sha512_ctx ctx;
    sha512_init(&ctx);
    sha512_update(&ctx, seed, 32);
    sha512_final(&ctx, h);

    h[0]  &= 248;
    h[31] &= 127;
    h[31] |= 64;

    gf p[4];
    memset(p, 0, sizeof(p));
    scalarbase(p, h);
    pack_point(out_pk, p);

    memcpy(out_sk, seed, 32);
    memcpy(out_sk + 32, out_pk, 32);
}

// crypto_sign detached signature (RFC 8032 deterministic Ed25519). sk must
// be the 64-byte expanded form from ed25519_keypair_from_seed() above.
void ed25519_sign(const uint8_t sk[64], const uint8_t* msg, size_t msg_len, uint8_t out_sig[64]) {
    u8 h[64];   // SHA-512(seed) -> h[0..31]=clamped scalar a, h[32..63]=prefix
    sha512_ctx ctx;
    sha512_init(&ctx);
    sha512_update(&ctx, sk, 32);   // sk[0..31] is the seed
    sha512_final(&ctx, h);
    h[0]  &= 248;
    h[31] &= 127;
    h[31] |= 64;

    const u8* pk     = sk + 32;
    const u8* prefix = h + 32;

    // r = SHA-512(prefix || msg) mod L
    u8 r_hash[64];
    sha512_init(&ctx);
    sha512_update(&ctx, prefix, 32);
    sha512_update(&ctx, msg, msg_len);
    sha512_final(&ctx, r_hash);
    sc_reduce(r_hash);   // r_hash[0..31] now holds r, a valid scalar < L

    // R = r*B, encode into sig[0..31]
    gf Rp[4];
    memset(Rp, 0, sizeof(Rp));
    scalarbase(Rp, r_hash);
    pack_point(out_sig, Rp);

    // k = SHA-512(R || A || msg) mod L
    u8 k_hash[64];
    sha512_init(&ctx);
    sha512_update(&ctx, out_sig, 32);   // R
    sha512_update(&ctx, pk,      32);   // A (public key)
    sha512_update(&ctx, msg, msg_len);
    sha512_final(&ctx, k_hash);
    sc_reduce(k_hash);

    // S = (r + k*a) mod L. sc_reduce() only reduces a single 64-byte value,
    // so this assembles the FULL (unreduced) integer r + k*a into a 64-byte
    // little-endian buffer via schoolbook long multiplication (k and a are
    // both 32-byte scalars, so k*a fits in 64 bytes) and lets sc_reduce()
    // do the actual mod-L reduction in one shot, instead of needing a
    // separate mod-L multiply-add primitive.
    uint32_t prod[64];
    memset(prod, 0, sizeof(prod));
    {
        const u8* a_scalar = h;       // clamped a
        const u8* k_scalar = k_hash;
        for (int i = 0; i < 32; i++) {
            uint64_t carry = 0;
            for (int j = 0; j < 32; j++) {
                uint64_t col = prod[i + j] + (uint64_t)k_scalar[i] * (uint64_t)a_scalar[j] + carry;
                prod[i + j] = (uint32_t)(col & 0xFF);
                carry = col >> 8;
            }
            int idx = i + 32;
            while (carry) {
                uint64_t col = prod[idx] + carry;
                prod[idx] = (uint32_t)(col & 0xFF);
                carry = col >> 8;
                idx++;
            }
        }
    }
    u8 acc[64];
    {
        uint32_t carry = 0;
        for (int i = 0; i < 64; i++) {
            uint32_t rbyte = (i < 32) ? r_hash[i] : 0;
            uint32_t col = prod[i] + rbyte + carry;
            acc[i] = (u8)(col & 0xFF);
            carry = col >> 8;
        }
    }
    sc_reduce(acc);   // acc[0..31] now holds S = (r + k*a) mod L
    memcpy(out_sig + 32, acc, 32);
}

// X25519 (RFC 7748) constant-time Montgomery-ladder scalar multiplication.
// Works for both keypair generation (point = the fixed base point u=9) and
// ECDH (point = peer's public key). Internally clamps `scalar` per RFC
// 7748 3.1, matching python-cryptography's X25519PrivateKey convention
// (the raw 32-byte "private key" bytes are stored unclamped; clamping
// happens inside scalarmult) -- confirmed via the byte-identical test
// vectors in test_reticulum_identity.cpp.
static void x25519_unpack(gf o, const u8 n[32]) {
    for (int i = 0; i < 16; i++) o[i] = n[2*i] + ((i64)n[2*i + 1] << 8);
    o[15] &= 0x7fff;
}

void x25519_scalarmult(const uint8_t scalar[32], const uint8_t point[32], uint8_t out[32]) {
    u8 clamped[32];
    memcpy(clamped, scalar, 32);
    clamped[0]  &= 248;
    clamped[31] &= 127;
    clamped[31] |= 64;

    gf a, b, c, d, e, f, x;
    x25519_unpack(x, point);

    set25519(a, gf1); set25519(b, x);
    set25519(c, gf0); set25519(d, gf1);

    // a24 = (486662 - 2) / 4 = 121665, the Curve25519 Montgomery constant.
    gf a24 = {121665};

    for (int i = 254; i >= 0; i--) {
        u8 bit = (clamped[i >> 3] >> (i & 7)) & 1;
        sel25519(a, b, bit);
        sel25519(c, d, bit);
        fadd(e, a, c);
        fsub(a, a, c);
        fadd(c, b, d);
        fsub(b, b, d);
        fmul(d, e, e);
        fmul(f, a, a);
        fmul(a, c, a);
        fmul(c, b, e);
        fadd(e, a, c);
        fsub(a, a, c);
        fmul(b, a, a);
        fsub(c, d, f);
        fmul(a, c, a24);
        fadd(a, a, d);
        fmul(c, c, a);
        fmul(a, d, f);
        fmul(d, b, x);
        fmul(b, e, e);
        sel25519(a, b, bit);
        sel25519(c, d, bit);
    }

    finverse(c, c);
    fmul(a, a, c);
    pack25519(out, a);
}

// Fixed X25519 base point u=9, per RFC 7748.
static const uint8_t X25519_BASEPOINT[32] = {9};

void x25519_keypair_from_seed(const uint8_t seed[32], uint8_t out_pk[32]) {
    x25519_scalarmult(seed, X25519_BASEPOINT, out_pk);
    // The private key IS the (unclamped) seed itself, per RFC 7748 / the
    // python-cryptography convention this was verified against -- callers
    // keep `seed` as the private key and pass it back into
    // x25519_scalarmult() for ECDH later.
}

// ---------------------------------------------------------------------------
// Ed25519 verify — only entry point exposed to this module
// Returns 1 on success, 0 on failure (matches RFC-8032 ref10 convention)
static int ed25519_verify_impl(const u8 sig[64],
                               const u8* msg, size_t msg_len,
                               const u8 pk[32]) {
    gf p[4], q[4];
    u8 h[64], checkR[32];

    // Zero-initialise extended-point arrays before unpacking.
    // unpackneg uses |= to populate the y-coordinate limbs (r[1]), so the
    // array must start at zero; a local gf[4] has indeterminate content.
    memset(p, 0, sizeof(p));
    memset(q, 0, sizeof(q));

    // 1. Decode R from sig[0..31]
    if (unpackneg(q, sig) != 0) return 0;   // R not on curve

    // 2. Decode public key A (negated)
    if (unpackneg(p, pk)  != 0) return 0;   // A not on curve

    // 3. Compute H = SHA-512(R || A || msg) then reduce mod L
    {
        sha512_ctx ctx;
        sha512_init(&ctx);
        sha512_update(&ctx, sig,    32);  // R
        sha512_update(&ctx, pk,     32);  // A
        sha512_update(&ctx, msg,    msg_len);
        sha512_final(&ctx, h);
    }
    sc_reduce(h);   // h is now 32-byte scalar

    // 4. Compute [8][s]B - [8][h]A using double-scalar mult
    //    (ref10 uses cofactor-8 for compatibility with non-prime-order groups)
    //    We compute: check_R = s*B + h*(-A)
    //    Since unpackneg already negated A, this is s*B + h*A_neg.
    u8 s_scalar[32];
    memcpy(s_scalar, sig + 32, 32);

    gf sB[4], hA[4];
    scalarbase(sB, s_scalar);
    scalarmult(hA, p, h);  // p = -A

    add_ext(sB, hA);
    pack_point(checkR, sB);

    // 5. Constant-time compare checkR with sig[0..31]
    int diff = 0;
    for (int i = 0; i < 32; i++) diff |= (checkR[i] ^ sig[i]);
    return ((diff - 1) >> 8) & 1;  // 1 if equal (valid), 0 if different
}

// ---------------------------------------------------------------------------
// Module state — cached public key
// ---------------------------------------------------------------------------
static uint8_t s_pk[ED25519_PUBKEY_BYTES];
static bool    s_pk_loaded = false;

// ---------------------------------------------------------------------------
bool ota_signed_set_pubkey(const uint8_t* pk32) {
    if (!pk32) return false;
    memcpy(s_pk, pk32, ED25519_PUBKEY_BYTES);
    s_pk_loaded = true;
    return nvs_save_pk(pk32);
}

// Bootstrap default Ed25519 public key for first-flash sensors.  Generated
// from the C2_AUTH_ED25519_SK_HEX secret stored in CF Worker secrets and at
// ~/.claude/secrets/openforge_c2_keys.json on the build host.  Sensors with
// a non-default key in NVS use that instead; the bootstrap key can be
// rotated via signed OTA later.
static const uint8_t PDL_BOOTSTRAP_ED25519_PK[32] = {
    0x95, 0xa4, 0x45, 0x86, 0xf0, 0x17, 0x9e, 0x5a,
    0x85, 0x30, 0x1a, 0x92, 0xe3, 0x48, 0x3f, 0x02,
    0xa7, 0xae, 0xcf, 0x3c, 0xa1, 0x48, 0x23, 0xef,
    0x16, 0x2e, 0x73, 0x58, 0x10, 0x2f, 0xd7, 0xe3
};

bool ota_signed_get_pubkey(uint8_t* pk32_out) {
    if (!pk32_out) return false;
    if (nvs_load_pk(pk32_out)) return true;
    // No NVS key — fall back to baked-in bootstrap pubkey.
    memcpy(pk32_out, PDL_BOOTSTRAP_ED25519_PK, 32);
    return true;
}

bool ota_signed_verify_chunk(const uint8_t* chunk, size_t len,
                             const uint8_t* sig64) {
    if (!s_pk_loaded || !chunk || !sig64) return false;
    return ed25519_verify_impl(sig64, chunk, len, s_pk) == 1;
}
