Skip to content

Commit 12aa3e6

Browse files
RealDiligentRealDiligent
andauthored
feat(engine): add the attestation-evidence envelope seam (#8551)
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 Co-authored-by: RealDiligent <nft.gold.eth@gmail.com>
1 parent 59faa47 commit 12aa3e6

4 files changed

Lines changed: 398 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Attestation-evidence envelope (#8541) -- the typed seam the attested-evaluation epic needs BEFORE any TEE
2+
// infrastructure exists. A backtest run already persists `metadata.corpusChecksum` plus head/base SHAs
3+
// (services/threshold-backtest-run.ts), which is what makes a verdict third-party reproducible for a public
4+
// corpus. This module describes "that run executed inside an attested environment" as a shape, so the later
5+
// runner work attaches evidence to runs instead of inventing an ad-hoc object at the call site.
6+
//
7+
// Deliberately pure and infrastructure-free: structural validation ONLY. Cryptographically verifying an
8+
// attestation report (checking the TEE vendor's signature chain, measurement allow-lists, freshness) is
9+
// separate maintainer work in the parent epic -- doing any of it here would be unreviewable scope and would
10+
// bake a verification policy into what is meant to be a transport shape. Same purity contract as the rest of
11+
// this module family: no IO, no randomness, no wall-clock reads.
12+
13+
import { createHash } from "node:crypto";
14+
15+
/** TEE technologies this envelope can describe. */
16+
export type AttestationTeeTechnology = "sev-snp" | "tdx";
17+
18+
/** Outcome of verifying the attestation report. `unverified` is the honest default: evidence was captured
19+
* but nothing has checked it yet -- distinct from `failed`, which records a verifier's negative verdict. */
20+
export type AttestationVerification =
21+
| { status: "unverified" }
22+
| { status: "verified"; verifierId: string; verifiedAt: string }
23+
| { status: "failed"; verifierId: string; verifiedAt: string; reason: string };
24+
25+
export type AttestationEnvelope = {
26+
/** Literal 1 -- a future shape change bumps this rather than silently widening the current one. */
27+
schemaVersion: 1;
28+
teeTechnology: AttestationTeeTechnology;
29+
/** Opaque label for the runtime image/class the workload ran as. Non-empty, <= 128 chars. */
30+
runtimeClass: string;
31+
/** Launch measurement, lowercase hex, 32-128 hex chars (widths differ per TEE technology). */
32+
measurement: string;
33+
/** sha256, lowercase hex, exactly 64 chars -- see {@link buildAttestationReportData} for the binding. */
34+
reportData: string;
35+
/** The raw attestation report, base64, non-empty and <= 65536 chars. Never parsed here. */
36+
attestationReport: string;
37+
verification: AttestationVerification;
38+
};
39+
40+
const TEE_TECHNOLOGIES: readonly string[] = ["sev-snp", "tdx"];
41+
const RUNTIME_CLASS_MAX = 128;
42+
const MEASUREMENT_MIN_HEX = 32;
43+
const MEASUREMENT_MAX_HEX = 128;
44+
const REPORT_DATA_HEX = 64;
45+
const ATTESTATION_REPORT_MAX = 65536;
46+
const LOWERCASE_HEX = /^[0-9a-f]+$/;
47+
const ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
48+
const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
49+
const ENVELOPE_KEYS: readonly string[] = [
50+
"schemaVersion",
51+
"teeTechnology",
52+
"runtimeClass",
53+
"measurement",
54+
"reportData",
55+
"attestationReport",
56+
"verification",
57+
];
58+
const VERIFICATION_KEYS: Record<AttestationVerification["status"], readonly string[]> = {
59+
unverified: ["status"],
60+
verified: ["status", "verifierId", "verifiedAt"],
61+
failed: ["status", "verifierId", "verifiedAt", "reason"],
62+
};
63+
64+
/**
65+
* The 32-byte payload a TEE binds into its attestation report, as lowercase hex: sha256 of
66+
* `${corpusChecksum}:${headSha}:${baseSha}`. Binding all three is what makes the report prove WHICH
67+
* evaluation ran (#8136) -- the corpus alone would not pin the code revision, and the SHAs alone would not
68+
* pin the data. Mirrors backtest-split.ts's own `createHash("sha256")` usage; no new dependency.
69+
*/
70+
export function buildAttestationReportData(binding: { corpusChecksum: string; headSha: string; baseSha: string }): string {
71+
return createHash("sha256").update(`${binding.corpusChecksum}:${binding.headSha}:${binding.baseSha}`).digest("hex");
72+
}
73+
74+
function nonEmptyString(value: unknown): value is string {
75+
return typeof value === "string" && value.length > 0;
76+
}
77+
78+
function validateVerification(value: unknown, errors: string[]): void {
79+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
80+
errors.push("verification: expected an object");
81+
return;
82+
}
83+
const record = value as Record<string, unknown>;
84+
const status = record["status"];
85+
if (status !== "unverified" && status !== "verified" && status !== "failed") {
86+
errors.push('verification.status: expected "unverified", "verified", or "failed"');
87+
return;
88+
}
89+
for (const key of Object.keys(record)) {
90+
if (!VERIFICATION_KEYS[status].includes(key)) errors.push(`verification.${key}: unexpected key`);
91+
}
92+
if (status === "unverified") return;
93+
94+
if (!nonEmptyString(record["verifierId"])) errors.push("verification.verifierId: expected a non-empty string");
95+
// Shape first: Date.parse alone accepts looser forms (a bare "2026-07-25" and other
96+
// implementation-defined fallbacks), while the regex alone would accept "2026-13-45T99:99:99Z".
97+
const verifiedAt = record["verifiedAt"];
98+
if (!nonEmptyString(verifiedAt) || !ISO_DATETIME.test(verifiedAt) || Number.isNaN(Date.parse(verifiedAt))) {
99+
errors.push("verification.verifiedAt: expected an ISO-8601 datetime string");
100+
}
101+
if (status === "failed" && !nonEmptyString(record["reason"])) {
102+
errors.push("verification.reason: expected a non-empty string");
103+
}
104+
}
105+
106+
/**
107+
* Structurally validate an unknown value as an {@link AttestationEnvelope}. Never throws for ANY input --
108+
* `null`, primitives, arrays and objects with extra keys all return `{ valid: false }` with one error per
109+
* failing field path, so a caller can log exactly what was wrong with a rejected envelope. Extra keys are
110+
* rejected rather than ignored: this shape is persisted evidence, and silently dropping an unrecognized
111+
* field would lose data a future schemaVersion may depend on.
112+
*/
113+
export function validateAttestationEnvelope(
114+
value: unknown,
115+
): { valid: true; envelope: AttestationEnvelope } | { valid: false; errors: string[] } {
116+
const errors: string[] = [];
117+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
118+
return { valid: false, errors: ["envelope: expected an object"] };
119+
}
120+
const record = value as Record<string, unknown>;
121+
122+
for (const key of Object.keys(record)) {
123+
if (!ENVELOPE_KEYS.includes(key)) errors.push(`${key}: unexpected key`);
124+
}
125+
126+
if (record["schemaVersion"] !== 1) errors.push("schemaVersion: expected the literal 1");
127+
128+
if (typeof record["teeTechnology"] !== "string" || !TEE_TECHNOLOGIES.includes(record["teeTechnology"])) {
129+
errors.push('teeTechnology: expected "sev-snp" or "tdx"');
130+
}
131+
132+
const runtimeClass = record["runtimeClass"];
133+
if (!nonEmptyString(runtimeClass) || runtimeClass.length > RUNTIME_CLASS_MAX) {
134+
errors.push(`runtimeClass: expected a non-empty string of at most ${RUNTIME_CLASS_MAX} characters`);
135+
}
136+
137+
const measurement = record["measurement"];
138+
if (
139+
typeof measurement !== "string" ||
140+
!LOWERCASE_HEX.test(measurement) ||
141+
measurement.length < MEASUREMENT_MIN_HEX ||
142+
measurement.length > MEASUREMENT_MAX_HEX
143+
) {
144+
errors.push(`measurement: expected ${MEASUREMENT_MIN_HEX}-${MEASUREMENT_MAX_HEX} lowercase hex characters`);
145+
}
146+
147+
const reportData = record["reportData"];
148+
if (typeof reportData !== "string" || reportData.length !== REPORT_DATA_HEX || !LOWERCASE_HEX.test(reportData)) {
149+
errors.push(`reportData: expected exactly ${REPORT_DATA_HEX} lowercase hex characters`);
150+
}
151+
152+
const attestationReport = record["attestationReport"];
153+
if (
154+
!nonEmptyString(attestationReport) ||
155+
attestationReport.length > ATTESTATION_REPORT_MAX ||
156+
!BASE64.test(attestationReport)
157+
) {
158+
errors.push(`attestationReport: expected non-empty base64 of at most ${ATTESTATION_REPORT_MAX} characters`);
159+
}
160+
161+
validateVerification(record["verification"], errors);
162+
163+
if (errors.length > 0) return { valid: false, errors };
164+
return { valid: true, envelope: record as AttestationEnvelope };
165+
}

