// ══════════════════════════════════════════════════════════════════
// Tool Integrity Verification — OWASP Layer 2
// ══════════════════════════════════════════════════════════════════
// Source: OWASP#802 four-layer integrity taxonomy
// Layer 1: Transmission integrity (TLS) ✅
// Layer 2: Tool integrity (THIS MODULE) — tool invoked = tool approved
// Layer 3: Authorization-at-execution (ExecutionAttestation) ✅
// Layer 4: Intent integrity (ActionIntent) ✅
//
// Prevents tool-swap attacks: agent authorized to call `web_search`
// but the tool registry is swapped to a malicious implementation.
// ══════════════════════════════════════════════════════════════════
import { createHash } from 'node:crypto';
import { sign, verify } from '../crypto/keys.js';
import { canonicalize } from './canonical.js';
/**
 * Register a tool in the integrity registry.
 * The runtime/attestor signs that this tool implementation is known and approved.
 */
export function createToolRegistryEntry(input) {
    const implHash = createHash('sha256')
        .update(input.implementation)
        .digest('hex');
    const now = new Date().toISOString();
    const body = {
        toolName: input.toolName,
        implementationHash: `sha256:${implHash}`,
        attestorId: input.attestorId,
        verifiedAt: now,
    };
    const signature = sign(canonicalize(body), input.attestorPrivateKey);
    return { ...body, signature };
}
/**
 * Verify tool integrity: is this the same tool that was approved?
 * Also checks per-invocation requirements if provided.
 */
export function verifyToolIntegrity(input) {
    const errors = [];
    const failedRequirements = [];
    // 1. Verify attestor signature
    const { signature, ...body } = input.registryEntry;
    const attestorSignatureValid = verify(canonicalize(body), signature, input.attestorPublicKey);
    if (!attestorSignatureValid)
        errors.push('Tool attestor signature invalid');
    // 2. Verify implementation hash matches
    const currentHash = `sha256:${createHash('sha256')
        .update(input.currentImplementation)
        .digest('hex')}`;
    const implementationVerified = currentHash === input.registryEntry.implementationHash;
    if (!implementationVerified) {
        errors.push(`Tool implementation hash mismatch: expected ${input.registryEntry.implementationHash}, got ${currentHash}`);
    }
    // 3. Check per-invocation requirements (if tool declares them)
    let requirementsMet = true;
    if (input.requirements && input.agentCapabilities) {
        const reqs = input.requirements;
        const caps = input.agentCapabilities;
        if (reqs.minGrade !== undefined && caps.grade < reqs.minGrade) {
            failedRequirements.push(`Grade ${caps.grade} < required ${reqs.minGrade}`);
            requirementsMet = false;
        }
        if (reqs.minTrustScore !== undefined && caps.trustScore < reqs.minTrustScore) {
            failedRequirements.push(`Trust score ${caps.trustScore} < required ${reqs.minTrustScore}`);
            requirementsMet = false;
        }
        if (reqs.requiredScopes) {
            const missing = reqs.requiredScopes.filter(s => !caps.scopes.includes(s));
            if (missing.length > 0) {
                failedRequirements.push(`Missing scopes: ${missing.join(', ')}`);
                requirementsMet = false;
            }
        }
        if (reqs.requiresWallet && !caps.hasWallet) {
            failedRequirements.push('Tool requires wallet but agent has none');
            requirementsMet = false;
        }
    }
    if (!requirementsMet)
        errors.push(`Agent does not meet tool requirements: ${failedRequirements.join('; ')}`);
    return {
        valid: errors.length === 0,
        implementationVerified,
        attestorSignatureValid,
        requirementsMet,
        failedRequirements,
        errors,
    };
}
// ══════════════════════════════════════════════════════════════════
// Tool Registry & Discovery Integrity — signed manifests, publisher
// identity verification, namespace governance, metadata-change reapproval.
//
// Maps to CoSAI control `controlToolRegistryandDiscoveryIntegrity`
// (cosai-oasis/secure-ai-tooling#162).
//
// `ToolManifest` is the single canonical surface for this work. The
// `ToolRegistryEntry` above is frozen legacy — kept backward-compatible,
// no new fields, no new behaviour. Every new check (publisher, namespace,
// re-approval) lives on the manifest path.
// ══════════════════════════════════════════════════════════════════
import { fromDIDKey, resolveDIDWeb } from './did-interop.js';
const SHA256 = (data) => 'sha256:' + createHash('sha256').update(data).digest('hex');
/** Hash a tool implementation — string as utf-8 bytes, Buffer as raw bytes. */
function hashImplementation(impl) {
    return SHA256(impl);
}
/** Hash the canonicalized metadata block. */
function hashMetadata(meta) {
    return SHA256(canonicalize(meta));
}
/** Canonical signing body — the manifest minus BOTH signatures. The attestor
 *  and the publisher sign exactly this string. */
