Files
MikrotikManager/backend/src/services/server-probe-legacy-verifier.test.ts
T

235 lines
16 KiB
TypeScript

import assert from "node:assert/strict"
import { createHash, randomUUID } from "node:crypto"
import { mkdtemp, readdir, rm, rmdir } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import type { ServerProbeExecutionSource } from "../modules/mcp/diagnostic-cancellation-source.js"
import { fingerprint } from "../modules/mcp/security.js"
import type { ServerProbeJournalContext } from "./server-probe-journal.js"
import type { ServerProbeReceipt } from "./server-probe-execution.js"
import { createLegacyServerProbeVerifier, createLegacyServerProbeVerifierForTests, type LegacyServerProbeBundle, type LegacyServerProbeVerifierCrypto } from "./server-probe-legacy-verifier.js"
const secret = "PRIVATE_LEGACY_PROVIDER_VALUE"
const stamp = new Date("2020-01-01T00:00:00Z")
async function fixture(kind: "rest" | "mcp" = "rest", terminal = true) {
const executionId = randomUUID(), receiptId = randomUUID(), ciphertexts = new Map<string, string>()
const context: ServerProbeJournalContext = { identity: kind === "rest"
? { kind, ownerId: "original-owner", executionId, issuedAt: 1, expiresAt: stamp.getTime(), requestId: randomUUID(), authentication: "jwt" }
: { kind, ownerId: "original-owner", executionId, issuedAt: 1, expiresAt: stamp.getTime(), connectionId: randomUUID(), credentialId: randomUUID(), operationName: "servers.probes.run" }, sourceServerId: 9, tool: "ping" }
const scope = { ownerId: context.identity.ownerId, receiptId, identityKind: kind, executionId, bindingProof: "a".repeat(64), authorityExpiresAt: stamp.toISOString() }
const source: ServerProbeExecutionSource = { scope, sourceDigest: "d".repeat(64), sourceState: "unknown", terminal: true,
serverIds: [9], stageBudget: 2, originalCredentialId: context.identity.kind === "mcp" ? context.identity.credentialId : randomUUID(),
originalContext: context as ServerProbeExecutionSource["originalContext"] }
const seenProofs: { domain: string; isString: boolean }[] = []
const crypto: LegacyServerProbeVerifierCrypto = {
open: async value => { const opened = ciphertexts.get(value); if (!opened) throw Error(secret); return opened },
proof: async (domain, value) => { seenProofs.push({ domain, isString: typeof value === "string" }); return createHash("sha256").update(domain + fingerprint(value)).digest("hex") },
}
const receipt: ServerProbeReceipt = { receiptId, tool: "ping", serverId: 9, state: "completed", httpStatus: 200,
output: secret, data: { kind: "ping" }, attemptedStages: 1, completedStages: 1, automaticReplay: false, forwardingVerified: false }
const id = context.identity
const anchor: Record<string, unknown> = { receipt_id: receiptId, binding_proof: scope.bindingProof, identity_kind: kind, execution_id: executionId,
actor_id: id.ownerId, authentication: id.kind === "mcp" ? "mcp" : id.authentication,
mcp_connection_id: id.kind === "mcp" ? id.connectionId : null, mcp_credential_id: id.kind === "mcp" ? id.credentialId : null,
request_id: id.kind === "rest" ? id.requestId : null, authority_issued_at: String(id.issuedAt), authority_expires_at: stamp,
source_server_id: 9, destination_server_id: null, tool: "ping", state: terminal ? "completed" : "claimed",
attempted_stages: terminal ? 1 : null, completed_stages: terminal ? 1 : null, receipt_ciphertext: null, receipt_proof: null,
created_at: stamp, finished_at: terminal ? stamp : null }
const stage: Record<string, unknown> = { receipt_id: receiptId, stage_index: 1, kind: "native-ping", timeout_ms: 1000,
state: "received", outcome: "received", created_at: stamp, received_at: stamp }
const bundle: LegacyServerProbeBundle = { identityKind: kind, executionId, anchor, stages: terminal ? [stage] : [] }
let loads = 0
const deps = { loadMcpSource: async (operationId: string) => { loads++; assert.equal(operationId, executionId); return source } }
const verify = createLegacyServerProbeVerifierForTests(deps, crypto)
const refreshReceipt = async (rawContext: unknown = context) => {
const plaintext = JSON.stringify({ version: 1, context: rawContext, bindingProof: scope.bindingProof, receipt })
const cipher = "fixture-cipher-" + randomUUID(); ciphertexts.set(cipher, plaintext)
anchor.receipt_ciphertext = cipher; anchor.receipt_proof = await crypto.proof("receipt", plaintext)
}
const quarantine = async () => {
bundle.quarantine = { receipt_id: receiptId, binding_proof: scope.bindingProof, identity_kind: kind, execution_id: executionId,
identity_proof: await crypto.proof("identity", context), error_code: "JOURNAL_UNKNOWN", attempted_stages: terminal ? 1 : 0,
completed_stages: terminal ? 1 : 0, created_at: stamp }
}
if (terminal) await refreshReceipt()
return { verify, deps, crypto, seenProofs, source, context, scope, bundle, receipt, anchor, stage, ciphertexts, refreshReceipt, quarantine, loads: () => loads }
}
let checks = 0
for (const kind of ["rest", "mcp"] as const) {
const f = await fixture(kind), result = await f.verify(f.bundle)
assert.equal(result.status, "verified")
if (result.status !== "verified") throw Error("Expected authenticated receipt")
assert.equal(result.authentication, "receipt"); assert.equal(result.bindingAuthenticated, true); assert.equal(result.progressAuthenticated, true)
assert.equal(result.migrationReady, false); assert.equal(result.stageLedgerAuthenticated, false); assert.equal(result.timestampsAuthenticated, false)
assert.equal(result.originalContext.identity.expiresAt, stamp.getTime()); assert.equal(result.stageCount, 1)
assert.equal(result.requiresDatabaseSourceAttestation, true); assert.equal(result.automaticReplay, false)
assert.equal(JSON.stringify(result).includes(secret), false)
assert.equal(JSON.stringify(result).includes("fixture-cipher-"), false)
assert(Object.isFrozen(result) && Object.isFrozen(result.originalContext) && Object.isFrozen(result.originalContext.identity) && Object.isFrozen(result.scope))
assert(f.seenProofs.filter(value => value.domain === "receipt").every(value => value.isString))
assert.equal(f.loads(), kind === "mcp" ? 1 : 0); checks++
}
{
const f = await fixture("mcp", false), result = await f.verify(f.bundle)
assert.equal(result.status, "verified")
if (result.status === "verified") { assert.equal(result.authentication, "approved-source"); assert.equal(result.progressAuthenticated, false) }
checks++
}
{
const f = await fixture("rest", false)
assert.equal((await f.verify(f.bundle)).status, "unresolved")
await f.quarantine()
const result = await f.verify(f.bundle)
assert.equal(result.status, "verified")
if (result.status === "verified") {
assert.equal(result.authentication, "identity-context-only"); assert.equal(result.bindingAuthenticated, false)
assert.equal(result.progressAuthenticated, false); assert.equal(result.quarantined, true)
}
assert(f.seenProofs.filter(value => value.domain === "identity").every(value => !value.isString))
// Identity MAC deliberately does not authenticate the binding or timestamp.
f.anchor.binding_proof = "b".repeat(64); f.bundle.quarantine!.binding_proof = "b".repeat(64)
f.bundle.quarantine!.created_at = new Date("2021-01-01T00:00:00Z")
const changed = await f.verify(f.bundle)
assert.equal(changed.status, "verified")
if (changed.status === "verified") { assert.equal(changed.bindingAuthenticated, false); assert.equal(changed.timestampsAuthenticated, false); assert.equal(changed.migrationReady, false) }
delete f.bundle.anchor; assert.equal((await f.verify(f.bundle)).status, "unresolved"); checks++
}
for (const orphan of ["quarantine", "cancellation"] as const) {
const f = await fixture("mcp", false)
if (orphan === "quarantine") await f.quarantine()
else {
const id = f.context.identity; assert(id.kind === "mcp")
f.bundle.cancellation = { operation_id: id.executionId, connection_id: id.connectionId, actor_id: id.ownerId }
}
delete f.bundle.anchor
const result = await f.verify(f.bundle); assert.equal(result.status, "verified")
if (result.status === "verified") { assert.equal(result.hasAnchor, false); assert.equal(result.authentication, "approved-source"); assert.equal(result.migrationReady, false) }
checks++
}
{
const f = await fixture("mcp", false); await f.quarantine(); delete f.bundle.anchor
f.bundle.quarantine!.attempted_stages = 1
// The sealed source authenticates context, but an absent anchor/stage ledger
// cannot substantiate nonzero orphan progress. Unresolved is insufficient
// evidence for transfer, not a claim that historical data was corrupted.
const result = await f.verify(f.bundle)
assert.deepEqual(result, { status: "unresolved", reasonCode: "QUARANTINE_INVALID", migrationReady: false, automaticReplay: false })
checks++
}
{
const f = await fixture("rest")
f.receipt.state = "failed"; f.receipt.httpStatus = 502; f.receipt.errorCode = "CANCELLED"
delete f.receipt.output; delete f.receipt.data; f.receipt.attemptedStages = 0; f.receipt.completedStages = 0
Object.assign(f.anchor, { state: "failed", attempted_stages: 0, completed_stages: 0 }); f.bundle.stages = []
await f.refreshReceipt(); assert.equal((await f.verify(f.bundle)).status, "verified"); checks++
}
for (const tool of ["dns", "route", "bandwidth", "mtu"] as const) {
const f = await fixture()
f.context.tool = tool; f.receipt.tool = tool; f.receipt.data = { kind: tool }; f.anchor.tool = tool
if (tool === "bandwidth") {
f.context.destinationServerId = 12; f.receipt.destinationServerId = 12; f.anchor.destination_server_id = 12
}
const kinds = tool === "mtu" ? ["source-dns-A", ...[1500,1492,1480,1476,1472,1468,1400,1280,1024,576].map(size => `native-mtu-${size}`)]
: [tool === "dns" ? "manager-dns-AAAA" : tool === "route" ? "native-route-read" : "native-bandwidth"]
f.bundle.stages = kinds.map((kind, index) => ({ ...f.stage, stage_index: index + 1, kind }))
f.receipt.attemptedStages = kinds.length; f.receipt.completedStages = kinds.length
f.anchor.attempted_stages = kinds.length; f.anchor.completed_stages = kinds.length
await f.refreshReceipt(); assert.equal((await f.verify(f.bundle)).status, "verified")
if (tool === "mtu") {
f.bundle.stages[1].kind = "native-mtu-1492"
assert.equal((await f.verify(f.bundle)).status, "unresolved")
}
checks++
}
{
const f = await fixture()
f.bundle.stages = [{ ...f.stage, kind: "source-dns-A", outcome: "source-unavailable" }, { ...f.stage, stage_index: 2 }]
f.receipt.attemptedStages = 2; f.receipt.completedStages = 2; f.anchor.attempted_stages = 2; f.anchor.completed_stages = 2
await f.refreshReceipt(); assert.equal((await f.verify(f.bundle)).status, "verified"); checks++
}
type Fixture = Awaited<ReturnType<typeof fixture>>
const mutations: Array<(f: Fixture) => void | Promise<void>> = [
f => { f.bundle.executionId = randomUUID() }, f => { f.bundle.identityKind = "mcp" },
f => { f.anchor.actor_id = "foreign" }, f => { f.anchor.authentication = "local" },
f => { f.anchor.mcp_connection_id = randomUUID() }, f => { f.anchor.mcp_credential_id = randomUUID() },
f => { f.anchor.request_id = randomUUID() }, f => { f.anchor.authority_issued_at = "2" },
f => { f.anchor.authority_expires_at = new Date("2030-01-01T00:00:00Z") },
f => { f.anchor.source_server_id = 10 }, f => { f.anchor.destination_server_id = 11 }, f => { f.anchor.tool = "dns" },
f => { f.anchor.receipt_id = randomUUID() }, f => { f.anchor.binding_proof = "b".repeat(64) },
f => { f.anchor.receipt_proof = "b".repeat(64) }, f => { f.anchor.receipt_ciphertext = "missing-private" },
async f => { f.anchor.receipt_proof = await f.crypto.proof("receipt", JSON.parse(f.ciphertexts.get(String(f.anchor.receipt_ciphertext))!)) },
f => { f.anchor.receipt_ciphertext = "x".repeat(262145) }, f => { f.anchor.finished_at = null },
f => { f.anchor.created_at = "invalid" }, f => { f.anchor.state = "cancelled" },
f => { f.anchor.attempted_stages = 2 }, f => { f.anchor.completed_stages = 0 },
async f => { f.receipt.serverId = 10; await f.refreshReceipt() },
async f => { f.receipt.automaticReplay = true as false; await f.refreshReceipt() },
async f => { f.receipt.state = "unknown"; await f.refreshReceipt() },
async f => { f.receipt.errorCode = "PRIVATE_UNKNOWN"; await f.refreshReceipt() },
async f => { await f.refreshReceipt({ ...f.context, extra: true }) },
f => { f.bundle.stages = [] }, f => { f.stage.stage_index = 2 }, f => { f.stage.receipt_id = randomUUID() },
f => { f.stage.timeout_ms = 120001 }, f => { f.stage.timeout_ms = 0 }, f => { f.stage.kind = "native-route-read" },
f => { f.stage.outcome = "source-unavailable" }, f => { f.stage.received_at = null },
f => { f.stage.state = "intent" }, f => { f.bundle.stages.push({ ...f.stage }) },
]
for (const mutate of mutations) {
const f = await fixture(); await mutate(f)
const result = await f.verify(f.bundle)
assert.equal(result.status, "unresolved"); assert.equal(JSON.stringify(result).includes(secret), false); checks++
}
const sourceMutations: Array<(f: Fixture) => void> = [
f => { Object.assign(f.source.scope, { ownerId: "foreign" }) }, f => { Object.assign(f.source.scope, { executionId: randomUUID() }) },
f => { Object.assign(f.source.scope, { bindingProof: "b".repeat(64) }) }, f => { Object.assign(f.source, { originalCredentialId: randomUUID() }) },
f => { Object.assign(f.source, { serverIds: [10] }) }, f => { Object.assign(f.source, { stageBudget: 1 }) },
f => { Object.assign(f.source.scope, { authorityExpiresAt: "2030-01-01T00:00:00Z" }) },
]
for (const mutate of sourceMutations) {
const f = await fixture("mcp"); mutate(f)
assert.equal((await f.verify(f.bundle)).status, "unresolved"); checks++
}
for (const mutate of [
(row: Record<string, unknown>) => { row.identity_proof = "b".repeat(64) },
(row: Record<string, unknown>) => { row.binding_proof = "b".repeat(64) },
(row: Record<string, unknown>) => { row.execution_id = randomUUID() },
(row: Record<string, unknown>) => { row.identity_kind = "rest" },
(row: Record<string, unknown>) => { row.attempted_stages = 12 },
(row: Record<string, unknown>) => { row.completed_stages = 1 },
]) {
const f = await fixture("mcp", false); await f.quarantine(); mutate(f.bundle.quarantine!)
assert.equal((await f.verify(f.bundle)).status, "unresolved"); checks++
}
for (const field of ["operation_id", "connection_id", "actor_id"]) {
const f = await fixture("mcp", false), id = f.context.identity; assert(id.kind === "mcp")
f.bundle.cancellation = { operation_id: id.executionId, connection_id: id.connectionId, actor_id: id.ownerId, [field]: randomUUID() }
assert.equal((await f.verify(f.bundle)).status, "unresolved"); checks++
}
{
const f = await fixture("mcp"), verify = createLegacyServerProbeVerifier({ loadMcpSource: async () => { throw Error(secret) } })
const result = await verify(f.bundle); assert.equal(result.status, "unresolved"); assert.equal(JSON.stringify(result).includes(secret), false); checks++
}
{
const f = await fixture(), verify = createLegacyServerProbeVerifierForTests(f.deps, { ...f.crypto, proof: async () => { throw Error(secret) } })
const result = await verify(f.bundle); assert.equal(result.status, "unresolved"); assert.equal(JSON.stringify(result).includes(secret), false); checks++
}
{
const state = await mkdtemp(join(tmpdir(), "mm-legacy-verifier-test-"))
const previousState = process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR = state
try {
const receipt = await fixture()
const quarantine = await fixture("rest", false)
await quarantine.quarantine()
const verify = createLegacyServerProbeVerifier(receipt.deps)
for (const bundle of [receipt.bundle, quarantine.bundle]) {
assert.equal((await verify(bundle)).status, "unresolved")
assert.deepEqual(await readdir(state), [], "Historical reads must leave missing encryption state untouched")
checks++
}
} finally {
if (previousState === undefined) delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR = previousState
await rm(join(state, "encryption-key"), { force: true })
await rmdir(state)
}
}
console.log(`server-probe-legacy-verifier: ${checks} historical authentication checks passed; migration readiness always false`)