fix(release): authenticate reusable validation evidence

This commit is contained in:
Vincent Koc
2026-08-21 03:57:40 -07:00
parent 465535bb20
commit 41b920c19c
5 changed files with 611 additions and 26 deletions
@@ -0,0 +1,41 @@
import type {
ReleaseValidationSourceArtifact,
ReleaseValidationVerifiedArtifactEvidence,
} from "./release-validation-receipt-contract.mjs";
export type GitHubReleaseValidationArtifactEvidence = ReleaseValidationSourceArtifact & {
entry_bytes: string;
};
export type GitHubReleaseValidationArtifactExpected = {
repository: string;
workflowPath: string;
workflowSha: string;
};
export function authenticateGitHubReleaseValidationArtifact(params: {
evidence: GitHubReleaseValidationArtifactEvidence;
expected: GitHubReleaseValidationArtifactExpected;
artifactMetadata: unknown;
workflowRun: unknown;
archiveBytes: Uint8Array;
nowMs: number;
}): ReleaseValidationVerifiedArtifactEvidence;
export function downloadAndAuthenticateGitHubReleaseValidationArtifact(params: {
evidence: GitHubReleaseValidationArtifactEvidence;
expected: GitHubReleaseValidationArtifactExpected & {
artifactSizeBytes: number;
runStatePolicy: "completed-success" | "same-run-producer-success";
workflowEvent: string;
workflowHeadBranch: string;
consumerRunAttempt?: number;
producerJobName?: string;
};
token: string;
nowMs: number;
fetchImpl?: typeof fetch;
timeoutMs?: number;
retryAttempts?: number;
retryDelayMs?: number;
}): Promise<ReleaseValidationVerifiedArtifactEvidence>;
@@ -0,0 +1,185 @@
import {
downloadActionsArtifactArchive,
inspectActionsArtifactZipWithPolicy,
sha256Digest,
} from "./lib/actions-artifact-archive.mjs";
import { isRecord } from "./lib/record-shared.mjs";
import {
RELEASE_VALIDATION_RECEIPT_MAX_BYTES,
verifyReleaseValidationArtifactEvidence,
} from "./release-validation-receipt-contract.mjs";
const MAX_ARCHIVE_BYTES = 16 * 1024 * 1024;
const REPOSITORY = "openclaw/openclaw";
const WORKFLOW_PATH = ".github/workflows/full-release-validation.yml";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
function fail(message) {
throw new Error(message);
}
function positiveInteger(value, label) {
if (!Number.isSafeInteger(value) || value < 1) {
fail(`${label} must be a positive safe integer`);
}
return value;
}
function nonNegativeInteger(value, label) {
if (!Number.isSafeInteger(value) || value < 0) {
fail(`${label} must be a non-negative safe integer`);
}
return value;
}
function string(value, label) {
if (typeof value !== "string" || value.length === 0) {
fail(`${label} must be a non-empty string`);
}
return value;
}
function repositoryName(value, label) {
if (!isRecord(value) || typeof value.full_name !== "string") {
fail(`${label} must include full_name`);
}
return value.full_name;
}
function timestampMs(value, label) {
const text = string(value, label);
const milliseconds = Date.parse(text);
if (!Number.isFinite(milliseconds)) {
fail(`${label} must be a valid timestamp`);
}
return milliseconds;
}
export function authenticateGitHubReleaseValidationArtifact(params) {
if (!isRecord(params)) {
fail("GitHub release validation artifact authentication parameters are required");
}
const evidence = params.evidence;
const expected = params.expected;
const artifact = params.artifactMetadata;
const run = params.workflowRun;
if (!isRecord(evidence) || !isRecord(expected) || !isRecord(artifact) || !isRecord(run)) {
fail("GitHub release validation artifact metadata is incomplete");
}
if (!(params.archiveBytes instanceof Uint8Array)) {
fail("GitHub release validation artifact archiveBytes must be a Uint8Array");
}
const archiveBytes = Buffer.from(
params.archiveBytes.buffer,
params.archiveBytes.byteOffset,
params.archiveBytes.byteLength,
);
const nowMs = nonNegativeInteger(params.nowMs, "GitHub artifact authentication nowMs");
const repository = string(expected.repository, "GitHub artifact expected repository");
const workflowPath = string(expected.workflowPath, "GitHub artifact expected workflow path");
const workflowSha = string(expected.workflowSha, "GitHub artifact expected workflow SHA");
if (
repository !== REPOSITORY ||
workflowPath !== WORKFLOW_PATH ||
!SHA_PATTERN.test(workflowSha)
) {
fail("GitHub artifact expected repository or workflow authority is unsupported");
}
const artifactId = positiveInteger(
Number(evidence.artifact_id),
"GitHub artifact evidence artifact_id",
);
const runId = positiveInteger(Number(evidence.run_id), "GitHub artifact evidence run_id");
const runAttempt = positiveInteger(evidence.run_attempt, "GitHub artifact evidence run_attempt");
const createdAtMs = timestampMs(evidence.created_at, "GitHub artifact evidence created_at");
const expiresAtMs = timestampMs(evidence.expires_at, "GitHub artifact evidence expires_at");
if (
artifact.id !== artifactId ||
artifact.name !== evidence.artifact_name ||
artifact.digest !== evidence.archive_digest ||
artifact.created_at !== evidence.created_at ||
artifact.expires_at !== evidence.expires_at ||
artifact.expired !== false ||
artifact.size_in_bytes !== archiveBytes.byteLength ||
!isRecord(artifact.workflow_run) ||
artifact.workflow_run.id !== runId ||
artifact.workflow_run.head_sha !== workflowSha
) {
fail("GitHub artifact metadata differs from the authenticated evidence tuple");
}
if (
run.id !== runId ||
run.run_attempt !== runAttempt ||
run.path !== workflowPath ||
run.head_sha !== workflowSha ||
repositoryName(run.repository, "GitHub workflow repository") !== repository ||
repositoryName(run.head_repository, "GitHub workflow head repository") !== repository
) {
fail("GitHub workflow metadata differs from the authenticated evidence tuple");
}
if (createdAtMs > nowMs || createdAtMs >= expiresAtMs || expiresAtMs <= nowMs) {
fail("GitHub artifact is expired or has invalid creation/expiry timestamps");
}
if (sha256Digest(archiveBytes) !== evidence.archive_digest) {
fail("downloaded GitHub artifact archive digest differs from metadata");
}
const files = inspectActionsArtifactZipWithPolicy(archiveBytes, {
expectedEntries: [evidence.entry_name],
maxArchiveBytes: MAX_ARCHIVE_BYTES,
maxExpandedBytes: RELEASE_VALIDATION_RECEIPT_MAX_BYTES,
maxEntryBytes: () => RELEASE_VALIDATION_RECEIPT_MAX_BYTES,
});
const entryBytes = files.get(evidence.entry_name);
if (
!entryBytes ||
typeof evidence.entry_bytes !== "string" ||
!entryBytes.equals(Buffer.from(evidence.entry_bytes, "ascii"))
) {
fail("GitHub artifact entry bytes differ from the authenticated evidence");
}
return verifyReleaseValidationArtifactEvidence(evidence, () => true);
}
export async function downloadAndAuthenticateGitHubReleaseValidationArtifact(params) {
if (!isRecord(params) || !isRecord(params.evidence) || !isRecord(params.expected)) {
fail("GitHub release validation artifact download parameters are required");
}
const evidence = params.evidence;
const expected = params.expected;
const downloaded = await downloadActionsArtifactArchive({
expected: {
artifactDigest: evidence.archive_digest,
artifactId: Number(evidence.artifact_id),
artifactName: evidence.artifact_name,
artifactSizeBytes: expected.artifactSizeBytes,
repository: expected.repository,
runStatePolicy: expected.runStatePolicy,
runAttempt: evidence.run_attempt,
runId: Number(evidence.run_id),
workflowEvent: expected.workflowEvent,
workflowHeadBranch: expected.workflowHeadBranch,
workflowPath: expected.workflowPath,
workflowSha: expected.workflowSha,
...(expected.consumerRunAttempt === undefined
? {}
: { consumerRunAttempt: expected.consumerRunAttempt }),
...(expected.producerJobName === undefined
? {}
: { producerJobName: expected.producerJobName }),
},
token: params.token,
...(params.fetchImpl === undefined ? {} : { fetchImpl: params.fetchImpl }),
...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
...(params.retryAttempts === undefined ? {} : { retryAttempts: params.retryAttempts }),
...(params.retryDelayMs === undefined ? {} : { retryDelayMs: params.retryDelayMs }),
maxArchiveBytes: MAX_ARCHIVE_BYTES,
});
return authenticateGitHubReleaseValidationArtifact({
evidence,
expected,
artifactMetadata: downloaded.artifactMetadata,
workflowRun: downloaded.workflowRun,
archiveBytes: downloaded.archiveBytes,
nowMs: params.nowMs,
});
}
@@ -90,6 +90,7 @@ export type ReleaseValidationSourceArtifact = {
archive_digest: ReleaseValidationReceiptDigest;
content_digest: ReleaseValidationReceiptDigest;
created_at: string;
expires_at: string;
url: string;
};
@@ -213,6 +214,15 @@ export const RELEASE_VALIDATION_RECEIPT_LOCATOR_SCHEMA: "openclaw.release-valida
export const RELEASE_VALIDATION_POLICY_ID: "openclaw.release-validation-policy.v1";
export const RELEASE_VALIDATION_RECEIPT_MAX_BYTES: number;
export const RELEASE_VALIDATION_RECEIPT_LOCATOR_MAX_BYTES: number;
export const RELEASE_VALIDATION_REUSE_POLICIES: Readonly<
Record<
ReleaseValidationIntent,
Readonly<{
max_age_ms: number;
cadence_ms: number;
}>
>
>;
export function validateReleaseValidationExecutionPlanSource(
value: unknown,
): ReleaseValidationExecutionPlanSource;
@@ -241,6 +251,19 @@ export function verifyReleaseValidationReceipt(
receiptValue: unknown,
input: ReleaseValidationReceiptSealInput,
): ReleaseValidationVerifiedReceipt;
export function validateReleaseValidationReceiptReuseFreshness(
receiptValue: ReleaseValidationVerifiedReceipt,
options: {
now_ms: number;
max_future_skew_ms: number;
},
): {
intent: ReleaseValidationIntent;
age_ms: number;
max_age_ms: number;
cadence_ms: number;
expires_at_ms: number;
};
export function canonicalReleaseValidationReceiptJson(value: unknown): string;
export function releaseValidationReceiptDigest(value: unknown): ReleaseValidationReceiptDigest;
export function parseReleaseValidationReceiptJson(text: string): ReleaseValidationReceipt;
@@ -19,6 +19,28 @@ export const RELEASE_VALIDATION_RECEIPT_LOCATOR_SCHEMA =
export const RELEASE_VALIDATION_POLICY_ID = "openclaw.release-validation-policy.v1";
export const RELEASE_VALIDATION_RECEIPT_MAX_BYTES = 256 * 1024;
export const RELEASE_VALIDATION_RECEIPT_LOCATOR_MAX_BYTES = 16 * 1024;
export const RELEASE_VALIDATION_REUSE_POLICIES = Object.freeze({
"release-beta": Object.freeze({
max_age_ms: 6 * 60 * 60 * 1000,
cadence_ms: 6 * 60 * 60 * 1000,
}),
"release-stable": Object.freeze({
max_age_ms: 6 * 60 * 60 * 1000,
cadence_ms: 6 * 60 * 60 * 1000,
}),
"main-daily": Object.freeze({
max_age_ms: 24 * 60 * 60 * 1000,
cadence_ms: 24 * 60 * 60 * 1000,
}),
"main-weekly": Object.freeze({
max_age_ms: 7 * 24 * 60 * 60 * 1000,
cadence_ms: 7 * 24 * 60 * 60 * 1000,
}),
"diagnostic-full": Object.freeze({
max_age_ms: 7 * 24 * 60 * 60 * 1000,
cadence_ms: 7 * 24 * 60 * 60 * 1000,
}),
});
const REPOSITORY = "openclaw/openclaw";
const WORKFLOW_PATH = ".github/workflows/full-release-validation.yml";
@@ -593,6 +615,7 @@ export function verifyReleaseValidationArtifactEvidence(value, authenticate) {
"archive_digest",
"content_digest",
"created_at",
"expires_at",
"url",
"entry_bytes",
],
@@ -617,6 +640,7 @@ export function verifyReleaseValidationArtifactEvidence(value, authenticate) {
archive_digest: digest(artifact.archive_digest, `${label}.archive_digest`),
content_digest: digest(artifact.content_digest, `${label}.content_digest`),
created_at: timestamp(artifact.created_at, `${label}.created_at`),
expires_at: timestamp(artifact.expires_at, `${label}.expires_at`),
url: exactArtifactUrl(artifact.url, parentRunId, artifactId, `${label}.url`),
entry_bytes: artifact.entry_bytes,
});
@@ -659,6 +683,7 @@ function validateSourceArtifacts(value, sources) {
"archive_digest",
"content_digest",
"created_at",
"expires_at",
"url",
"entry_bytes",
],
@@ -680,6 +705,7 @@ function validateSourceArtifacts(value, sources) {
archive_digest: digest(artifact.archive_digest, `${label}.archive_digest`),
content_digest: digest(artifact.content_digest, `${label}.content_digest`),
created_at: timestamp(artifact.created_at, `${label}.created_at`),
expires_at: timestamp(artifact.expires_at, `${label}.expires_at`),
url: exactArtifactUrl(artifact.url, parentRunId, artifactId, `${label}.url`),
};
if (
@@ -696,6 +722,7 @@ function validateSourceArtifacts(value, sources) {
) ||
result.entry_name !== REQUIRED_ARTIFACTS[kind].entry ||
result.content_digest !== exactBytesDigest(expectedBytes[kind]) ||
Date.parse(result.expires_at) <= Date.parse(result.created_at) ||
artifact.entry_bytes !== expectedBytes[kind]
) {
fail(`${label} coordinates differ from its source object`);
@@ -873,7 +900,11 @@ export function sealReleaseValidationReceipt(input) {
});
const sealedAt = timestamp(input.sealedAt, "release validation receipt sealed_at");
if (
sourceArtifacts.some((artifact) => Date.parse(artifact.created_at) > Date.parse(sealedAt)) ||
sourceArtifacts.some(
(artifact) =>
Date.parse(artifact.created_at) > Date.parse(sealedAt) ||
Date.parse(artifact.expires_at) <= Date.parse(sealedAt),
) ||
Date.parse(diagnosticDrain.observed_at) > Date.parse(sealedAt)
) {
fail("release validation receipt was sealed before its sources completed");
@@ -1092,6 +1123,7 @@ function validateReceiptArtifacts(value, context) {
"archive_digest",
"content_digest",
"created_at",
"expires_at",
"url",
],
label,
@@ -1112,6 +1144,7 @@ function validateReceiptArtifacts(value, context) {
archive_digest: digest(artifact.archive_digest, `${label}.archive_digest`),
content_digest: digest(artifact.content_digest, `${label}.content_digest`),
created_at: timestamp(artifact.created_at, `${label}.created_at`),
expires_at: timestamp(artifact.expires_at, `${label}.expires_at`),
url: exactArtifactUrl(artifact.url, artifactRunId, artifactId, `${label}.url`),
};
const required = expected[kind];
@@ -1120,7 +1153,8 @@ function validateReceiptArtifacts(value, context) {
result.run_attempt !== required.attempt ||
result.artifact_name !== required.name ||
result.entry_name !== required.entry ||
result.content_digest !== required.digest
result.content_digest !== required.digest ||
Date.parse(result.expires_at) <= Date.parse(result.created_at)
) {
fail(`${label} coordinates differ from its receipt sources`);
}
@@ -1148,7 +1182,10 @@ function validateReceiptArtifacts(value, context) {
Date.parse(byKind["execution-plan"].created_at) < startedAt ||
Date.parse(byKind["execution-plan"].created_at) > decisionAt ||
Date.parse(byKind["release-plan-lock"].created_at) > decisionAt ||
artifacts.some((artifact) => Date.parse(artifact.created_at) > sealedAt)
artifacts.some(
(artifact) =>
Date.parse(artifact.created_at) > sealedAt || Date.parse(artifact.expires_at) <= sealedAt,
)
) {
fail("release validation receipt source artifact timestamps are invalid");
}
@@ -1407,6 +1444,53 @@ export function verifyReleaseValidationReceipt(receiptValue, input) {
return receipt;
}
export function validateReleaseValidationReceiptReuseFreshness(receiptValue, optionsValue) {
const receipt = authenticatedReceipt(receiptValue, "release validation receipt reuse candidate");
const options = object(optionsValue, "release validation receipt reuse options");
exactKeys(options, ["now_ms", "max_future_skew_ms"], "release validation receipt reuse options");
const nowMs = nonNegativeInteger(options.now_ms, "release validation receipt reuse now_ms");
const futureSkewMs = nonNegativeInteger(
options.max_future_skew_ms,
"release validation receipt reuse max_future_skew_ms",
);
const policy = RELEASE_VALIDATION_REUSE_POLICIES[receipt.validation.intent];
const maxAgeMs = policy.max_age_ms;
const cadenceMs = policy.cadence_ms;
const sealedAtMs = Date.parse(receipt.timestamps.sealed_at);
const sourceTimes = [
receipt.timestamps.started_at,
receipt.timestamps.decision_at,
receipt.timestamps.drain_completed_at,
receipt.timestamps.sealed_at,
...receipt.source_artifacts.map((artifact) => artifact.created_at),
].map(Date.parse);
const futureBoundaryMs = nowMs + futureSkewMs;
if (
!Number.isSafeInteger(futureBoundaryMs) ||
sourceTimes.some((value) => value > futureBoundaryMs)
) {
fail("release validation receipt reuse evidence is newer than the allowed future skew");
}
const policyExpiryMs = sealedAtMs + Math.min(maxAgeMs, cadenceMs);
if (!Number.isSafeInteger(policyExpiryMs)) {
fail("release validation receipt reuse policy expiry exceeds safe integer range");
}
const artifactExpiryMs = Math.min(
...receipt.source_artifacts.map((artifact) => Date.parse(artifact.expires_at)),
);
const expiresAtMs = Math.min(policyExpiryMs, artifactExpiryMs);
if (nowMs >= expiresAtMs) {
fail("release validation receipt reuse evidence is expired");
}
return {
intent: receipt.validation.intent,
age_ms: Math.max(0, nowMs - sealedAtMs),
max_age_ms: maxAgeMs,
cadence_ms: cadenceMs,
expires_at_ms: expiresAtMs,
};
}
export function canonicalReleaseValidationReceiptJson(value) {
return canonicalReleaseJson(validateReleaseValidationReceipt(value));
}
@@ -7,6 +7,7 @@ import {
createReleasePlanLock,
releaseCanonicalDigest,
} from "../../scripts/release-plan-contract.mjs";
import { authenticateGitHubReleaseValidationArtifact } from "../../scripts/release-validation-github-artifact-authenticator.mjs";
import {
canonicalReleaseValidationReceiptJson,
canonicalReleaseValidationReceiptLocatorJson,
@@ -18,6 +19,7 @@ import {
validateReleaseValidationExecutionPlanSource,
validateReleaseValidationReceipt,
validateReleaseValidationReceiptLocatorForReceipt,
validateReleaseValidationReceiptReuseFreshness,
validateReleaseValidationStateSource,
verifyReleaseValidationArtifactEvidence,
verifyReleaseValidationReceipt,
@@ -45,8 +47,48 @@ function addSeconds(value: string, seconds: number): string {
return new Date(Date.parse(value) + seconds * 1000).toISOString().replace(".000Z", "Z");
}
function exactBytesDigest(value: string): `sha256:${string}` {
return `sha256:${createHash("sha256").update(value, "ascii").digest("hex")}`;
function exactBytesDigest(value: string | Uint8Array): `sha256:${string}` {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
function crc32(bytes: Buffer): number {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function createZip(name: string, bytes: Buffer): Buffer {
const nameBytes = Buffer.from(name, "utf8");
const checksum = crc32(bytes);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt32LE(checksum, 14);
local.writeUInt32LE(bytes.length, 18);
local.writeUInt32LE(bytes.length, 22);
local.writeUInt16LE(nameBytes.length, 26);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(0x0314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt32LE(checksum, 16);
central.writeUInt32LE(bytes.length, 20);
central.writeUInt32LE(bytes.length, 24);
central.writeUInt16LE(nameBytes.length, 28);
central.writeUInt32LE((0o100600 * 0x10000) >>> 0, 38);
const centralOffset = local.length + nameBytes.length + bytes.length;
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(1, 8);
end.writeUInt16LE(1, 10);
end.writeUInt32LE(central.length + nameBytes.length, 12);
end.writeUInt32LE(centralOffset, 16);
return Buffer.concat([local, nameBytes, bytes, central, nameBytes, end]);
}
function job(
@@ -190,6 +232,51 @@ type Fixture = FixtureBase & {
sourceArtifacts: ReleaseValidationVerifiedArtifactEvidence[];
};
type RawArtifactEvidence = ReleaseValidationSourceArtifact & {
entry_bytes: string;
};
function githubArtifactAuthenticationParams(evidence: RawArtifactEvidence) {
const archiveBytes = createZip(evidence.entry_name, Buffer.from(evidence.entry_bytes, "ascii"));
return {
evidence: { ...evidence, archive_digest: exactBytesDigest(archiveBytes) },
expected: {
repository: "openclaw/openclaw",
workflowPath: ".github/workflows/full-release-validation.yml",
workflowSha: TOOLING_SHA,
},
artifactMetadata: {
id: Number(evidence.artifact_id),
name: evidence.artifact_name,
digest: exactBytesDigest(archiveBytes),
created_at: evidence.created_at,
expires_at: evidence.expires_at,
expired: false,
size_in_bytes: archiveBytes.length,
workflow_run: { id: Number(evidence.run_id), head_sha: TOOLING_SHA },
},
workflowRun: {
id: Number(evidence.run_id),
run_attempt: evidence.run_attempt,
path: ".github/workflows/full-release-validation.yml",
head_sha: TOOLING_SHA,
repository: { full_name: "openclaw/openclaw" },
head_repository: { full_name: "openclaw/openclaw" },
},
archiveBytes,
nowMs: Math.min(
Date.parse(evidence.expires_at) - 1,
Date.parse(evidence.created_at) + 60 * 60 * 1000,
),
};
}
function authenticateEvidence(
evidence: RawArtifactEvidence,
): ReleaseValidationVerifiedArtifactEvidence {
return authenticateGitHubReleaseValidationArtifact(githubArtifactAuthenticationParams(evidence));
}
function sourceArtifacts(fixture: FixtureBase): ReleaseValidationVerifiedArtifactEvidence[] {
const coordinates = [
{
@@ -200,7 +287,6 @@ function sourceArtifacts(fixture: FixtureBase): ReleaseValidationVerifiedArtifac
run_attempt: fixture.decision.parent_run_attempt,
content: fixture.decision,
created_at: addSeconds(fixture.decision.observed_at, 60),
archive: "1",
},
{
kind: "diagnostic-drain",
@@ -210,7 +296,6 @@ function sourceArtifacts(fixture: FixtureBase): ReleaseValidationVerifiedArtifac
run_attempt: fixture.diagnosticDrain.parent_run_attempt,
content: fixture.diagnosticDrain,
created_at: addSeconds(fixture.diagnosticDrain.observed_at, 60),
archive: "2",
},
{
kind: "execution-plan",
@@ -220,7 +305,6 @@ function sourceArtifacts(fixture: FixtureBase): ReleaseValidationVerifiedArtifac
run_attempt: fixture.executionPlan.parent_run_attempt,
content: fixture.executionPlan,
created_at: addSeconds(fixture.executionPlan.started_at, 60),
archive: "3",
},
{
kind: "release-plan-lock",
@@ -230,28 +314,25 @@ function sourceArtifacts(fixture: FixtureBase): ReleaseValidationVerifiedArtifac
run_attempt: fixture.executionPlan.parent_run_attempt,
content: fixture.releasePlanLock,
created_at: addSeconds(fixture.executionPlan.started_at, -60),
archive: "4",
},
] as const;
return coordinates.map((artifact) => {
const entryBytes = canonicalReleaseJson(artifact.content);
return verifyReleaseValidationArtifactEvidence(
{
kind: artifact.kind,
artifact_id: artifact.artifact_id,
artifact_name: artifact.artifact_name,
entry_name: artifact.entry_name,
run_id: PARENT_RUN_ID,
run_attempt: artifact.run_attempt,
archive_digest:
`sha256:${artifact.archive.repeat(64)}` as ReleaseValidationSourceArtifact["archive_digest"],
content_digest: exactBytesDigest(entryBytes),
created_at: artifact.created_at,
url: `${PARENT_RUN_URL}/artifacts/${artifact.artifact_id}`,
entry_bytes: entryBytes,
},
() => true,
);
const expiresAt = addSeconds(artifact.created_at, 7 * 24 * 60 * 60);
return authenticateEvidence({
kind: artifact.kind,
artifact_id: artifact.artifact_id,
artifact_name: artifact.artifact_name,
entry_name: artifact.entry_name,
run_id: PARENT_RUN_ID,
run_attempt: artifact.run_attempt,
archive_digest: `sha256:${"0".repeat(64)}`,
content_digest: exactBytesDigest(entryBytes),
created_at: artifact.created_at,
expires_at: expiresAt,
url: `${PARENT_RUN_URL}/artifacts/${artifact.artifact_id}`,
entry_bytes: entryBytes,
});
});
}
@@ -568,6 +649,65 @@ describe("release validation receipt source sealer", () => {
"coordinates differ from its source object",
);
});
it("rejects mismatched GitHub metadata, expiry, workflow identity, and archive bytes", () => {
const raw = structuredClone(
inputFixture().sourceArtifacts[0]!,
) as unknown as RawArtifactEvidence;
const expired = githubArtifactAuthenticationParams(raw);
(expired.artifactMetadata as Record<string, unknown>).expired = true;
expect(() => authenticateGitHubReleaseValidationArtifact(expired)).toThrow("metadata differs");
const wrongCreated = githubArtifactAuthenticationParams(raw);
(wrongCreated.artifactMetadata as Record<string, unknown>).created_at = "2026-08-21T10:02:00Z";
expect(() => authenticateGitHubReleaseValidationArtifact(wrongCreated)).toThrow(
"metadata differs",
);
const wrongWorkflow = githubArtifactAuthenticationParams(raw);
(wrongWorkflow.workflowRun as Record<string, unknown>).path = ".github/workflows/ci.yml";
expect(() => authenticateGitHubReleaseValidationArtifact(wrongWorkflow)).toThrow(
"workflow metadata differs",
);
for (const mutate of [
(params: ReturnType<typeof githubArtifactAuthenticationParams>) => {
(params.artifactMetadata as Record<string, unknown>).id = 9999;
},
(params: ReturnType<typeof githubArtifactAuthenticationParams>) => {
(params.artifactMetadata as Record<string, unknown>).name = "wrong-name";
},
(params: ReturnType<typeof githubArtifactAuthenticationParams>) => {
(params.artifactMetadata as Record<string, unknown>).digest = `sha256:${"f".repeat(64)}`;
},
(params: ReturnType<typeof githubArtifactAuthenticationParams>) => {
(params.workflowRun as Record<string, unknown>).run_attempt = 99;
},
(params: ReturnType<typeof githubArtifactAuthenticationParams>) => {
(params.workflowRun as Record<string, any>).repository.full_name = "other/repo";
},
]) {
const mismatched = githubArtifactAuthenticationParams(raw);
mutate(mismatched);
expect(() => authenticateGitHubReleaseValidationArtifact(mismatched)).toThrow(
/metadata differs/,
);
}
const unsupportedAuthority = githubArtifactAuthenticationParams(raw);
unsupportedAuthority.expected.repository = "other/repo";
expect(() => authenticateGitHubReleaseValidationArtifact(unsupportedAuthority)).toThrow(
"authority is unsupported",
);
const tamperedArchive = githubArtifactAuthenticationParams(raw);
tamperedArchive.archiveBytes = Buffer.from(tamperedArchive.archiveBytes);
tamperedArchive.archiveBytes.writeUInt8(tamperedArchive.archiveBytes.readUInt8(40) ^ 1, 40);
expect(() => authenticateGitHubReleaseValidationArtifact(tamperedArchive)).toThrow(
"archive digest differs",
);
});
});
describe("release validation receipt lineage", () => {
@@ -665,6 +805,118 @@ describe("release validation receipt lineage", () => {
});
});
describe("release validation receipt reuse freshness", () => {
const sealedAtMs = Date.parse("2026-08-21T10:32:00Z");
it("selects the intent policy and returns the bounded effective expiry", () => {
const receipt = sealReleaseValidationReceipt(inputFixture());
expect(
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: sealedAtMs + 60 * 60 * 1000,
max_future_skew_ms: 60_000,
}),
).toEqual({
intent: "release-beta",
age_ms: 60 * 60 * 1000,
max_age_ms: 6 * 60 * 60 * 1000,
cadence_ms: 6 * 60 * 60 * 1000,
expires_at_ms: sealedAtMs + 6 * 60 * 60 * 1000,
});
});
it("rejects invalid clocks and evidence beyond the allowed future skew", () => {
const receipt = sealReleaseValidationReceipt(inputFixture());
expect(() =>
validateReleaseValidationReceiptReuseFreshness(structuredClone(receipt), {
now_ms: sealedAtMs,
max_future_skew_ms: 0,
}),
).toThrow("authenticated release validation receipt");
for (const nowMs of [Number.NaN, Number.POSITIVE_INFINITY, -1, 1.5]) {
expect(() =>
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: nowMs,
max_future_skew_ms: 60_000,
}),
).toThrow("now_ms");
}
for (const futureSkewMs of [Number.NaN, Number.POSITIVE_INFINITY, -1, 1.5]) {
expect(() =>
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: sealedAtMs,
max_future_skew_ms: futureSkewMs,
}),
).toThrow("max_future_skew_ms");
}
expect(() =>
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: sealedAtMs - 61_000,
max_future_skew_ms: 60_000,
}),
).toThrow("future skew");
expect(() =>
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: Number.MAX_SAFE_INTEGER,
max_future_skew_ms: 1,
}),
).toThrow("future skew");
});
it("expires at the policy window or earlier authenticated artifact expiry", () => {
const receipt = sealReleaseValidationReceipt(inputFixture());
expect(() =>
validateReleaseValidationReceiptReuseFreshness(receipt, {
now_ms: sealedAtMs + 6 * 60 * 60 * 1000,
max_future_skew_ms: 0,
}),
).toThrow("expired");
const artifactBoundInput = inputFixture();
const shortLived = structuredClone(
artifactBoundInput.sourceArtifacts[0]!,
) as unknown as RawArtifactEvidence;
shortLived.expires_at = addSeconds("2026-08-21T10:32:00Z", 30 * 60);
artifactBoundInput.sourceArtifacts[0] = authenticateEvidence(shortLived);
const artifactBoundReceipt = sealReleaseValidationReceipt(artifactBoundInput);
expect(() =>
validateReleaseValidationReceiptReuseFreshness(artifactBoundReceipt, {
now_ms: sealedAtMs + 30 * 60 * 1000,
max_future_skew_ms: 0,
}),
).toThrow("expired");
});
it("uses fixed per-intent policy and rejects caller-supplied policy overrides", () => {
const betaReceipt = sealReleaseValidationReceipt(inputFixture());
expect(() =>
validateReleaseValidationReceiptReuseFreshness(betaReceipt, {
now_ms: sealedAtMs + 7 * 60 * 60 * 1000,
max_future_skew_ms: 0,
}),
).toThrow("expired");
const dailyReceipt = sealReleaseValidationReceipt(mainDailyInputFixture());
expect(
validateReleaseValidationReceiptReuseFreshness(dailyReceipt, {
now_ms: sealedAtMs + 7 * 60 * 60 * 1000,
max_future_skew_ms: 0,
}),
).toMatchObject({
intent: "main-daily",
max_age_ms: 24 * 60 * 60 * 1000,
cadence_ms: 24 * 60 * 60 * 1000,
});
expect(() =>
validateReleaseValidationReceiptReuseFreshness(betaReceipt, {
now_ms: sealedAtMs,
max_future_skew_ms: 0,
policies: {},
} as any),
).toThrow("keys must be exactly");
});
});
describe("release validation receipt canonical bytes and locator", () => {
it("rejects unknown fields, duplicate keys, noncanonical bytes, and digest tampering", () => {
const input = inputFixture();