function canonicalManifestBody(m) {
    const { signature: _s, publisherSignature: _p, ...body } = m;
    return canonicalize(body);
}
/**
 * Resolve a publisher/owner DID to an Ed25519 public key (hex) via its
 * trust root (D1). Never throws — failures return `publicKey: null` with a
 * reason so the caller can record `publisherVerified: false`.
 */
async function resolveTrustRootKey(did, trustRoot, opts) {
    // Default: APS-native DID — self-certifying, zero external dependency.
    const root = trustRoot ?? { type: 'aps', ref: did };
    if (root.type === 'aps') {
        // did:key / did:aps carry the Ed25519 key in the identifier itself.
        try {
            const asKey = did.startsWith('did:aps:')
                ? 'did:key:' + did.slice('did:aps:'.length)
                : did;
            return { publicKey: fromDIDKey(asKey), method: 'aps-native-did' };
        }
        catch (e) {
            return { publicKey: null, method: 'aps-native-did', error: e instanceof Error ? e.message : String(e) };
        }
    }
    if (root.type === 'raw-key') {
        // TRUST ASSUMPTION (external root): the caller asserts that `root.ref` is
        // the publisher's Ed25519 public key. APS verifies the signature against
        // this key but does NOT verify any chain proving the key belongs to the
        // named DID — that binding is the caller's to vouch for. The raw key
        // resolves a publisher identity, nothing more.
        const key = root.ref.trim().toLowerCase();
        if (!/^[0-9a-f]{64}$/.test(key)) {
            return { publicKey: null, method: 'raw-key', error: 'raw-key trust root is not a 32-byte hex Ed25519 key' };
        }
        return { publicKey: key, method: 'raw-key' };
    }
    // did:web — external root anchored on domain control. Native fetch only;
    // no fetch polyfill is ever added (D4).
    const resolver = opts?.didWebResolver;
    if (!resolver && typeof fetch !== 'function') {
        return { publicKey: null, method: 'did:web', error: 'did_web_resolution_unavailable' };
    }
    try {
        const doc = await (resolver ? resolver(root.ref) : resolveDIDWeb(root.ref));
        const key = firstEd25519Key(doc);
        if (!key)
            return { publicKey: null, method: 'did:web', error: 'no Ed25519 verification method in did:web document' };
        return { publicKey: key, method: 'did:web' };
    }
    catch (e) {
        return { publicKey: null, method: 'did:web', error: e instanceof Error ? e.message : String(e) };
    }
}
/** Minimal extractor — the first Ed25519 verification method in a DID doc. */
function firstEd25519Key(doc) {
    const vms = doc?.verificationMethod;
    if (!Array.isArray(vms))
        return null;
    for (const vm of vms) {
        if (!vm || typeof vm !== 'object')
            continue;
        const v = vm;
        const type = String(v.type ?? '');
        const mb = typeof v.publicKeyMultibase === 'string' ? v.publicKeyMultibase : '';
        const isEd25519 = type.includes('Ed25519') || mb.startsWith('z6Mk');
        if (!isEd25519)
            continue;
        if (typeof v.publicKeyHex === 'string' && /^[0-9a-fA-F]{64}$/.test(v.publicKeyHex)) {
            return v.publicKeyHex.toLowerCase();
        }
        if (mb.startsWith('z')) {
            try {
                return fromDIDKey('did:key:' + mb);
            }
            catch { /* try next */ }
        }
    }
    return null;
}
/** Namespace match. `acme/*` covers `acme/<anything>`; a bare `acme` covers
 *  `acme` and `acme/<anything>`. Mechanical prefix rule — no globbing beyond
 *  a trailing `/*`. */
