Files
workinf_Blender_Wasm/web/protocol/io-format-receipt-freshness.ts
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

186 lines
9.4 KiB
TypeScript

import {
validateIOFormatBoundReceiptSet,
type IOFormatBoundRuntimeReceiptSetIR,
} from "./io-format-receipt-binding";
import {
type IOFormatRuntimeIdentityIR,
type IOFormatRuntimeRouteQuery,
} from "./io-format-runtime-receipt";
export const IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA = 1 as const;
export const IO_FORMAT_RECEIPT_FRESHNESS_TASK = "M12-05F" as const;
export interface IOFormatReceiptFreshnessEnvelopeIR {
schemaVersion: typeof IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA;
task: typeof IO_FORMAT_RECEIPT_FRESHNESS_TASK;
parentBindingSha256: string;
boundReceiptSetSha256: string;
runtimeSha256: string;
bound: IOFormatBoundRuntimeReceiptSetIR;
}
export interface IOFormatReceiptFreshnessExpectedIR {
parentBindingSha256: string;
parentReceiptSetSha256: string;
inventorySha256: string;
boundReceiptSetSha256: string;
runtimeSha256: string;
runtime: IOFormatRuntimeIdentityIR;
receiptIdentities: Array<Pick<IOFormatBoundRuntimeReceiptSetIR["receipts"][number], "format" | "family" | "operation" | "operator" | "registered" | "rnaIdentifier" | "buildOption" | "buildOptionEnabled" | "runtimeStatus" | "variants" | "extensions" | "sourceSha256" | "settingsSha256" | "runtimeSha256">>;
}
export type IOFormatReceiptFreshnessFailure =
| "RECEIPT_INVALID"
| "RECEIPT_FORGED"
| "RECEIPT_STALE"
| "RECEIPT_CROSS_VERSION";
export class IOFormatReceiptFreshnessError extends Error {
readonly code = "IO_FORMAT_UNSUPPORTED" as const;
readonly reason: IOFormatReceiptFreshnessFailure;
constructor(reason: IOFormatReceiptFreshnessFailure, message: string) {
super(`IO_FORMAT_UNSUPPORTED: ${reason}: ${message}`);
this.name = "IOFormatReceiptFreshnessError";
this.reason = reason;
}
}
const SHA256 = /^[a-f0-9]{64}$/;
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", `${path} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(value: Record<string, unknown>, expected: readonly string[], path: string): void {
const actual = Object.keys(value).sort();
const allowed = [...expected].sort();
if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) {
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", `${path} contains undeclared fields`);
}
}
function sha(value: unknown, path: string): string {
if (typeof value !== "string" || !SHA256.test(value)) {
throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", `${path} is not SHA-256`);
}
return value;
}
function stableValue(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stableValue);
if (typeof value === "object" && value !== null) {
return Object.fromEntries(Object.keys(value as Record<string, unknown>).sort().map((key) => [key, stableValue((value as Record<string, unknown>)[key])]));
}
return value;
}
function stableJSON(value: unknown): string {
return JSON.stringify(stableValue(value));
}
function sameRuntime(left: IOFormatRuntimeIdentityIR, right: IOFormatRuntimeIdentityIR): boolean {
return stableJSON(left) === stableJSON(right);
}
export function parseIOFormatReceiptFreshness(value: unknown): IOFormatReceiptFreshnessEnvelopeIR {
const input = record(value, "input");
exactKeys(input, ["schemaVersion", "task", "parentBindingSha256", "boundReceiptSetSha256", "runtimeSha256", "bound"], "input");
if (input.schemaVersion !== IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA || input.task !== IO_FORMAT_RECEIPT_FRESHNESS_TASK) {
throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", "freshness envelope header is invalid");
}
const boundInput = record(input.bound, "bound");
const parentReceiptSetSha256 = sha(boundInput.parentReceiptSetSha256, "bound.parentReceiptSetSha256");
const inventorySha256 = sha(boundInput.inventorySha256, "bound.inventorySha256");
let bound: IOFormatBoundRuntimeReceiptSetIR;
try {
bound = validateIOFormatBoundReceiptSet(input.bound, parentReceiptSetSha256, inventorySha256);
}
catch (error) {
if (error instanceof IOFormatReceiptFreshnessError) throw error;
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", error instanceof Error ? error.message : "bound receipt set is invalid");
}
return {
schemaVersion: IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA,
task: IO_FORMAT_RECEIPT_FRESHNESS_TASK,
parentBindingSha256: sha(input.parentBindingSha256, "input.parentBindingSha256"),
boundReceiptSetSha256: sha(input.boundReceiptSetSha256, "input.boundReceiptSetSha256"),
runtimeSha256: sha(input.runtimeSha256, "input.runtimeSha256"),
bound,
};
}
function expectedHashes(expected: IOFormatReceiptFreshnessExpectedIR): void {
sha(expected.parentBindingSha256, "expected.parentBindingSha256");
sha(expected.parentReceiptSetSha256, "expected.parentReceiptSetSha256");
sha(expected.inventorySha256, "expected.inventorySha256");
sha(expected.boundReceiptSetSha256, "expected.boundReceiptSetSha256");
sha(expected.runtimeSha256, "expected.runtimeSha256");
if (!Array.isArray(expected.receiptIdentities) || expected.receiptIdentities.length !== 14) {
throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", "expected receipt identity set is incomplete");
}
}
export function validateIOFormatReceiptFreshness(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR): IOFormatReceiptFreshnessEnvelopeIR {
expectedHashes(expected);
const parsed = parseIOFormatReceiptFreshness(value);
if (parsed.parentBindingSha256 !== expected.parentBindingSha256 || parsed.bound.parentReceiptSetSha256 !== expected.parentReceiptSetSha256 || parsed.bound.inventorySha256 !== expected.inventorySha256) {
throw new IOFormatReceiptFreshnessError("RECEIPT_STALE", "receipt parent or inventory identity is stale");
}
if (parsed.boundReceiptSetSha256 !== expected.boundReceiptSetSha256) {
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "receipt-set content identity does not match the trusted build");
}
if (parsed.runtimeSha256 !== expected.runtimeSha256 || !sameRuntime(parsed.bound.runtime, expected.runtime)) {
throw new IOFormatReceiptFreshnessError("RECEIPT_CROSS_VERSION", "runtime identity does not match the trusted build");
}
for (const receipt of parsed.bound.receipts) {
if (receipt.runtimeSha256 !== expected.runtimeSha256) {
throw new IOFormatReceiptFreshnessError("RECEIPT_CROSS_VERSION", `${receipt.format}:${receipt.operation} runtime identity is stale`);
}
const expectedReceipt = expected.receiptIdentities.find((candidate) => candidate.format === receipt.format && candidate.operation === receipt.operation);
if (!expectedReceipt || stableJSON(receipt) !== stableJSON(expectedReceipt)) {
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", `${receipt.format}:${receipt.operation} receipt content is not trusted`);
}
}
return parsed;
}
async function sha256Text(value: string): Promise<string> {
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
/** Recomputes the canonical receipt-set and runtime digests for an independent checker. */
export async function verifyIOFormatReceiptFreshness(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR): Promise<IOFormatReceiptFreshnessEnvelopeIR> {
const parsed = validateIOFormatReceiptFreshness(value, expected);
if (await sha256Text(stableJSON(parsed.bound)) !== parsed.boundReceiptSetSha256) {
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "receipt-set canonical digest does not match its contents");
}
if (await sha256Text(stableJSON(parsed.bound.runtime)) !== parsed.runtimeSha256) {
throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "runtime canonical digest does not match its contents");
}
return parsed;
}
export type IOFormatFreshRuntimeRouteResult =
| { status: "READY"; format: IOFormatRuntimeRouteQuery["format"]; operation: IOFormatRuntimeRouteQuery["operation"]; operator: string; receipt: IOFormatReceiptFreshnessEnvelopeIR["bound"]["receipts"][number]; freshness: "VERIFIED" }
| { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; reason: IOFormatReceiptFreshnessFailure | "UNAVAILABLE"; format: IOFormatRuntimeRouteQuery["format"]; operation: IOFormatRuntimeRouteQuery["operation"] };
export function resolveFreshIOFormatRuntimeRoute(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR, query: IOFormatRuntimeRouteQuery): IOFormatFreshRuntimeRouteResult {
try {
const parsed = validateIOFormatReceiptFreshness(value, expected);
const receipt = parsed.bound.receipts.find((candidate) => candidate.format === query.format && candidate.operation === query.operation);
if (!receipt || receipt.runtimeStatus !== "AVAILABLE" || receipt.registered !== true || receipt.rnaIdentifier === null || receipt.buildOptionEnabled === false) {
return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", reason: "UNAVAILABLE", format: query.format, operation: query.operation };
}
return { status: "READY", format: query.format, operation: query.operation, operator: receipt.operator, receipt, freshness: "VERIFIED" };
}
catch (error) {
const reason = error instanceof IOFormatReceiptFreshnessError ? error.reason : "RECEIPT_INVALID";
return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", reason, format: query.format, operation: query.operation };
}
}