From 92a90937452626d654472721b79ca2ec7f1f3b88 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 25 Jul 2026 04:04:01 +0800 Subject: [PATCH] feat(engine): add the attestation-evidence envelope seam Adds packages/loopover-engine/src/calibration/attestation-envelope.ts with the three exports the attested-evaluation epic needs before any TEE infrastructure exists: the AttestationEnvelope type, buildAttestationReportData (lowercase-hex sha256 of corpusChecksum:headSha:baseSha, mirroring backtest-split.ts's createHash usage), and validateAttestationEnvelope, a structural validator that never throws and names every failing field path. Structural validation only -- no cryptographic verification of the attestation report, no new dependencies, no src/** wiring, and no changes to existing calibration modules. One barrel line, plus a root vitest mirror so the module's coverage is visible to Codecov alongside the engine's own node:test twin. Closes #8541 --- .../src/calibration/attestation-envelope.ts | 165 ++++++++++++++++ packages/loopover-engine/src/index.ts | 1 + .../test/attestation-envelope.test.ts | 49 +++++ test/unit/attestation-envelope-engine.test.ts | 183 ++++++++++++++++++ 4 files changed, 398 insertions(+) create mode 100644 packages/loopover-engine/src/calibration/attestation-envelope.ts create mode 100644 packages/loopover-engine/test/attestation-envelope.test.ts create mode 100644 test/unit/attestation-envelope-engine.test.ts 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..cf4546a80f --- /dev/null +++ b/packages/loopover-engine/src/calibration/attestation-envelope.ts @@ -0,0 +1,165 @@ +// Attestation-evidence envelope (#8541) -- the typed seam the attested-evaluation epic needs BEFORE any TEE +// infrastructure exists. A backtest run already persists `metadata.corpusChecksum` plus head/base SHAs +// (services/threshold-backtest-run.ts), which is what makes a verdict third-party reproducible for a public +// corpus. This module describes "that run executed inside an attested environment" as a shape, so the later +// runner work attaches evidence to runs instead of inventing an ad-hoc object at the call site. +// +// Deliberately pure and infrastructure-free: structural validation ONLY. Cryptographically verifying an +// attestation report (checking the TEE vendor's signature chain, measurement allow-lists, freshness) is +// separate maintainer work in the parent epic -- doing any of it here would be unreviewable scope and would +// bake a verification policy into what is meant to be a transport shape. Same purity contract as the rest of +// this module family: no IO, no randomness, no wall-clock reads. + +import { createHash } from "node:crypto"; + +/** TEE technologies this envelope can describe. */ +export type AttestationTeeTechnology = "sev-snp" | "tdx"; + +/** Outcome of verifying the attestation report. `unverified` is the honest default: evidence was captured + * but nothing has checked it yet -- distinct from `failed`, which records a verifier's negative verdict. */ +export type AttestationVerification = + | { status: "unverified" } + | { status: "verified"; verifierId: string; verifiedAt: string } + | { status: "failed"; verifierId: string; verifiedAt: string; reason: string }; + +export type AttestationEnvelope = { + /** Literal 1 -- a future shape change bumps this rather than silently widening the current one. */ + schemaVersion: 1; + teeTechnology: AttestationTeeTechnology; + /** Opaque label for the runtime image/class the workload ran as. Non-empty, <= 128 chars. */ + runtimeClass: string; + /** Launch measurement, lowercase hex, 32-128 hex chars (widths differ per TEE technology). */ + measurement: string; + /** sha256, lowercase hex, exactly 64 chars -- see {@link buildAttestationReportData} for the binding. */ + reportData: string; + /** The raw attestation report, base64, non-empty and <= 65536 chars. Never parsed here. */ + attestationReport: string; + verification: AttestationVerification; +}; + +const TEE_TECHNOLOGIES: readonly string[] = ["sev-snp", "tdx"]; +const RUNTIME_CLASS_MAX = 128; +const MEASUREMENT_MIN_HEX = 32; +const MEASUREMENT_MAX_HEX = 128; +const REPORT_DATA_HEX = 64; +const ATTESTATION_REPORT_MAX = 65536; +const LOWERCASE_HEX = /^[0-9a-f]+$/; +const ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/; +const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/; +const ENVELOPE_KEYS: readonly string[] = [ + "schemaVersion", + "teeTechnology", + "runtimeClass", + "measurement", + "reportData", + "attestationReport", + "verification", +]; +const VERIFICATION_KEYS: Record = { + unverified: ["status"], + verified: ["status", "verifierId", "verifiedAt"], + failed: ["status", "verifierId", "verifiedAt", "reason"], +}; + +/** + * The 32-byte payload a TEE binds into its attestation report, as lowercase hex: sha256 of + * `${corpusChecksum}:${headSha}:${baseSha}`. Binding all three is what makes the report prove WHICH + * evaluation ran (#8136) -- the corpus alone would not pin the code revision, and the SHAs alone would not + * pin the data. Mirrors backtest-split.ts's own `createHash("sha256")` usage; no new dependency. + */ +export function buildAttestationReportData(binding: { corpusChecksum: string; headSha: string; baseSha: string }): string { + return createHash("sha256").update(`${binding.corpusChecksum}:${binding.headSha}:${binding.baseSha}`).digest("hex"); +} + +function nonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +function validateVerification(value: unknown, errors: string[]): void { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + errors.push("verification: expected an object"); + return; + } + const record = value as Record; + const status = record["status"]; + if (status !== "unverified" && status !== "verified" && status !== "failed") { + errors.push('verification.status: expected "unverified", "verified", or "failed"'); + return; + } + for (const key of Object.keys(record)) { + if (!VERIFICATION_KEYS[status].includes(key)) errors.push(`verification.${key}: unexpected key`); + } + if (status === "unverified") return; + + if (!nonEmptyString(record["verifierId"])) errors.push("verification.verifierId: expected a non-empty string"); + // Shape first: Date.parse alone accepts looser forms (a bare "2026-07-25" and other + // implementation-defined fallbacks), while the regex alone would accept "2026-13-45T99:99:99Z". + const verifiedAt = record["verifiedAt"]; + if (!nonEmptyString(verifiedAt) || !ISO_DATETIME.test(verifiedAt) || Number.isNaN(Date.parse(verifiedAt))) { + errors.push("verification.verifiedAt: expected an ISO-8601 datetime string"); + } + if (status === "failed" && !nonEmptyString(record["reason"])) { + errors.push("verification.reason: expected a non-empty string"); + } +} + +/** + * Structurally validate an unknown value as an {@link AttestationEnvelope}. Never throws for ANY input -- + * `null`, primitives, arrays and objects with extra keys all return `{ valid: false }` with one error per + * failing field path, so a caller can log exactly what was wrong with a rejected envelope. Extra keys are + * rejected rather than ignored: this shape is persisted evidence, and silently dropping an unrecognized + * field would lose data a future schemaVersion may depend on. + */ +export function validateAttestationEnvelope( + value: unknown, +): { valid: true; envelope: AttestationEnvelope } | { valid: false; errors: string[] } { + const errors: string[] = []; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return { valid: false, errors: ["envelope: expected an object"] }; + } + const record = value as Record; + + for (const key of Object.keys(record)) { + if (!ENVELOPE_KEYS.includes(key)) errors.push(`${key}: unexpected key`); + } + + if (record["schemaVersion"] !== 1) errors.push("schemaVersion: expected the literal 1"); + + if (typeof record["teeTechnology"] !== "string" || !TEE_TECHNOLOGIES.includes(record["teeTechnology"])) { + errors.push('teeTechnology: expected "sev-snp" or "tdx"'); + } + + const runtimeClass = record["runtimeClass"]; + if (!nonEmptyString(runtimeClass) || runtimeClass.length > RUNTIME_CLASS_MAX) { + errors.push(`runtimeClass: expected a non-empty string of at most ${RUNTIME_CLASS_MAX} characters`); + } + + const measurement = record["measurement"]; + if ( + typeof measurement !== "string" || + !LOWERCASE_HEX.test(measurement) || + measurement.length < MEASUREMENT_MIN_HEX || + measurement.length > MEASUREMENT_MAX_HEX + ) { + errors.push(`measurement: expected ${MEASUREMENT_MIN_HEX}-${MEASUREMENT_MAX_HEX} lowercase hex characters`); + } + + const reportData = record["reportData"]; + if (typeof reportData !== "string" || reportData.length !== REPORT_DATA_HEX || !LOWERCASE_HEX.test(reportData)) { + errors.push(`reportData: expected exactly ${REPORT_DATA_HEX} lowercase hex characters`); + } + + const attestationReport = record["attestationReport"]; + if ( + !nonEmptyString(attestationReport) || + attestationReport.length > ATTESTATION_REPORT_MAX || + !BASE64.test(attestationReport) + ) { + errors.push(`attestationReport: expected non-empty base64 of at most ${ATTESTATION_REPORT_MAX} characters`); + } + + validateVerification(record["verification"], errors); + + if (errors.length > 0) return { valid: false, errors }; + return { valid: true, envelope: record as AttestationEnvelope }; +} diff --git a/packages/loopover-engine/src/index.ts b/packages/loopover-engine/src/index.ts index 5ed9bb76da..29272ae686 100644 --- a/packages/loopover-engine/src/index.ts +++ b/packages/loopover-engine/src/index.ts @@ -181,6 +181,7 @@ export * from "./calibration/backtest-split.js"; export * from "./calibration/backtest-threshold.js"; export * from "./calibration/provider-track-record.js"; export * from "./calibration/reliability-curve.js"; +export * from "./calibration/attestation-envelope.js"; export { GOVERNOR_LEDGER_EVENT_TYPES, normalizeGovernorLedgerEvent, 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..fee6d1b17c --- /dev/null +++ b/packages/loopover-engine/test/attestation-envelope.test.ts @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { buildAttestationReportData, validateAttestationEnvelope } from "../dist/index.js"; + +const BASE = { + schemaVersion: 1, + teeTechnology: "sev-snp", + runtimeClass: "loopover-backtest-runner", + measurement: "a".repeat(64), + reportData: "b".repeat(64), + attestationReport: "QUJD", + verification: { status: "unverified" }, +}; + +test("barrel: the public entrypoint re-exports the attestation-envelope primitives (#8541)", () => { + assert.equal(typeof buildAttestationReportData, "function"); + assert.equal(typeof validateAttestationEnvelope, "function"); +}); + +test("buildAttestationReportData pins the corpusChecksum:headSha:baseSha binding (#8541)", () => { + assert.equal( + buildAttestationReportData({ corpusChecksum: "abc123", headSha: "head456", baseSha: "base789" }), + "fcc875115df49bf143b18fc8a8071e9a946858407fabf61e59ef1607d1cfb140", + ); +}); + +test("validateAttestationEnvelope accepts a well-formed envelope and each verification variant (#8541)", () => { + assert.equal(validateAttestationEnvelope(BASE).valid, true); + assert.equal( + validateAttestationEnvelope({ ...BASE, verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-07-25T00:00:00.000Z" } }).valid, + true, + ); + assert.equal( + validateAttestationEnvelope({ + ...BASE, + verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-25T00:00:00.000Z", reason: "signature mismatch" }, + }).valid, + true, + ); +}); + +test("validateAttestationEnvelope rejects structurally invalid input without throwing (#8541)", () => { + for (const bad of [null, undefined, 42, "envelope", [], { ...BASE, schemaVersion: 2 }, { ...BASE, reportData: "b".repeat(63) }, { ...BASE, rogue: 1 }]) { + const result = validateAttestationEnvelope(bad); + assert.equal(result.valid, false); + assert.ok(Array.isArray(result.errors) && result.errors.length > 0); + } +}); diff --git a/test/unit/attestation-envelope-engine.test.ts b/test/unit/attestation-envelope-engine.test.ts new file mode 100644 index 0000000000..b5f1fa51a9 --- /dev/null +++ b/test/unit/attestation-envelope-engine.test.ts @@ -0,0 +1,183 @@ +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 own node:test suite runs against dist and is invisible to Codecov, so this +// vitest mirror is what gives the module its codecov/patch coverage -- the same seam #8438 used for +// signal-tracking.ts. The companion packages/loopover-engine/test/attestation-envelope.test.ts gates the +// engine workspace's own `npm run test` against the built barrel. +import { + buildAttestationReportData, + validateAttestationEnvelope, + type AttestationEnvelope, +} from "../../packages/loopover-engine/src/calibration/attestation-envelope.js"; + +const MEASUREMENT = "a".repeat(64); +const REPORT_DATA = "b".repeat(64); + +function envelope(overrides: Record = {}): Record { + return { + schemaVersion: 1, + teeTechnology: "sev-snp", + runtimeClass: "loopover-backtest-runner", + measurement: MEASUREMENT, + reportData: REPORT_DATA, + attestationReport: "QUJD", + verification: { status: "unverified" }, + ...overrides, + }; +} + +/** Assert rejection AND that the error names the failing field path (the contract callers log). */ +function expectRejected(value: unknown, fieldPath: string): string[] { + const result = validateAttestationEnvelope(value); + expect(result.valid).toBe(false); + if (result.valid) throw new Error("expected invalid"); + expect(result.errors.some((error) => error.startsWith(fieldPath))).toBe(true); + return result.errors; +} + +describe("buildAttestationReportData (#8541)", () => { + it("is the lowercase-hex sha256 of corpusChecksum:headSha:baseSha (pinned vector)", () => { + // Precomputed: sha256("abc123:head456:base789"). Pinned so a change to the binding format -- which would + // silently invalidate every previously-attested run -- fails here instead of shipping. + expect(buildAttestationReportData({ corpusChecksum: "abc123", headSha: "head456", baseSha: "base789" })).toBe( + "fcc875115df49bf143b18fc8a8071e9a946858407fabf61e59ef1607d1cfb140", + ); + }); + + it("produces exactly 64 lowercase hex chars and is deterministic and field-order sensitive", () => { + const data = buildAttestationReportData({ corpusChecksum: "c", headSha: "h", baseSha: "b" }); + expect(data).toMatch(/^[0-9a-f]{64}$/); + expect(data).toBe(buildAttestationReportData({ corpusChecksum: "c", headSha: "h", baseSha: "b" })); + // Swapping which value lands in which position must change the digest (the fields are not interchangeable). + expect(data).not.toBe(buildAttestationReportData({ corpusChecksum: "h", headSha: "c", baseSha: "b" })); + }); + + it("emits output usable as an envelope's reportData", () => { + const reportData = buildAttestationReportData({ corpusChecksum: "c", headSha: "h", baseSha: "b" }); + expect(validateAttestationEnvelope(envelope({ reportData })).valid).toBe(true); + }); +}); + +describe("validateAttestationEnvelope (#8541)", () => { + it("accepts a well-formed envelope and returns it narrowed", () => { + const result = validateAttestationEnvelope(envelope()); + expect(result.valid).toBe(true); + if (!result.valid) throw new Error(result.errors.join("; ")); + const narrowed: AttestationEnvelope = result.envelope; + expect(narrowed.schemaVersion).toBe(1); + expect(narrowed.verification.status).toBe("unverified"); + }); + + it("never throws for non-object input, returning a single envelope-level error", () => { + for (const value of [null, undefined, 0, 1, "", "envelope", true, false, [], [envelope()], Symbol("x"), 9n]) { + const result = validateAttestationEnvelope(value); + expect(result.valid).toBe(false); + if (result.valid) throw new Error("expected invalid"); + expect(result.errors).toEqual(["envelope: expected an object"]); + } + }); + + it("rejects an unexpected top-level key by name", () => { + const errors = expectRejected(envelope({ rogue: 1 }), "rogue"); + expect(errors.some((error) => error.includes("unexpected key"))).toBe(true); + }); + + it("requires the literal schemaVersion 1 (both arms)", () => { + expect(validateAttestationEnvelope(envelope({ schemaVersion: 1 })).valid).toBe(true); + for (const bad of [0, 2, "1", null, undefined]) expectRejected(envelope({ schemaVersion: bad }), "schemaVersion"); + }); + + it("accepts each supported teeTechnology and rejects anything else", () => { + for (const good of ["sev-snp", "tdx"]) expect(validateAttestationEnvelope(envelope({ teeTechnology: good })).valid).toBe(true); + for (const bad of ["SEV-SNP", "sgx", "", 1, null]) expectRejected(envelope({ teeTechnology: bad }), "teeTechnology"); + }); + + it("bounds runtimeClass at 1..128 characters (accepts the boundary, rejects just past it)", () => { + expect(validateAttestationEnvelope(envelope({ runtimeClass: "x" })).valid).toBe(true); + expect(validateAttestationEnvelope(envelope({ runtimeClass: "x".repeat(128) })).valid).toBe(true); + expectRejected(envelope({ runtimeClass: "x".repeat(129) }), "runtimeClass"); + for (const bad of ["", 1, null, undefined]) expectRejected(envelope({ runtimeClass: bad }), "runtimeClass"); + }); + + it("requires measurement to be 32..128 lowercase hex (boundaries accepted, just-past rejected)", () => { + expect(validateAttestationEnvelope(envelope({ measurement: "a".repeat(32) })).valid).toBe(true); + expect(validateAttestationEnvelope(envelope({ measurement: "a".repeat(128) })).valid).toBe(true); + expectRejected(envelope({ measurement: "a".repeat(31) }), "measurement"); + expectRejected(envelope({ measurement: "a".repeat(129) }), "measurement"); + expectRejected(envelope({ measurement: "A".repeat(64) }), "measurement"); // uppercase hex + expectRejected(envelope({ measurement: "g".repeat(64) }), "measurement"); // non-hex + expectRejected(envelope({ measurement: 64 }), "measurement"); + }); + + it("requires reportData to be exactly 64 lowercase hex (63 and 65 both rejected)", () => { + expect(validateAttestationEnvelope(envelope({ reportData: "b".repeat(64) })).valid).toBe(true); + expectRejected(envelope({ reportData: "b".repeat(63) }), "reportData"); + expectRejected(envelope({ reportData: "b".repeat(65) }), "reportData"); + expectRejected(envelope({ reportData: "B".repeat(64) }), "reportData"); // uppercase + expectRejected(envelope({ reportData: "z".repeat(64) }), "reportData"); // non-hex + expectRejected(envelope({ reportData: null }), "reportData"); + }); + + it("requires attestationReport to be non-empty base64 within the size cap", () => { + expect(validateAttestationEnvelope(envelope({ attestationReport: "QUJD" })).valid).toBe(true); + expect(validateAttestationEnvelope(envelope({ attestationReport: "QQ==" })).valid).toBe(true); + expect(validateAttestationEnvelope(envelope({ attestationReport: "A".repeat(65536) })).valid).toBe(true); + expectRejected(envelope({ attestationReport: "A".repeat(65537) }), "attestationReport"); + expectRejected(envelope({ attestationReport: "" }), "attestationReport"); + expectRejected(envelope({ attestationReport: "not base64!" }), "attestationReport"); + expectRejected(envelope({ attestationReport: 1 }), "attestationReport"); + }); + + describe("verification union", () => { + const VERIFIED_AT = "2026-07-25T00:00:00.000Z"; + + it("accepts every valid variant", () => { + expect(validateAttestationEnvelope(envelope({ verification: { status: "unverified" } })).valid).toBe(true); + expect( + validateAttestationEnvelope(envelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: VERIFIED_AT } })).valid, + ).toBe(true); + expect( + validateAttestationEnvelope( + envelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: VERIFIED_AT, reason: "signature mismatch" } }), + ).valid, + ).toBe(true); + }); + + it("rejects a non-object or unknown status", () => { + for (const bad of [null, "unverified", 1, []]) expectRejected(envelope({ verification: bad }), "verification"); + expectRejected(envelope({ verification: { status: "pending" } }), "verification.status"); + }); + + it("rejects a missing or invalid member of the verified variant", () => { + expectRejected(envelope({ verification: { status: "verified", verifiedAt: VERIFIED_AT } }), "verification.verifierId"); + expectRejected(envelope({ verification: { status: "verified", verifierId: "", verifiedAt: VERIFIED_AT } }), "verification.verifierId"); + expectRejected(envelope({ verification: { status: "verified", verifierId: "v1" } }), "verification.verifiedAt"); + expectRejected(envelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "not-a-date" } }), "verification.verifiedAt"); + // Date.parse alone tolerates this; the shape check is what rejects it. + expectRejected(envelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-07-25" } }), "verification.verifiedAt"); + // Matches the ISO shape but is not a real instant -- covers the Date.parse operand specifically. + expectRejected(envelope({ verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-13-45T99:99:99Z" } }), "verification.verifiedAt"); + }); + + it("rejects a missing or empty reason on the failed variant, and extra keys on any variant", () => { + expectRejected(envelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: VERIFIED_AT } }), "verification.reason"); + expectRejected( + envelope({ verification: { status: "failed", verifierId: "v1", verifiedAt: VERIFIED_AT, reason: "" } }), + "verification.reason", + ); + // unverified carries no other members -- an extra key is named, not ignored. + expectRejected(envelope({ verification: { status: "unverified", verifierId: "v1" } }), "verification.verifierId"); + }); + }); + + it("reports every failing field at once rather than stopping at the first", () => { + const errors = expectRejected( + { schemaVersion: 2, teeTechnology: "sgx", runtimeClass: "", measurement: "zz", reportData: "b", attestationReport: "", verification: null }, + "schemaVersion", + ); + for (const field of ["teeTechnology", "runtimeClass", "measurement", "reportData", "attestationReport", "verification"]) { + expect(errors.some((error) => error.startsWith(field))).toBe(true); + } + }); +});