function toolNameUnderNamespace(toolName, namespace) {
    if (namespace.endsWith('/*')) {
        return toolName.startsWith(namespace.slice(0, -1));
    }
    return toolName === namespace || toolName.startsWith(namespace + '/');
}
/**
 * Create a signed tool manifest. The attestor signs the canonical body; if a
 * publisher private key is supplied, the publisher co-signs the same body.
 */
export function createToolManifest(input) {
    const body = {
        toolName: input.toolName,
        namespace: input.namespace,
        implementationHash: hashImplementation(input.implementation),
        metadataHash: hashMetadata(input.metadata),
        publisherDid: input.publisherDid,
        trustRoot: input.trustRoot,
        attestorDid: input.attestorDid,
        attestorTrustRoot: input.attestorTrustRoot,
        metadataVersion: input.metadataVersion ?? 1,
        approvalState: input.approvalState ?? 'approved',
        verifiedAt: input.verifiedAt ?? new Date().toISOString(),
    };
    const canon = canonicalize(body);
    const signature = sign(canon, input.attestorPrivateKey);
    const publisherSignature = input.publisherPrivateKey
        ? sign(canon, input.publisherPrivateKey)
        : undefined;
    return { ...body, signature, publisherSignature };
}
/**
 * Verify a tool manifest — attestor signature, optional implementation and
 * metadata hashes, publisher identity (Part 1b), namespace governance
 * (Part 2) and re-approval state (Part 3). Async because did:web resolution
 * is async; the APS-native and raw-key paths resolve synchronously.
 */
