fix: close immutable promotion trust gaps
This commit is contained in:
@@ -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",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user