/**
 * ProvenanceSigner -- P4-AIR-03 (EU AI Act Art. 50)
 *
 * Signs AI-generated narrative report bodies with AI-provenance metadata.
 * Produces a signed envelope that auditors can independently verify to
 * confirm the report was generated by our runtime with the declared model.
 *
 * Signed envelope fields:
 *   - contentHash: SHA-256 of the narrative body string
 *   - art50Disclosure: EU AI Act Art. 50 metadata block
 *   - packBindings: compliance packs that were in force at generation time
 *   - chainHeadHash: audit-chain tip hash at generate time
 *   - signedAt: ISO-8601 timestamp
 *   - signature: HMAC-SHA256 over canonical JSON of all above fields
 *
 * Security:
 *   - Signing key must be AI_PROVENANCE_HMAC_KEY env var (>= 32 chars).
 *   - AI_PROVENANCE_HMAC_KEY must be distinct from all other boot secrets
 *     (enforced at boot by the distinct-secret boot gate).
 *   - Canonical JSON sorts keys alphabetically before signing to prevent
 *     field-reordering attacks.
 *
 * Spec: P4-AIR-03 AC 7-9
 *
 * @connexum/ai-governance
 */
import * as crypto from 'node:crypto';
const SDK_VERSION = '1.0.0-beta.1';
// ---------------------------------------------------------------------------
// Boot-gate check (exported so the boot gate can call it)
// ---------------------------------------------------------------------------
/**
 * Validate the AI_PROVENANCE_HMAC_KEY at boot time.
 *
 * Requirements:
 *   - Must be present (non-empty).
 *   - Must be >= 32 characters.
 *   - Must be distinct from the other 5 named boot secrets.
 *
 * Throws on any violation (fail-closed).
 */
export function validateProvenanceHmacKey(provenanceKey, otherSecrets = {}) {
    if (!provenanceKey) {
        throw new Error('SECURITY: AI_PROVENANCE_HMAC_KEY is not set. ' +
            'This key is required to sign AI-provenance envelopes (P4-AIR-03 / EU AI Act Art. 50). ' +
            'Set AI_PROVENANCE_HMAC_KEY in the environment (32+ chars, distinct from ' +
            'TRUST_ANCHOR_HMAC, ENTITLEMENT_SIGNING_KEY, JWT_SECRET, ENCRYPTION_KEY, APPROVAL_SECRET).');
    }
    if (provenanceKey.length < 32) {
        throw new Error('SECURITY: AI_PROVENANCE_HMAC_KEY must be at least 32 characters. ' +
            'Shorter secrets are trivially brute-forceable.');
    }
    for (const [name, value] of Object.entries(otherSecrets)) {
        if (value && provenanceKey === value) {
            throw new Error(`SECURITY: AI_PROVENANCE_HMAC_KEY must not equal ${name}. ` +
                'Shared signing key defeats separation of AI-provenance attestation from other security controls.');
        }
    }
}
// ---------------------------------------------------------------------------
// Canonical JSON
// ---------------------------------------------------------------------------
/**
 * Error thrown by canonicalJson when homoglyph duplicate keys are detected.
 *
 * H9d: An object whose keys collide after NFKC normalization is a tamper
 * attempt. The canonical form would silently drop one of the values.
 * We throw instead so callers can emit an audit event and fail closed.
 *
 * Example: { "\u0041dmin": 1, "Admin": 2 } -- '\u0041' is 'A' after NFKC,
 * so both keys normalize to "Admin". The second entry would shadow the first.
 */
export class HomoglyphKeyError extends Error {
    /** Object path where the collision was detected (dot-notation) */
    path;
    /** The two raw keys that collide after NFKC normalization */
    collidingKeys;
    /** The shared NFKC-normalized form */
    normalizedKey;
    constructor(path, key1, key2, normalizedKey) {
        super(`canonicalJson: homoglyph duplicate key detected at "${path}": ` +
            `"${key1}" and "${key2}" both normalize to "${normalizedKey}" under NFKC. ` +
            'This is a tamper attempt. Refusing to produce canonical form.');
        this.name = 'HomoglyphKeyError';
        this.path = path;
        this.collidingKeys = [key1, key2];
        this.normalizedKey = normalizedKey;
    }
}
/**
 * Produce a canonical (sorted-keys, deterministic) JSON string.
 * Prevents field-reordering attacks on the signature.
 *
 * H4 fix: arrays are now handled deterministically:
 *   - Arrays of objects: each element has its keys sorted recursively.
 *   - Arrays of primitives: elements are sorted lexicographically so that
 *     ["b","a"] and ["a","b"] produce the same canonical form.
 *   - Mixed arrays: objects are recursed, primitives are sorted by
 *     their JSON serialization.
 *
 * H9d fix: object keys are NFKC-normalized before sorting. If two keys in
 * the same object normalize to the same NFKC form (homoglyph attack),
 * HomoglyphKeyError is thrown. Callers must catch this and emit an audit
 * event (canonicalJson.homoglyph-key-rejected) before failing closed.
 */
