fix: close immutable promotion trust gaps

This commit is contained in:
DongHyeonka
2026-08-02 06:39:41 +09:00
parent 7c5ed80407
commit 92e5cace5c
16 changed files with 1228 additions and 274 deletions
+189 -107
View File
@@ -1,30 +1,199 @@
import { generateKeyPairSync, sign } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { providerEvidenceSignaturePayload } from "./lib/provider-evidence.ts";
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
import {
evaluatePromotionEvidence,
providerEvidenceSignaturePayload,
} from "./lib/provider-evidence.ts";
createReleaseCandidateManifest,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "./lib/release-candidate.ts";
const candidateDistSha256 = "1".repeat(64);
const lockfileSha256 = "2".repeat(64);
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const trust = {
vulnerabilityTrust: {
keyId: "fixture-vulnerability-key",
publicKey: vulnerabilityKeys.publicKey,
},
provenanceTrust: {
keyId: "fixture-provenance-key",
publicKey: provenanceKeys.publicKey,
},
};
const fixtureRoot = await mkdtemp(
path.join(tmpdir(), "supply-chain-provider-fixture-"),
);
try {
const rawLockfile = "lockfileVersion: '9.0'\n";
const lockfileSha256 = createHash("sha256")
.update(rawLockfile)
.digest("hex");
await mkdir(path.join(fixtureRoot, "dist"), { recursive: true });
await writeFile(path.join(fixtureRoot, "dist/app.js"), "immutable\n");
await writeFile(path.join(fixtureRoot, "pnpm-lock.yaml"), rawLockfile);
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (file === "pnpm-lock.yaml") continue;
await mkdir(path.dirname(path.join(fixtureRoot, file)), {
recursive: true,
});
const value =
file === "artifacts/release/dependency-inventory.json"
? { lockfileSha256 }
: { fixture: file };
await writeFile(
path.join(fixtureRoot, file),
`${JSON.stringify(value)}\n`,
);
}
const candidate = await createReleaseCandidateManifest(fixtureRoot);
await writeFile(
path.join(fixtureRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
`${JSON.stringify(candidate)}\n`,
);
const validEnvironment = await writeProviderEnvironment(
fixtureRoot,
"valid",
candidate.distSha256,
candidate.lockfileSha256,
);
const wrongEnvironment = await writeProviderEnvironment(
fixtureRoot,
"wrong",
"3".repeat(64),
candidate.lockfileSha256,
);
const acceptLocalEvidence = async () => ({
status: "PASS" as const,
failures: [] as const,
});
const fixtures = {
absent: await verifyPromotionInputs({
repositoryRoot: fixtureRoot,
environment: {},
verifyLocalEvidence: acceptLocalEvidence,
}),
validImmutable: await verifyPromotionInputs({
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
}),
wrongDigest: await verifyPromotionInputs({
repositoryRoot: fixtureRoot,
environment: wrongEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
}),
postAttestationMutation: null as Awaited<
ReturnType<typeof verifyPromotionInputs>
> | null,
};
await writeFile(path.join(fixtureRoot, "dist/app.js"), "mutated\n");
fixtures.postAttestationMutation = await verifyPromotionInputs({
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
});
const passed =
fixtures.validImmutable.status === "PASS" &&
fixtures.absent.status === "FAIL_UNVERIFIED" &&
fixtures.wrongDigest.status === "FAIL_UNVERIFIED" &&
fixtures.postAttestationMutation.status === "FAIL_UNVERIFIED";
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-provider-fixtures.json",
`${JSON.stringify(
{
schemaVersion: 1,
fixtures: Object.fromEntries(
Object.entries(fixtures).map(([name, result]) => [
name,
{ status: result?.status, failures: result?.failures },
]),
),
passingFixtureCount: Object.values(fixtures).filter(
(result) => result?.status === "PASS",
).length,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
"Supply-chain provider fixtures failed closed incorrectly\n",
);
process.exitCode = 1;
} else {
process.stdout.write(
"Supply-chain provider fixtures: only the valid immutable fixture PASS\n",
);
}
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
async function writeProviderEnvironment(
repositoryRoot: string,
name: string,
distDigest: string,
lockfileSha256: string,
): Promise<NodeJS.ProcessEnv> {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const directory = `provider/${name}`;
await mkdir(path.join(repositoryRoot, directory), { recursive: true });
const vulnerability = signedEvidence(
{
schemaVersion: 1,
provider: "fixture-vulnerability-provider",
generatedAt: "2026-08-01T00:00:00.000Z",
scannedLockfileSha256: lockfileSha256,
scannedDistSha256: distDigest,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
);
const provenance = signedEvidence(
{
schemaVersion: 1,
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
generatedAt: "2026-08-01T00:00:00.000Z",
subject: { name: "dist", digest: { sha256: distDigest } },
},
"fixture-provenance-key",
provenanceKeys.privateKey,
);
await Promise.all([
writeFile(
path.join(repositoryRoot, directory, "vulnerability.json"),
`${JSON.stringify(vulnerability)}\n`,
),
writeFile(
path.join(repositoryRoot, directory, "provenance.json"),
`${JSON.stringify(provenance)}\n`,
),
writeFile(
path.join(repositoryRoot, directory, "vulnerability.pem"),
vulnerabilityKeys.publicKey
.export({ type: "spki", format: "pem" })
.toString(),
),
writeFile(
path.join(repositoryRoot, directory, "provenance.pem"),
provenanceKeys.publicKey
.export({ type: "spki", format: "pem" })
.toString(),
),
]);
return {
VULNERABILITY_REPORT_PATH: `${directory}/vulnerability.json`,
PROVENANCE_ATTESTATION_PATH: `${directory}/provenance.json`,
VULNERABILITY_PUBLIC_KEY_PATH: `${directory}/vulnerability.pem`,
VULNERABILITY_KEY_ID: "fixture-vulnerability-key",
PROVENANCE_PUBLIC_KEY_PATH: `${directory}/provenance.pem`,
PROVENANCE_KEY_ID: "fixture-provenance-key",
};
}
function signedEvidence(
value: Record<string, unknown>,
keyId: string,
privateKey: typeof vulnerabilityKeys.privateKey,
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
) {
return {
...value,
@@ -39,90 +208,3 @@ function signedEvidence(
},
};
}
function evidenceFor(distDigest: string) {
return {
vulnerabilityReport: signedEvidence(
{
schemaVersion: 1,
provider: "fixture-vulnerability-provider",
generatedAt: "2026-08-01T00:00:00.000Z",
scannedLockfileSha256: lockfileSha256,
scannedDistSha256: distDigest,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
),
provenanceAttestation: signedEvidence(
{
schemaVersion: 1,
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
generatedAt: "2026-08-01T00:00:00.000Z",
subject: { name: "dist", digest: { sha256: distDigest } },
},
"fixture-provenance-key",
provenanceKeys.privateKey,
),
};
}
const base = {
candidate: { distSha256: candidateDistSha256, lockfileSha256 },
currentDistSha256: candidateDistSha256,
localStatus: "PASS",
...trust,
};
const validEvidence = evidenceFor(candidateDistSha256);
const fixtures = {
absent: evaluatePromotionEvidence({
...base,
vulnerabilityReport: null,
provenanceAttestation: null,
}),
validImmutable: evaluatePromotionEvidence({ ...base, ...validEvidence }),
wrongDigest: evaluatePromotionEvidence({
...base,
...evidenceFor("3".repeat(64)),
}),
postAttestationMutation: evaluatePromotionEvidence({
...base,
...validEvidence,
currentDistSha256: "4".repeat(64),
}),
};
const passed =
fixtures.validImmutable.status === "PASS" &&
fixtures.absent.status === "FAIL_UNVERIFIED" &&
fixtures.wrongDigest.status === "FAIL_UNVERIFIED" &&
fixtures.postAttestationMutation.status === "FAIL_UNVERIFIED";
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-provider-fixtures.json",
`${JSON.stringify(
{
schemaVersion: 1,
fixtures: Object.fromEntries(
Object.entries(fixtures).map(([name, result]) => [
name,
{ status: result.status, failures: result.failures },
]),
),
passingFixtureCount: Object.values(fixtures).filter(
(result) => result.status === "PASS",
).length,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write("Supply-chain provider fixtures failed closed incorrectly\n");
process.exit(1);
}
process.stdout.write(
"Supply-chain provider fixtures: only the valid immutable fixture PASS\n",
);