packages/loopover-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export * from "./calibration/backtest-split.js";
181181
export * from "./calibration/backtest-threshold.js";
182182
export * from "./calibration/provider-track-record.js";
183183
export * from "./calibration/reliability-curve.js";
184+
export * from "./calibration/attestation-envelope.js";
184185
export {
185186
GOVERNOR_LEDGER_EVENT_TYPES,
186187
normalizeGovernorLedgerEvent,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import { buildAttestationReportData, validateAttestationEnvelope } from "../dist/index.js";
5+
6+
const BASE = {
7+
schemaVersion: 1,
8+
teeTechnology: "sev-snp",
9+
runtimeClass: "loopover-backtest-runner",
10+
measurement: "a".repeat(64),
11+
reportData: "b".repeat(64),
12+
attestationReport: "QUJD",
13+
verification: { status: "unverified" },
14+
};
15+
16+
test("barrel: the public entrypoint re-exports the attestation-envelope primitives (#8541)", () => {
17+
assert.equal(typeof buildAttestationReportData, "function");
18+
assert.equal(typeof validateAttestationEnvelope, "function");
19+
});
20+
21+
test("buildAttestationReportData pins the corpusChecksum:headSha:baseSha binding (#8541)", () => {
22+
assert.equal(
23+
buildAttestationReportData({ corpusChecksum: "abc123", headSha: "head456", baseSha: "base789" }),
24+
"fcc875115df49bf143b18fc8a8071e9a946858407fabf61e59ef1607d1cfb140",
25+
);
26+
});
27+
28+
test("validateAttestationEnvelope accepts a well-formed envelope and each verification variant (#8541)", () => {
29+
assert.equal(validateAttestationEnvelope(BASE).valid, true);
30+
assert.equal(
31+
validateAttestationEnvelope({ ...BASE, verification: { status: "verified", verifierId: "v1", verifiedAt: "2026-07-25T00:00:00.000Z" } }).valid,
32+
true,
33+
);
34+
assert.equal(
35+
validateAttestationEnvelope({
36+
...BASE,
37+
verification: { status: "failed", verifierId: "v1", verifiedAt: "2026-07-25T00:00:00.000Z", reason: "signature mismatch" },
38+
}).valid,
39+
true,
40+
);
41+
});
42+
43+
test("validateAttestationEnvelope rejects structurally invalid input without throwing (#8541)", () => {
44+
for (const bad of [null, undefined, 42, "envelope", [], { ...BASE, schemaVersion: 2 }, { ...BASE, reportData: "b".repeat(63) }, { ...BASE, rogue: 1 }]) {
45+
const result = validateAttestationEnvelope(bad);
46+
assert.equal(result.valid, false);
47+
assert.ok(Array.isArray(result.errors) && result.errors.length > 0);
48+
}
49+
});

0 commit comments

Comments
 (0)