diff --git a/packages/loopover-engine/src/calibration/attestation-envelope.ts b/packages/loopover-engine/src/calibration/attestation-envelope.ts new file mode 100644 index 0000000000..481b3c6f5a --- /dev/null +++ b/packages/loopover-engine/src/calibration/attestation-envelope.ts @@ -0,0 +1,97 @@ +// Attestation-evidence envelope (#8541, attested-evaluation epic) — the typed evidence seam that lets a later +// runner attach "this backtest run executed inside an attested TEE" evidence to a persisted run, without +// inventing an ad-hoc shape. This file is PURE STRUCTURAL code only: a schema type, a deterministic +// report-data binder, and a never-throwing structural validator. It performs NO cryptographic verification of +// the attestation report (that is later maintainer work in the epic), reads no IO, and adds no dependency. + +import { createHash } from "node:crypto"; + +/** The verification outcome recorded on an envelope. Discriminated on `status`: an envelope may be captured + * before any verifier has run (`unverified`), or carry a verifier's pass (`verified`) / fail (`failed`) + * judgment. `verifiedAt` is an ISO-8601 datetime; `reason` explains a failure. */ +export type AttestationVerification = + | { status: "unverified" } + | { status: "verified"; verifierId: string; verifiedAt: string } + | { status: "failed"; verifierId: string; verifiedAt: string; reason: string }; + +/** Evidence that a run executed inside an attested environment. Structural only — the `attestationReport` is + * an opaque base64 blob this module never cryptographically verifies. */ +export type AttestationEnvelope = { + schemaVersion: 1; + teeTechnology: "sev-snp" | "tdx"; + runtimeClass: string; + measurement: string; + reportData: string; + attestationReport: string; + verification: AttestationVerification; +}; + +/** + * The 32-byte `reportData` an attestation report must bind to, as lowercase-hex sha256 of + * `${corpusChecksum}:${headSha}:${baseSha}` — the same tuple that already makes a persisted run + * third-party reproducible (#8136). Deterministic; mirrors backtest-split.ts's `createHash("sha256")` usage. + */ +export function buildAttestationReportData(binding: { corpusChecksum: string; headSha: string; baseSha: string }): string { + return createHash("sha256").update(`${binding.corpusChecksum}:${binding.headSha}:${binding.baseSha}`).digest("hex"); +} + +const KNOWN_ENVELOPE_KEYS = new Set(["schemaVersion", "teeTechnology", "runtimeClass", "measurement", "reportData", "attestationReport", "verification"]); +const MEASUREMENT_RE = /^[0-9a-f]{32,128}$/; +const REPORT_DATA_RE = /^[0-9a-f]{64}$/; +const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/; +// An ISO-8601 datetime (date + time + zone). Paired with a Date.parse check so a well-shaped but impossible +// value (e.g. month 13) is still rejected -- the regex governs shape, Date.parse governs real-calendar validity. +const ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +function isIsoDateTime(value: unknown): boolean { + return typeof value === "string" && ISO_DATETIME_RE.test(value) && !Number.isNaN(Date.parse(value)); +} + +function validateVerification(value: unknown, errors: string[]): void { + if (!isRecord(value)) { + errors.push("verification: must be an object"); + return; + } + if (value.status === "unverified") return; + if (value.status === "verified" || value.status === "failed") { + if (!isNonEmptyString(value.verifierId)) errors.push("verification.verifierId: must be a non-empty string"); + if (!isIsoDateTime(value.verifiedAt)) errors.push("verification.verifiedAt: must be an ISO-8601 datetime"); + if (value.status === "failed" && !isNonEmptyString(value.reason)) errors.push("verification.reason: must be a non-empty string"); + return; + } + errors.push('verification.status: must be "unverified", "verified", or "failed"'); +} + +/** + * Structurally validate an unknown value against {@link AttestationEnvelope}. Never throws for any input + * (null, primitives, arrays, objects with extra keys). On failure, `errors` names every failing field path; + * on success, `envelope` is the input narrowed to the type. Structural only — no cryptographic verification. + */ +export function validateAttestationEnvelope(value: unknown): { valid: true; envelope: AttestationEnvelope } | { valid: false; errors: string[] } { + if (!isRecord(value)) { + return { valid: false, errors: ["envelope: must be a non-null object"] }; + } + const errors: string[] = []; + for (const key of Object.keys(value)) { + if (!KNOWN_ENVELOPE_KEYS.has(key)) errors.push(`envelope: unexpected key "${key}"`); + } + if (value.schemaVersion !== 1) errors.push("schemaVersion: must be the literal 1"); + if (value.teeTechnology !== "sev-snp" && value.teeTechnology !== "tdx") errors.push('teeTechnology: must be "sev-snp" or "tdx"'); + if (!isNonEmptyString(value.runtimeClass) || value.runtimeClass.length > 128) errors.push("runtimeClass: must be a non-empty string of at most 128 chars"); + if (typeof value.measurement !== "string" || !MEASUREMENT_RE.test(value.measurement)) errors.push("measurement: must be 32-128 lowercase hex chars"); + if (typeof value.reportData !== "string" || !REPORT_DATA_RE.test(value.reportData)) errors.push("reportData: must be exactly 64 lowercase hex chars"); + if (typeof value.attestationReport !== "string" || value.attestationReport.length > 65536 || !BASE64_RE.test(value.attestationReport)) { + errors.push("attestationReport: must be non-empty base64 of at most 65536 chars"); + } + validateVerification(value.verification, errors); + if (errors.length > 0) return { valid: false, errors }; + return { valid: true, envelope: value as AttestationEnvelope }; +} diff --git a/packages/loopover-engine/src/index.ts b/packages/loopover-engine/src/index.ts index 5ed9bb76da..640cdfd260 100644 --- a/packages/loopover-engine/src/index.ts +++ b/packages/loopover-engine/src/index.ts @@ -163,6 +163,7 @@ export * from "./governor/kill-switch.js"; export * from "./governor/action-mode.js"; export * from "./governor/chokepoint.js"; export * from "./calibration/signal-tracking.js"; +export * from "./calibration/attestation-envelope.js"; export * from "./calibration/backtest-corpus.js"; export * from "./calibration/repo-corpus-slice.js"; export * from "./calibration/ams-prediction-corpus.js"; diff --git a/packages/loopover-engine/test/attestation-envelope.test.ts b/packages/loopover-engine/test/attestation-envelope.test.ts new file mode 100644 index 0000000000..4ed2c1f4da --- /dev/null +++ b/packages/loopover-engine/test/attestation-envelope.test.ts @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { buildAttestationReportData, validateAttestationEnvelope, type AttestationEnvelope } from "../dist/index.js"; + +function validEnvelope(overrides: Partial> = {}): Record { + return { + schemaVersion: 1, + teeTechnology: "sev-snp", + runtimeClass: "confidential-runner-v1", + measurement: "a".repeat(64), + reportData: "b".repeat(64), + attestationReport: "QUJDZA==", + verification: { status: "unverified" }, + ...overrides, + }; +} + +function errorsOf(value: unknown): string[] { + const result = validateAttestationEnvelope(value); + assert.equal(result.valid, false); + return result.valid ? [] : result.errors; +} + +test("buildAttestationReportData binds corpusChecksum:headSha:baseSha as lowercase-hex sha256 (pinned vector)", () => { + assert.equal( + buildAttestationReportData({ corpusChecksum: "corpus-abc", headSha: "head-111", baseSha: "base-222" }), + "f3fe846e3d8db839cfa76d48577cad76159f75460d2bf881568ba5d926319e28", + ); +}); + +test("validateAttestationEnvelope accepts a fully valid envelope", () => { + const result = validateAttestationEnvelope(validEnvelope()); + assert.equal(result.valid, true); +}); + +test("validateAttestationEnvelope never throws and rejects non-object inputs", () => { + for (const bad of [null, undefined, 42, "str", true, []]) { + const result = validateAttestationEnvelope(bad); + assert.equal(result.valid, false); + if (!result.valid) assert.deepEqual(result.errors, ["envelope: must be a non-null object"]); + } +}); + +test("validateAttestationEnvelope rejects an unexpected extra key, naming it", () => { + assert.ok(errorsOf(validEnvelope({ extra: 1 } as never)).includes('envelope: unexpected key "extra"')); +}); + +test("validateAttestationEnvelope requires schemaVersion literal 1", () => { + assert.ok(errorsOf(validEnvelope({ schemaVersion: 2 })).includes("schemaVersion: must be the literal 1")); + assert.ok(errorsOf(validEnvelope({ schemaVersion: "1" })).includes("schemaVersion: must be the literal 1")); +}); + +test("validateAttestationEnvelope accepts both teeTechnology values, rejects others", () => { + assert.equal(validateAttestationEnvelope(validEnvelope({ teeTechnology: "tdx" })).valid, true); + assert.ok(errorsOf(validEnvelope({ teeTechnology: "sgx" })).includes('teeTechnology: must be "sev-snp" or "tdx"')); +}); + +test("validateAttestationEnvelope enforces runtimeClass (empty/non-string/too-long/boundary)", () => { + assert.ok(errorsOf(validEnvelope({ runtimeClass: "" })).includes("runtimeClass: must be a non-empty string of at most 128 chars")); + assert.ok(errorsOf(validEnvelope({ runtimeClass: 5 })).includes("runtimeClass: must be a non-empty string of at most 128 chars")); + assert.ok(errorsOf(validEnvelope({ runtimeClass: "a".repeat(129) })).includes("runtimeClass: must be a non-empty string of at most 128 chars")); + assert.equal(validateAttestationEnvelope(validEnvelope({ runtimeClass: "a".repeat(128) })).valid, true); +}); + +test("validateAttestationEnvelope enforces measurement 32-128 lowercase hex", () => { + assert.equal(validateAttestationEnvelope(validEnvelope({ measurement: "a".repeat(32) })).valid, true); + assert.equal(validateAttestationEnvelope(validEnvelope({ measurement: "a".repeat(128) })).valid, true); + for (const bad of ["a".repeat(31), "a".repeat(129), "A".repeat(64), "g".repeat(64), 123]) { + assert.ok(errorsOf(validEnvelope({ measurement: bad })).includes("measurement: must be 32-128 lowercase hex chars")); + } +}); + +test("validateAttestationEnvelope enforces reportData exactly-64 lowercase hex", () => { + for (const bad of ["b".repeat(63), "b".repeat(65), "B".repeat(64), 64]) { + assert.ok(errorsOf(validEnvelope({ reportData: bad })).includes("reportData: must be exactly 64 lowercase hex chars")); + } +}); + +test("validateAttestationEnvelope enforces attestationReport base64 (empty/too-long/non-base64/non-string/boundary)", () => { + for (const bad of ["", "A".repeat(65537), "not base64!", 7]) { + assert.ok(errorsOf(validEnvelope({ attestationReport: bad })).includes("attestationReport: must be non-empty base64 of at most 65536 chars")); + } + assert.equal(validateAttestationEnvelope(validEnvelope({ attestationReport: "A".repeat(65536) })).valid, true); +}); + +test("validateAttestationEnvelope validates the verification discriminated union", () => { + assert.ok(errorsOf(validEnvelope({ verification: "unverified" })).includes("verification: must be an object")); + assert.ok(errorsOf(validEnvelope({ verification: { status: "maybe" } })).includes('verification.status: must be "unverified", "verified", or "failed"')); + assert.equal(validateAttestationEnvelope(validEnvelope({ verification: { status: "unverified" } })).valid, true); + assert.equal(validateAttestationEnvelope(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00.000Z" } })).valid, true); + assert.equal(validateAttestationEnvelope(validEnvelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00+00:00", reason: "mismatch" } })).valid, true); + assert.ok(errorsOf(validEnvelope({ verification: { status: "verified", verifiedAt: "2026-07-24T00:00:00Z" } })).includes("verification.verifierId: must be a non-empty string")); + assert.ok(errorsOf(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "not-a-date" } })).includes("verification.verifiedAt: must be an ISO-8601 datetime")); + assert.ok(errorsOf(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-13-99T00:00:00Z" } })).includes("verification.verifiedAt: must be an ISO-8601 datetime")); + assert.ok(errorsOf(validEnvelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00Z" } })).includes("verification.reason: must be a non-empty string")); +}); diff --git a/test/unit/attestation-envelope-engine.test.ts b/test/unit/attestation-envelope-engine.test.ts new file mode 100644 index 0000000000..a70ad9ae94 --- /dev/null +++ b/test/unit/attestation-envelope-engine.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "vitest"; +// Direct src-path import (not the `@loopover/engine` barrel, which resolves to dist and is NOT in vitest's +// coverage.include): the engine's node:test suite runs against dist and is invisible to Codecov, so this +// vitest mirror is what gives packages/loopover-engine/src/calibration/attestation-envelope.ts its +// codecov/patch coverage (the "engine blind-spot rule", same as #8438 did for signal-tracking.ts). The +// companion packages/loopover-engine/test/attestation-envelope.test.ts is the node:test that gates the engine +// workspace's own `npm run test`. +import { buildAttestationReportData, validateAttestationEnvelope } from "../../packages/loopover-engine/src/calibration/attestation-envelope.js"; +import type { AttestationEnvelope } from "../../packages/loopover-engine/src/calibration/attestation-envelope.js"; + +function validEnvelope(overrides: Partial> = {}): Record { + return { + schemaVersion: 1, + teeTechnology: "sev-snp", + runtimeClass: "confidential-runner-v1", + measurement: "a".repeat(64), + reportData: "b".repeat(64), + attestationReport: "QUJDZA==", + verification: { status: "unverified" }, + ...overrides, + }; +} + +function firstErrors(value: unknown): string[] { + const result = validateAttestationEnvelope(value); + expect(result.valid).toBe(false); + return result.valid ? [] : result.errors; +} + +describe("buildAttestationReportData (#8541)", () => { + it("binds corpusChecksum:headSha:baseSha as lowercase-hex sha256 (pinned vector)", () => { + expect(buildAttestationReportData({ corpusChecksum: "corpus-abc", headSha: "head-111", baseSha: "base-222" })).toBe( + "f3fe846e3d8db839cfa76d48577cad76159f75460d2bf881568ba5d926319e28", + ); + }); + + it("is deterministic and produces a 64-char lowercase hex string", () => { + const out = buildAttestationReportData({ corpusChecksum: "x", headSha: "y", baseSha: "z" }); + expect(out).toMatch(/^[0-9a-f]{64}$/); + expect(buildAttestationReportData({ corpusChecksum: "x", headSha: "y", baseSha: "z" })).toBe(out); + }); +}); + +describe("validateAttestationEnvelope (#8541)", () => { + it("accepts a fully valid envelope and narrows it", () => { + const result = validateAttestationEnvelope(validEnvelope()); + expect(result.valid).toBe(true); + if (result.valid) expect(result.envelope.schemaVersion).toBe(1); + }); + + it("never throws and rejects non-object inputs (null, primitives, arrays)", () => { + for (const bad of [null, undefined, 42, "str", true, []]) { + const result = validateAttestationEnvelope(bad); + expect(result.valid).toBe(false); + if (!result.valid) expect(result.errors).toEqual(["envelope: must be a non-null object"]); + } + }); + + it("rejects an unexpected extra key, naming it", () => { + expect(firstErrors(validEnvelope({ extra: 1 } as never))).toContain('envelope: unexpected key "extra"'); + }); + + it("requires schemaVersion to be the literal 1", () => { + expect(firstErrors(validEnvelope({ schemaVersion: 2 }))).toContain("schemaVersion: must be the literal 1"); + expect(firstErrors(validEnvelope({ schemaVersion: "1" }))).toContain("schemaVersion: must be the literal 1"); + }); + + it("accepts both teeTechnology values and rejects any other", () => { + expect(validateAttestationEnvelope(validEnvelope({ teeTechnology: "tdx" })).valid).toBe(true); + expect(firstErrors(validEnvelope({ teeTechnology: "sgx" }))).toContain('teeTechnology: must be "sev-snp" or "tdx"'); + }); + + it("requires a non-empty runtimeClass of at most 128 chars", () => { + expect(firstErrors(validEnvelope({ runtimeClass: "" }))).toContain("runtimeClass: must be a non-empty string of at most 128 chars"); + expect(firstErrors(validEnvelope({ runtimeClass: 5 }))).toContain("runtimeClass: must be a non-empty string of at most 128 chars"); + expect(firstErrors(validEnvelope({ runtimeClass: "a".repeat(129) }))).toContain("runtimeClass: must be a non-empty string of at most 128 chars"); + expect(validateAttestationEnvelope(validEnvelope({ runtimeClass: "a".repeat(128) })).valid).toBe(true); + }); + + it("requires measurement to be 32-128 lowercase hex chars (boundaries + non-hex + non-string)", () => { + expect(validateAttestationEnvelope(validEnvelope({ measurement: "a".repeat(32) })).valid).toBe(true); + expect(validateAttestationEnvelope(validEnvelope({ measurement: "a".repeat(128) })).valid).toBe(true); + expect(firstErrors(validEnvelope({ measurement: "a".repeat(31) }))).toContain("measurement: must be 32-128 lowercase hex chars"); + expect(firstErrors(validEnvelope({ measurement: "a".repeat(129) }))).toContain("measurement: must be 32-128 lowercase hex chars"); + expect(firstErrors(validEnvelope({ measurement: "A".repeat(64) }))).toContain("measurement: must be 32-128 lowercase hex chars"); + expect(firstErrors(validEnvelope({ measurement: "g".repeat(64) }))).toContain("measurement: must be 32-128 lowercase hex chars"); + expect(firstErrors(validEnvelope({ measurement: 123 }))).toContain("measurement: must be 32-128 lowercase hex chars"); + }); + + it("requires reportData to be exactly 64 lowercase hex chars (63/65 + non-hex + non-string)", () => { + expect(firstErrors(validEnvelope({ reportData: "b".repeat(63) }))).toContain("reportData: must be exactly 64 lowercase hex chars"); + expect(firstErrors(validEnvelope({ reportData: "b".repeat(65) }))).toContain("reportData: must be exactly 64 lowercase hex chars"); + expect(firstErrors(validEnvelope({ reportData: "B".repeat(64) }))).toContain("reportData: must be exactly 64 lowercase hex chars"); + expect(firstErrors(validEnvelope({ reportData: 64 }))).toContain("reportData: must be exactly 64 lowercase hex chars"); + }); + + it("requires attestationReport to be non-empty base64 of at most 65536 chars (empty/too-long/non-base64/non-string)", () => { + expect(firstErrors(validEnvelope({ attestationReport: "" }))).toContain("attestationReport: must be non-empty base64 of at most 65536 chars"); + expect(firstErrors(validEnvelope({ attestationReport: "A".repeat(65537) }))).toContain("attestationReport: must be non-empty base64 of at most 65536 chars"); + expect(firstErrors(validEnvelope({ attestationReport: "not base64!" }))).toContain("attestationReport: must be non-empty base64 of at most 65536 chars"); + expect(firstErrors(validEnvelope({ attestationReport: 7 }))).toContain("attestationReport: must be non-empty base64 of at most 65536 chars"); + expect(validateAttestationEnvelope(validEnvelope({ attestationReport: "A".repeat(65536) })).valid).toBe(true); + }); + + it("validates the verification discriminated union — unverified/verified/failed and their members", () => { + // Non-object and unknown status. + expect(firstErrors(validEnvelope({ verification: "unverified" }))).toContain("verification: must be an object"); + expect(firstErrors(validEnvelope({ verification: { status: "maybe" } }))).toContain('verification.status: must be "unverified", "verified", or "failed"'); + // Valid variants. + expect(validateAttestationEnvelope(validEnvelope({ verification: { status: "unverified" } })).valid).toBe(true); + expect(validateAttestationEnvelope(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00.000Z" } })).valid).toBe(true); + expect(validateAttestationEnvelope(validEnvelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00+00:00", reason: "measurement mismatch" } })).valid).toBe(true); + // Missing / invalid members. + expect(firstErrors(validEnvelope({ verification: { status: "verified", verifiedAt: "2026-07-24T00:00:00Z" } }))).toContain("verification.verifierId: must be a non-empty string"); + expect(firstErrors(validEnvelope({ verification: { status: "verified", verifierId: 9, verifiedAt: "2026-07-24T00:00:00Z" } }))).toContain("verification.verifierId: must be a non-empty string"); + expect(firstErrors(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "not-a-date" } }))).toContain("verification.verifiedAt: must be an ISO-8601 datetime"); + // Well-shaped but impossible datetime (regex matches, Date.parse is NaN). + expect(firstErrors(validEnvelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-13-99T00:00:00Z" } }))).toContain("verification.verifiedAt: must be an ISO-8601 datetime"); + expect(firstErrors(validEnvelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-24T00:00:00Z" } }))).toContain("verification.reason: must be a non-empty string"); + }); + + it("accumulates every failing field path in one pass", () => { + const errors = firstErrors({ schemaVersion: 2, teeTechnology: "sgx", runtimeClass: "", measurement: "z", reportData: "z", attestationReport: "", verification: { status: "nope" } }); + expect(errors.length).toBeGreaterThanOrEqual(7); + }); +});