fix: promote immutable verified release bundles

This commit is contained in:
DongHyeonka
2026-08-02 06:08:06 +09:00
parent 100a3bb6ba
commit 7c5ed80407
17 changed files with 1203 additions and 263 deletions
+255 -1
View File
@@ -1,4 +1,7 @@
import { readFile } from "node:fs/promises";
import { generateKeyPairSync, sign } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
@@ -17,6 +20,15 @@ import {
selectIncludedInventoryFiles,
} from "../../scripts/lib/secret-scan-policy.ts";
import { checkSecurityFixtures } from "../../scripts/lib/security-fixture-check.ts";
import {
evaluatePromotionEvidence,
providerEvidenceSignaturePayload,
} from "../../scripts/lib/provider-evidence.ts";
import {
createReleaseCandidateManifest,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
verifyReleaseCandidate,
} from "../../scripts/lib/release-candidate.ts";
const integrity = `sha512-${Buffer.alloc(64, 7).toString("base64")}`;
const dependency = {
@@ -30,7 +42,249 @@ const dependency = {
dependencies: [],
};
const candidateDistSha256 = "1".repeat(64);
const lockfileSha256 = "2".repeat(64);
function signedProviderEvidence(
value: Record<string, unknown>,
keyId: string,
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
) {
return {
...value,
signature: {
algorithm: "Ed25519",
keyId,
value: sign(
null,
providerEvidenceSignaturePayload(value),
privateKey,
).toString("base64"),
},
};
}
describe("supply-chain policy", () => {
it("rejects release candidate dist bytes changed after manifest creation", async () => {
const root = await mkdtemp(path.join(tmpdir(), "release-candidate-"));
try {
await mkdir(path.join(root, "dist/.vite"), { recursive: true });
await writeFile(path.join(root, "dist/app.js"), "immutable\n");
await writeFile(path.join(root, "dist/.vite/metadata.json"), "{}\n");
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
await writeFile(
path.join(root, file),
file === "artifacts/release/dependency-inventory.json"
? `${JSON.stringify({ lockfileSha256 })}\n`
: `${file}\n`,
);
}
const manifest = await createReleaseCandidateManifest(root);
expect((await verifyReleaseCandidate(manifest, root)).failures).toEqual(
[],
);
await writeFile(path.join(root, "dist/app.js"), "mutated\n");
expect(
(await verifyReleaseCandidate(manifest, root)).failures,
).toEqual(
expect.arrayContaining([
"release candidate dist digest mismatch",
"release candidate bundle digest mismatch",
"release candidate file set or file digest mismatch",
]),
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("fails promotion when external provider evidence is absent", () => {
const result = evaluatePromotionEvidence({
candidate: {
distSha256: candidateDistSha256,
lockfileSha256,
},
currentDistSha256: candidateDistSha256,
localStatus: "PASS",
vulnerabilityReport: null,
provenanceAttestation: null,
vulnerabilityTrust: null,
provenanceTrust: null,
});
expect(result.status).toBe("FAIL_UNVERIFIED");
});
it("passes only signed provider evidence for the exact immutable candidate", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const vulnerabilityReport = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-vulnerability-provider",
generatedAt: "2026-08-01T00:00:00.000Z",
scannedLockfileSha256: lockfileSha256,
scannedDistSha256: candidateDistSha256,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
);
const provenanceAttestation = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
generatedAt: "2026-08-01T00:00:00.000Z",
subject: {
name: "dist",
digest: { sha256: candidateDistSha256 },
},
},
"fixture-provenance-key",
provenanceKeys.privateKey,
);
const result = evaluatePromotionEvidence({
candidate: {
distSha256: candidateDistSha256,
lockfileSha256,
},
currentDistSha256: candidateDistSha256,
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: {
keyId: "fixture-vulnerability-key",
publicKey: vulnerabilityKeys.publicKey,
},
provenanceTrust: {
keyId: "fixture-provenance-key",
publicKey: provenanceKeys.publicKey,
},
});
expect(result).toMatchObject({
status: "PASS",
vulnerabilityStatus: "PASS",
provenanceAttestationStatus: "PASS",
failures: [],
});
});
it("rejects correctly signed provider evidence for a different digest", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const wrongDistSha256 = "3".repeat(64);
const vulnerabilityReport = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-vulnerability-provider",
generatedAt: "2026-08-01T00:00:00.000Z",
scannedLockfileSha256: lockfileSha256,
scannedDistSha256: wrongDistSha256,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
);
const provenanceAttestation = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
generatedAt: "2026-08-01T00:00:00.000Z",
subject: { name: "dist", digest: { sha256: wrongDistSha256 } },
},
"fixture-provenance-key",
provenanceKeys.privateKey,
);
const result = evaluatePromotionEvidence({
candidate: {
distSha256: candidateDistSha256,
lockfileSha256,
},
currentDistSha256: candidateDistSha256,
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: {
keyId: "fixture-vulnerability-key",
publicKey: vulnerabilityKeys.publicKey,
},
provenanceTrust: {
keyId: "fixture-provenance-key",
publicKey: provenanceKeys.publicKey,
},
});
expect(result.status).toBe("FAIL_UNVERIFIED");
expect(result.failures).toEqual(
expect.arrayContaining([
"vulnerability report dist digest mismatch",
"provenance attestation dist digest mismatch",
]),
);
});
it("rejects candidate bytes changed after provider attestation", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const vulnerabilityReport = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-vulnerability-provider",
generatedAt: "2026-08-01T00:00:00.000Z",
scannedLockfileSha256: lockfileSha256,
scannedDistSha256: candidateDistSha256,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
);
const provenanceAttestation = signedProviderEvidence(
{
schemaVersion: 1,
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
generatedAt: "2026-08-01T00:00:00.000Z",
subject: {
name: "dist",
digest: { sha256: candidateDistSha256 },
},
},
"fixture-provenance-key",
provenanceKeys.privateKey,
);
const result = evaluatePromotionEvidence({
candidate: {
distSha256: candidateDistSha256,
lockfileSha256,
},
currentDistSha256: "4".repeat(64),
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: {
keyId: "fixture-vulnerability-key",
publicKey: vulnerabilityKeys.publicKey,
},
provenanceTrust: {
keyId: "fixture-provenance-key",
publicKey: provenanceKeys.publicKey,
},
});
expect(result.status).toBe("FAIL_UNVERIFIED");
expect(result.failures).toContain(
"candidate dist bytes changed after immutable build",
);
});
it.each([
["empty", []],
["empty entry", [""]],