export async function verifyToolManifest(input) {
    const m = input.manifest;
    const errors = [];
    const canon = canonicalManifestBody(m);
    // 1. Attestor signature. When the manifest asserts an `attestorDid`, the
    //    RESOLVED attestor key is authoritative (G1) — a caller-supplied
    //    `attestorPublicKey` cannot override or substitute for it, which is the
    //    whole point of binding. Without an `attestorDid`, the pass-1 behaviour
    //    holds: verify against the caller-supplied key.
    let attestorSignatureValid = false;
    let attestorVerified = false;
    let attestorResolutionMethod = 'caller-supplied-key';
    if (m.attestorDid !== undefined) {
        const resolved = await resolveTrustRootKey(m.attestorDid, m.attestorTrustRoot, {
            didWebResolver: input.didWebResolver,
        });
        attestorResolutionMethod = resolved.method;
        if (!resolved.publicKey) {
            // External trust roots can fail to resolve — a clean fail, never a throw.
            errors.push(`Attestor key resolution failed: ${resolved.error ?? 'unknown'}`);
        }
        else {
            attestorSignatureValid = verify(canon, m.signature, resolved.publicKey);
            attestorVerified = attestorSignatureValid;
            if (!attestorSignatureValid) {
                errors.push('Tool manifest attestor signature invalid (resolved attestor key)');
            }
        }
    }
    else if (input.attestorPublicKey !== undefined) {
        attestorSignatureValid = verify(canon, m.signature, input.attestorPublicKey);
        if (!attestorSignatureValid)
            errors.push('Tool manifest attestor signature invalid');
    }
    else {
        errors.push('No attestorDid on manifest and no attestorPublicKey supplied');
    }
    // 2. Implementation hash — only when a current implementation is supplied.
    let implementationVerified = true;
    if (input.currentImplementation !== undefined) {
        implementationVerified = hashImplementation(input.currentImplementation) === m.implementationHash;
        if (!implementationVerified) {
            errors.push(`Tool implementation hash mismatch: expected ${m.implementationHash}`);
        }
    }
    // 3. Metadata hash — only when current metadata is supplied.
    let metadataVerified = true;
    if (input.currentMetadata !== undefined) {
        metadataVerified = hashMetadata(input.currentMetadata) === m.metadataHash;
        if (!metadataVerified) {
            errors.push(`Tool metadata hash mismatch: expected ${m.metadataHash}`);
        }
    }
    // 4. Publisher identity (Part 1b) — runs only when a publisherDid is asserted.
    let publisherVerified = false;
    let publisherResolutionMethod = 'not-asserted';
    if (m.publisherDid !== undefined) {
        if (!m.publisherSignature) {
            publisherResolutionMethod = 'no-publisher-signature';
            errors.push('Publisher DID asserted but no publisherSignature present');
        }
        else {
            const resolved = await resolveTrustRootKey(m.publisherDid, m.trustRoot, {
                didWebResolver: input.didWebResolver,
            });
            publisherResolutionMethod = resolved.method;
            if (!resolved.publicKey) {
                errors.push(`Publisher key resolution failed: ${resolved.error ?? 'unknown'}`);
            }
            else {
                publisherVerified = verify(canon, m.publisherSignature, resolved.publicKey);
                if (!publisherVerified)
                    errors.push('Publisher signature invalid');
            }
        }
    }
    // 5. Namespace governance (Part 2) — anti-typosquat / anti-shadowing.
    //    Open by default: claims are opt-in, a name under no claim passes.
    let namespaceVerified = true;
    let namespaceViolation = false;
    for (const claim of input.namespaceClaims ?? []) {
        if (!toolNameUnderNamespace(m.toolName, claim.namespace))
            continue;
        if (m.publisherDid !== claim.ownerDid) {
            namespaceViolation = true;
            namespaceVerified = false;
            errors.push(`Namespace violation: tool "${m.toolName}" falls under claimed namespace ` +
                `"${claim.namespace}" owned by ${claim.ownerDid}, but manifest publisherDid ` +
                `is ${m.publisherDid ?? '(none)'}`);
        }
    }
    // 6. Re-approval (Part 3) — a manifest pending re-approval is not valid.
    const reapprovalRequired = m.approvalState === 'pending-reapproval';
    if (reapprovalRequired)
        errors.push('tool metadata changed, awaiting re-approval');
    return {
        valid: errors.length === 0,
        attestorSignatureValid,
        attestorVerified,
        attestorResolutionMethod,
        implementationVerified,
        metadataVerified,
        publisherVerified,
        publisherResolutionMethod,
        namespaceVerified,
        namespaceViolation,
        reapprovalRequired,
        errors,
    };
}
/** Create a signed namespace ownership claim. */
export function createNamespaceClaim(input) {
    const canon = canonicalize({ namespace: input.namespace, ownerDid: input.ownerDid });
    return {
        namespace: input.namespace,
        ownerDid: input.ownerDid,
        trustRoot: input.trustRoot,
        signature: sign(canon, input.ownerPrivateKey),
    };
}
/**
 * Verify a namespace claim — resolve the owner key via the claim's own trust
 * root and check the owner signature over canonical `{namespace, ownerDid}`.
 * `resolveOpts` carries an optional injected did:web resolver.
 */
export async function verifyNamespaceClaim(claim, resolveOpts) {
    const errors = [];
    const canon = canonicalize({ namespace: claim.namespace, ownerDid: claim.ownerDid });
    const resolved = await resolveTrustRootKey(claim.ownerDid, claim.trustRoot, resolveOpts);
    let ownerVerified = false;
    if (!resolved.publicKey) {
        errors.push(`Namespace owner key resolution failed: ${resolved.error ?? 'unknown'}`);
    }
    else {
        ownerVerified = verify(canon, claim.signature, resolved.publicKey);
        if (!ownerVerified)
            errors.push('Namespace claim owner signature invalid');
    }
    return { valid: errors.length === 0, ownerVerified, resolutionMethod: resolved.method, errors };
}
/**
 * Revise a tool manifest (Part 3). Produces a new manifest re-signed by the
 * attestor. Hash-delta rule — the concrete link between Part 1a and Part 3:
 * the revision moves to `pending-reapproval` with `metadataVersion + 1` IFF
 * the implementation hash OR the metadata hash differs from the previous
 * manifest. If neither hash changed it is not a substantive revision and the
 * version / approval state are unchanged.
 *
 * The attestor identity (`attestorDid` / `attestorTrustRoot`) is carried
 * forward. If the previous manifest asserts a `publisherDid`, a
 * `publisherPrivateKey` MUST be supplied so the publisher field is re-signed
 * over the revised body — revising a publisher-bearing manifest without it
 * throws rather than emit a manifest with a stale publisher signature.
 */
