Files
Portfolio/tests/integration/release-manifest.test.ts

315 lines
9.8 KiB
TypeScript

import { generateKeyPairSync, sign } from "node:crypto";
import { describe, expect, test } from "vitest";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
import {
computePreparedAssetSetDigest,
computeProductionInputRoot,
} from "../../src/security/production-input-root.ts";
import { validateReleaseManifest } from "../../src/security/release-manifest.ts";
// @ts-expect-error The executable JavaScript verifier intentionally has no browser-facing declaration.
import { verifyApprovalRequestForTest } from "../../scripts/release/verify-approvals.mjs";
const h = (c: string) => c.repeat(64);
describe("production input and private-source release manifest", () => {
test("hashes an exact manifest-bound PreparedAssetSet and rejects missing or extra members", () => {
const bytes = new TextEncoder().encode("image");
const member = {
assetId: "asset-one",
publicPath: "asset-one.webp",
mediaType: "image/webp",
width: 10,
height: 20,
durationMs: null,
bytes,
};
const manifest = [
{
id: "asset-one",
fileName: "asset-one.webp",
sha256:
"6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d",
mediaType: "image/webp",
width: 10,
height: 20,
durationMs: null,
},
];
expect(computePreparedAssetSetDigest([member], manifest)).toMatch(
/^[a-f0-9]{64}$/,
);
expect(() => computePreparedAssetSetDigest([], manifest)).toThrow();
expect(() =>
computePreparedAssetSetDigest(
[
member,
{ ...member, assetId: "asset-two", publicPath: "asset-two.webp" },
],
manifest,
),
).toThrow();
});
test("matches the fixed four-label input-root vector", () => {
expect(
computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
).toBe("38707dd72ded582a346048ff66a1625cdaca91f6536dd27c935bc3b8af262ae8");
});
test("validates an exact initial-launch private-source manifest", () => {
const inputDigests = {
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
};
const manifest = {
schema: "portfolio-release-manifest/v1",
launchMode: "private-source-initial-launch",
buildId: "release-2026-09-02",
inputDigests,
productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
artifacts: [
{ path: "index.html", sha256: h("e"), bytes: 42, encoding: "identity" },
],
deployFiles: ["index.html"],
sourceCommit: h("f"),
sourceTree: h("0"),
approvedRecordRoot: h("1"),
approvedAssetRoot: h("2"),
approvalProofSetDigest: h("3"),
approvalProjectionAttestationDigest: h("4"),
scanEvidenceDigest: h("5"),
policyVersions: { approval: "1", privacy: "1", release: "1" },
toolVersions: { node: "22.22.1" },
predecessorManifestDigest: null,
currentManifestDigest: null,
issuedAt: "2026-09-02T00:00:00Z",
expiresAt: "2026-09-03T00:00:00Z",
minimumBoundExpiry: "2026-09-03T00:00:00Z",
};
expect(
validateReleaseManifest(manifest, { now: "2026-09-02T01:00:00Z" }),
).toEqual(manifest);
expect(
canonicalJsonDigest("portfolio-release-manifest/v1", manifest),
).toMatch(/^[a-f0-9]{64}$/);
});
test.each([
["public Git target", { publicGitTarget: "https://example.com/repo.git" }],
["extra deploy file", { deployFiles: ["index.html", "extra.txt"] }],
[
"case-fold collision",
{
artifacts: [
{ path: "A.js", sha256: h("e"), bytes: 1, encoding: "identity" },
{ path: "a.js", sha256: h("f"), bytes: 1, encoding: "identity" },
],
deployFiles: ["A.js", "a.js"],
},
],
["expired manifest", { expiresAt: "2026-09-02T00:30:00Z" }],
])("rejects %s", (_label, override) => {
const inputDigests = {
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
};
const baseline = {
schema: "portfolio-release-manifest/v1",
launchMode: "private-source-initial-launch",
buildId: "release",
inputDigests,
productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
artifacts: [
{ path: "index.html", sha256: h("e"), bytes: 42, encoding: "identity" },
],
deployFiles: ["index.html"],
sourceCommit: h("f"),
sourceTree: h("0"),
approvedRecordRoot: h("1"),
approvedAssetRoot: h("2"),
approvalProofSetDigest: h("3"),
approvalProjectionAttestationDigest: h("4"),
scanEvidenceDigest: h("5"),
policyVersions: { approval: "1", privacy: "1", release: "1" },
toolVersions: { node: "22" },
predecessorManifestDigest: null,
currentManifestDigest: null,
issuedAt: "2026-09-02T00:00:00Z",
expiresAt: "2026-09-03T00:00:00Z",
minimumBoundExpiry: "2026-09-03T00:00:00Z",
};
expect(() =>
validateReleaseManifest(
{ ...baseline, ...override },
{ now: "2026-09-02T01:00:00Z" },
),
).toThrow();
});
test("approval verifier requires an authenticated exact ledger and rejects replay after revoke", () => {
const keys = generateKeyPairSync("ed25519");
const approvalPayload = {
schema: "portfolio-approval-payload/v1",
keyId: "approval-key",
issuerEpoch: 1,
authorizedRole: "approver",
evidenceFingerprints: [h("1")],
recordClass: "content",
recordType: "profile",
publicRecordIdOrAssetId: "profile-main",
approvedPublicRecordHashOrManifestRecordHash: h("2"),
sanitizedOutputHashOrNull: null,
decision: "approve",
issuedAt: "2026-09-01T00:00:00Z",
expiresAt: "2026-09-04T00:00:00Z",
previousLedgerHeadHash: h("0"),
ledgerRevision: 1,
policyVersion: "policy-1",
};
const envelope = {
schema: "portfolio-approval-envelope/v1",
payload: approvalPayload,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
value: sign(
null,
canonicalValueBytes(approvalPayload),
keys.privateKey,
).toString("base64url"),
},
};
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope,
);
const head = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", {
previousLedgerHeadHash: h("0"),
ledgerRevision: 1,
envelopeDigest,
});
const request = {
schema: "portfolio-approval-verification-request/v1",
latestLedgerHeadHash: head,
latestLedgerRevision: 1,
trustedKeys: [
{
keyId: "approval-key",
issuerEpoch: 1,
scope: "portfolio-approval-decision/v1",
authorizedRoles: ["approver"],
publicKeyPem: keys.publicKey.export({ type: "spki", format: "pem" }),
},
],
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1",
genesisHeadHash: h("0"),
entries: [envelope],
},
contentRecords: [
{
recordType: "profile",
publicRecordId: "profile-main",
approvedPublicRecordHash: h("2"),
envelope,
},
],
assetRecords: [],
inputDigests: {
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
},
};
const result = verifyApprovalRequestForTest(canonicalValueBytes(request), {
now: "2026-09-02T00:00:00Z",
});
expect(Object.keys(result).sort()).toEqual([
"approvalProofSetDigest",
"approvedAssetRoot",
"approvedRecordRoot",
"earliestApprovalExpiry",
"latestLedgerHeadHash",
"latestLedgerRevision",
"productionInputRoot",
"projectionSchema",
"projectionScope",
"schema",
]);
const serialized = new TextDecoder().decode(canonicalValueBytes(result));
expect(serialized).not.toContain("evidenceFingerprints");
const revokePayload = {
...approvalPayload,
decision: "revoke",
issuedAt: "2026-09-02T01:00:00Z",
ledgerRevision: 2,
previousLedgerHeadHash: head,
};
const revoke = {
schema: "portfolio-approval-envelope/v1",
payload: revokePayload,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
value: sign(
null,
canonicalValueBytes(revokePayload),
keys.privateKey,
).toString("base64url"),
},
};
const revokeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
revoke,
);
const revokedHead = canonicalJsonDigest(
"portfolio-approval-ledger-entry/v1",
{
previousLedgerHeadHash: head,
ledgerRevision: 2,
envelopeDigest: revokeDigest,
},
);
expect(() =>
verifyApprovalRequestForTest(
canonicalValueBytes({
...request,
latestLedgerHeadHash: revokedHead,
latestLedgerRevision: 2,
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1",
genesisHeadHash: h("0"),
entries: [envelope, revoke],
},
}),
{ now: "2026-09-02T02:00:00Z" },
),
).toThrow(/latest|revoke|supersed/i);
});
});