export function canonicalJson(obj) {
    return JSON.stringify(_canonicalize(obj, ''));
}
function _canonicalize(val, path) {
    if (val === null || val === undefined)
        return val;
    if (Array.isArray(val)) {
        // Recurse into each element first
        const recurse = val.map((el, i) => _canonicalize(el, `${path}[${i}]`));
        // Sort the array for determinism:
        //   - All elements serialized to string for stable comparison
        return recurse.sort((a, b) => {
            const sa = JSON.stringify(a) ?? '';
            const sb = JSON.stringify(b) ?? '';
            return sa < sb ? -1 : sa > sb ? 1 : 0;
        });
    }
    if (typeof val === 'object') {
        const rawKeys = Object.keys(val);
        // H9d: Normalize keys with NFKC and detect homoglyph collisions
        // before sorting. A collision means two different raw keys map to
        // the same normalized form -- a tamper attempt.
        const normalizedMap = new Map(); // normalizedKey -> first rawKey
        for (const rawKey of rawKeys) {
            const normalized = rawKey.normalize('NFKC');
            if (normalizedMap.has(normalized)) {
                const firstKey = normalizedMap.get(normalized);
                const currentPath = path ? `${path}.${rawKey}` : rawKey;
                throw new HomoglyphKeyError(currentPath, firstKey, rawKey, normalized);
            }
            normalizedMap.set(normalized, rawKey);
        }
        // Sort by NFKC-normalized key for consistent ordering
        const sortedRawKeys = rawKeys.sort((a, b) => {
            const na = a.normalize('NFKC');
            const nb = b.normalize('NFKC');
            return na < nb ? -1 : na > nb ? 1 : 0;
        });
        const sorted = {};
        for (const rawKey of sortedRawKeys) {
            const normalizedKey = rawKey.normalize('NFKC');
            const childPath = path ? `${path}.${normalizedKey}` : normalizedKey;
            sorted[normalizedKey] = _canonicalize(val[rawKey], childPath);
        }
        return sorted;
    }
    return val;
}
// ---------------------------------------------------------------------------
// SHA-256 helpers
// ---------------------------------------------------------------------------
function sha256(input) {
    return crypto.createHash('sha256').update(input, 'utf-8').digest('hex');
}
function hmacSha256(payload, key) {
    return crypto.createHmac('sha256', key).update(payload, 'utf-8').digest('hex');
}
// ---------------------------------------------------------------------------
// Sign
// ---------------------------------------------------------------------------
/**
 * Sign a narrative body and produce a ProvenanceEnvelope.
 *
 * The envelope can be attached to a report response so external auditors
 * can verify it with verifyProvenance().
 *
 * @throws Error if hmacKey is missing or shorter than 32 characters.
 */
export function signProvenance(input) {
    const key = input.hmacKey ?? process.env['AI_PROVENANCE_HMAC_KEY'] ?? '';
    if (!key || key.length < 32) {
        throw new Error('AI_PROVENANCE_HMAC_KEY is missing or too short (must be >= 32 chars). ' +
            'Cannot sign AI-provenance envelope.');
    }
    const contentHash = sha256(input.narrativeBody);
    const signedAt = new Date().toISOString();
    const art50Disclosure = {
        wasLlmGenerated: true,
        model: input.modelId,
        modelProvider: input.modelProvider,
        generatedAt: input.generatedAt,
        governanceRuntime: `@connexum/ai-governance@${SDK_VERSION}`,
    };
    // Build the unsigned envelope (all fields except signature)
    const unsigned = {
        contentHash,
        art50Disclosure,
        packBindings: [...input.packBindings].sort(), // deterministic order
        chainHeadHash: input.chainHeadHash,
        signedAt,
    };
    const canonical = canonicalJson(unsigned);
    const signature = hmacSha256(canonical, key);
    return { ...unsigned, signature };
}
// ---------------------------------------------------------------------------
// Verify
// ---------------------------------------------------------------------------
/**
 * Verify a ProvenanceEnvelope produced by signProvenance().
 *
 * Checks:
 *   1. All required fields are present.
 *   2. The HMAC signature is valid over the canonical JSON of the non-sig fields.
 *
 * Does NOT re-hash the narrative body against contentHash -- the caller
 * should separately verify contentHash === SHA-256(narrativeBody) if they
 * have the body available.
 *
 * Returns { valid: true } on success, { valid: false, reason } on failure.
 */
export function verifyProvenance(envelope, hmacKey) {
    // Check required fields
    const requiredFields = [
        'contentHash',
        'art50Disclosure',
        'packBindings',
        'chainHeadHash',
        'signedAt',
        'signature',
    ];
    for (const field of requiredFields) {
        if (envelope[field] === undefined || envelope[field] === null) {
            return { valid: false, reason: `Missing required field: ${field}` };
        }
    }
    if (!envelope.art50Disclosure.wasLlmGenerated) {
        return { valid: false, reason: 'art50Disclosure.wasLlmGenerated must be true' };
    }
    if (!hmacKey || hmacKey.length < 32) {
        return { valid: false, reason: 'Verification key is missing or too short' };
    }
    // Reconstruct the unsigned portion and recompute signature
    const { signature: _sig, ...unsigned } = envelope;
    const canonical = canonicalJson(unsigned);
    const expected = hmacSha256(canonical, hmacKey);
    try {
        const sigBuf = Buffer.from(_sig, 'hex');
        const expBuf = Buffer.from(expected, 'hex');
        if (sigBuf.length !== expBuf.length) {
            return { valid: false, reason: 'Signature length mismatch' };
        }
        if (!crypto.timingSafeEqual(sigBuf, expBuf)) {
            return { valid: false, reason: 'Signature mismatch -- envelope may have been tampered with' };
        }
    }
    catch {
        return { valid: false, reason: 'Signature comparison failed (malformed hex)' };
    }
    return { valid: true };
}
/**
 * Verify that the contentHash in an envelope matches the actual body.
 *
 * This is a separate check so callers can verify both the envelope
 * signature AND the body integrity in two steps.
 */
export function verifyContentHash(envelope, narrativeBody) {
    const actual = sha256(narrativeBody);
    if (actual !== envelope.contentHash) {
        return {
            valid: false,
            reason: `Content hash mismatch: expected ${envelope.contentHash}, got ${actual}`,
        };
    }
    return { valid: true };
}
//# sourceMappingURL=provenance-signer.js.map