export function reviseToolManifest(prevManifest, changes, attestorPrivateKey, opts) {
    if (prevManifest.publisherDid !== undefined && !opts?.publisherPrivateKey) {
        throw new Error('reviseToolManifest: this manifest asserts a publisherDid; a ' +
            'publisherPrivateKey is required to re-sign the publisher field over ' +
            'the revised body');
    }
    const implementationHash = changes.implementation !== undefined
        ? hashImplementation(changes.implementation)
        : prevManifest.implementationHash;
    const metadataHash = changes.metadata !== undefined
        ? hashMetadata(changes.metadata)
        : prevManifest.metadataHash;
    const substantive = implementationHash !== prevManifest.implementationHash ||
        metadataHash !== prevManifest.metadataHash;
    const body = {
        toolName: prevManifest.toolName,
        namespace: prevManifest.namespace,
        implementationHash,
        metadataHash,
        publisherDid: prevManifest.publisherDid,
        trustRoot: prevManifest.trustRoot,
        attestorDid: prevManifest.attestorDid,
        attestorTrustRoot: prevManifest.attestorTrustRoot,
        metadataVersion: substantive ? prevManifest.metadataVersion + 1 : prevManifest.metadataVersion,
        approvalState: substantive ? 'pending-reapproval' : prevManifest.approvalState,
        verifiedAt: opts?.verifiedAt ?? new Date().toISOString(),
    };
    const canon = canonicalize(body);
    const publisherSignature = prevManifest.publisherDid !== undefined
        ? sign(canon, opts.publisherPrivateKey)
        : undefined;
    return { ...body, signature: sign(canon, attestorPrivateKey), publisherSignature };
}
/**
 * Re-approve a manifest pending re-approval (Part 3). Only an attestor can move
 * `pending-reapproval` -> `approved`.
 *
 * The approval is bound to a resolved attestor identity, not merely any
 * caller-provided keypair: the returned manifest carries `attestorDid` /
 * `attestorTrustRoot`, and its signature then verifies against the resolved
 * attestor key in `verifyToolManifest`. If `opts.attestorDid` is omitted the
 * attestor identity already on the manifest is carried forward.
 *
 * If the manifest asserts a `publisherDid`, a `publisherPrivateKey` is required
 * so the publisher field is re-signed over the approved body. Throws if the
 * manifest is not pending re-approval, or on a missing required publisher key.
 */
export function reapproveToolManifest(manifest, opts) {
    if (manifest.approvalState !== 'pending-reapproval') {
        throw new Error('reapproveToolManifest: manifest is not pending re-approval');
    }
    if (manifest.publisherDid !== undefined && !opts.publisherPrivateKey) {
        throw new Error('reapproveToolManifest: this manifest asserts a publisherDid; a ' +
            'publisherPrivateKey is required to re-sign the publisher field over ' +
            'the approved body');
    }
    const { signature: _s, publisherSignature: _p, ...rest } = manifest;
    const body = {
        ...rest,
        attestorDid: opts.attestorDid ?? manifest.attestorDid,
        attestorTrustRoot: opts.attestorTrustRoot ?? manifest.attestorTrustRoot,
        approvalState: 'approved',
        verifiedAt: opts.verifiedAt ?? new Date().toISOString(),
    };
    const canon = canonicalize(body);
    const publisherSignature = manifest.publisherDid !== undefined
        ? sign(canon, opts.publisherPrivateKey)
        : undefined;
    return { ...body, signature: sign(canon, opts.attestorPrivateKey), publisherSignature };
}
//# sourceMappingURL=tool-integrity.js.map