fix: promote immutable verified release bundles
This commit is contained in:
@@ -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", [""]],
|
||||
|
||||
Reference in New Issue
Block a user