/**
 * Run Certificate verifier — standalone surface for external clients (CLI, dashboards).
 *
 * This module is the **single source of truth** for offline verification of a
 * `SignedRunProofCertificate`. The CC server (`src/proof/ed25519-{signer,verifier}.ts`)
 * holds the signing side (DB-coupled assembly + signing); this module holds the
 * verification side, which is intentionally dependency-free (no DB, no MCP).
 *
 * Pipeline (mirrors server signer):
 *   1. Strip embedded `signature` field
 *   2. JCS-canonicalize (RFC 8785 subset: sorted keys, -0 → 0)
 *   3. SHA-256 → first 31 bytes → felt252-safe
 *   4. Poseidon([0x1, sha_felt, CERT_MARKER_FELT]) → cert_hash_felt252
 *   5. Felt → 32-byte buffer → Ed25519 verify
 *
 * Domain separator: `CERT_MARKER_FELT` = UTF-8 "run_cert" felt252 (right-aligned).
 *
 * @see ../../../src/proof/ed25519-signer.ts (signer, CC server)
 * @see docs/ietf/draft-vauban-skill-attestation-00.md (RFC profile)
 * @public
 */
import { createHash, createPublicKey, verify as cryptoVerify, } from "node:crypto";
import { hash } from "starknet";
// ─── Constants ────────────────────────────────────────────────────────────────
/** Domain separator: UTF-8 "run_cert" → felt252 right-aligned, zero-padded. */
export const CERT_MARKER_FELT = "0x" + Buffer.from("run_cert", "utf8").toString("hex").padStart(62, "0");
// ─── JCS canonicalization (RFC 8785 subset) ──────────────────────────────────
function normalizeValue(value) {
    if (value === null)
        return null;
    if (typeof value === "number") {
        if (Object.is(value, -0))
            return 0;
        return value;
    }
    if (Array.isArray(value))
        return value.map(normalizeValue);
    if (typeof value === "object") {
        const obj = value;
        const sorted = {};
        for (const key of Object.keys(obj).sort()) {
            sorted[key] = normalizeValue(obj[key]);
        }
        return sorted;
    }
    return value;
}
function canonicalizeJcs(data) {
    return JSON.stringify(normalizeValue(data));
}
// ─── Felt helpers ─────────────────────────────────────────────────────────────
function felt252ToBytes(felt) {
    const hex = felt.replace(/^0x/, "").padStart(64, "0");
    return Buffer.from(hex, "hex");
}
/**
 * Compute the felt252 hash that Ed25519 signs over for a Run Certificate.
 * Strips any embedded `signature` field before hashing — verification is
 * idempotent.
 */
export function computeCertHashFelt252(cert) {
    const { signature: _stripped, ...unsigned } = cert;
    void _stripped;
    const canonical = canonicalizeJcs(unsigned);
    const sha = createHash("sha256").update(canonical, "utf8").digest("hex");
    const shaFelt = "0x" + sha.substring(0, 62);
    return hash.computePoseidonHashOnElements(["0x1", shaFelt, CERT_MARKER_FELT]);
}
// ─── Public-key reconstruction ────────────────────────────────────────────────
/**
 * Reconstruct an Ed25519 `KeyObject` from base64 SPKI DER (as embedded in
 * `signature.pubkey_spki_b64`).
 *
 * @throws if base64 is malformed or the key is not Ed25519.
 */
export function publicKeyFromSpkiB64(pubkeySpkiB64) {
    const der = Buffer.from(pubkeySpkiB64, "base64");
    if (der.length === 0) {
        throw new Error("[cert-verify] empty SPKI buffer");
    }
    const key = createPublicKey({ key: der, format: "der", type: "spki" });
    if (key.asymmetricKeyType !== "ed25519") {
        throw new Error(`[cert-verify] SPKI is not Ed25519 (got ${key.asymmetricKeyType})`);
    }
    return key;
}
// ─── Verifier ─────────────────────────────────────────────────────────────────
/**
 * Verify a signed Run Certificate. Returns a structured result — never throws
 * on verification failure (only on malformed input).
 *
 * The 7 possible failure reasons are typed via `CertVerifyFailReason` so callers
 * can branch precisely. `recomputed_cert_hash_felt252` is always returned for
 * audit trail / debug inspection.
 */
export function verifyRunCertificate(cert, opts = {}) {
    const recomputed = computeCertHashFelt252(cert);
    const sig = cert.signature;
    if (!sig) {
        return {
            valid: false,
            reason: "missing_signature",
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    if (sig.alg !== "Ed25519") {
        return {
            valid: false,
            reason: "wrong_alg",
            details: `expected Ed25519, got ${String(sig.alg)}`,
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    if (sig.cert_hash_felt252 !== recomputed) {
        return {
            valid: false,
            reason: "hash_mismatch",
            details: `embedded=${sig.cert_hash_felt252} recomputed=${recomputed}`,
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    if (opts.expectedKid && opts.expectedKid !== sig.kid) {
        return {
            valid: false,
            reason: "kid_mismatch",
            details: `expected kid=${opts.expectedKid}, got ${sig.kid}`,
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    let pubkey;
    if (opts.expectedPublicKey) {
        pubkey = opts.expectedPublicKey;
    }
    else {
        try {
            pubkey = publicKeyFromSpkiB64(sig.pubkey_spki_b64);
        }
        catch (err) {
            return {
                valid: false,
                reason: "pubkey_unresolvable",
                details: err instanceof Error ? err.message : String(err),
                recomputed_cert_hash_felt252: recomputed,
            };
        }
    }
    let sigBytes;
    try {
        sigBytes = Buffer.from(sig.value, "base64");
    }
    catch {
        return {
            valid: false,
            reason: "malformed_signature",
            details: "signature.value is not valid base64",
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    if (sigBytes.length !== 64) {
        return {
            valid: false,
            reason: "malformed_signature",
            details: `Ed25519 signature must be 64 bytes (got ${sigBytes.length})`,
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    const msgBytes = felt252ToBytes(recomputed);
    const ok = cryptoVerify(null, msgBytes, pubkey, sigBytes);
    if (!ok) {
        return {
            valid: false,
            reason: "signature_invalid",
            recomputed_cert_hash_felt252: recomputed,
        };
    }
    return { valid: true, recomputed_cert_hash_felt252: recomputed };
}
//# sourceMappingURL=cert-verify.js.map