refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 16:02:21 +09:00
parent 30ceac23c1
commit 4dc033cf33
72 changed files with 13370 additions and 1549 deletions
+307 -260
View File
@@ -1,248 +1,161 @@
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import {
createHash,
generateKeyPairSync,
sign,
type KeyObject,
} from "node:crypto";
import {
cp,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
localEvidenceAssessmentArtifactSchema,
supplyChainProviderFixturesArtifactSchema,
} from "./contracts/release-artifacts.ts";
import { PROMOTED_FILE_NAMES } from "./contracts/promotion-artifacts.ts";
import {
captureCiCandidateArchive,
} from "./lib/ci-candidate-archive.ts";
import {
cleanupFinalizedPromotion,
finalizeVerifiedPromotion,
} from "./lib/promotion-stager.ts";
import { verifyExactPromotionBundle } from "./lib/exact-promotion-bundle.ts";
import {
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
} from "./lib/provider-evidence.ts";
import { localEvidenceAssessmentArtifactSchema } from "./contracts/release-artifacts.ts";
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
import {
createReleaseCandidateManifest,
LOCAL_EVIDENCE_ASSESSMENT_PATH,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
releaseCandidateManifestSchema,
} from "./lib/release-candidate.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const NOW = Date.parse("2026-08-02T01:00:00.000Z");
const FIXTURE_SOURCE = Object.freeze({
revision: "a".repeat(40),
sourceSetSha256: "b".repeat(64),
});
const FIXTURE_LOCAL_IDENTITY = Object.freeze({
sourceRevision: FIXTURE_SOURCE.revision,
sourceSetSha256: FIXTURE_SOURCE.sourceSetSha256,
assessmentSha256: "c".repeat(64),
});
const fixtureRoot = await mkdtemp(
path.join(tmpdir(), "supply-chain-provider-fixture-"),
);
const repositoryRoot = process.cwd();
const fixtureRoot = await mkdtemp(path.join(tmpdir(), "provider-exact-five-fixture-"));
try {
const repositoryRoot = process.cwd();
const actualCandidate = releaseCandidateManifestSchema.parse(
await cp(repositoryRoot, fixtureRoot, {
recursive: true,
filter: (source) => {
const relative = path.relative(repositoryRoot, source);
if (!relative) return true;
const first = relative.split(path.sep)[0];
return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? "");
},
});
await cp(path.join(repositoryRoot, "artifacts"), path.join(fixtureRoot, "artifacts"), {
recursive: true,
});
await rm(path.join(fixtureRoot, "artifacts/release"), { recursive: true, force: true });
await symlink(path.join(repositoryRoot, "node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: repositoryRoot,
encoding: "utf8",
});
if (git.status !== 0) throw new Error(`provider fixture git identity failed: ${git.stderr}`);
const [revision, sourceDateEpoch] = git.stdout.trim().split(/\r?\n/u);
if (!revision || !sourceDateEpoch) throw new Error("provider fixture git identity is incomplete");
const build = spawnSync("corepack", ["pnpm", "build:release-candidate"], {
cwd: fixtureRoot,
encoding: "utf8",
timeout: 120_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
CI: "true",
VITE_BUILD_ID: "provider-exact-five-fixture",
VITE_COMMIT_SHA: revision,
RELEASE_ID: "provider-exact-five-fixture",
SOURCE_DATE_EPOCH: sourceDateEpoch,
CI_RUNNER_IMAGE: `fixture@sha256:${"a".repeat(64)}`,
},
});
if (build.status !== 0) throw new Error(`${build.stdout}\n${build.stderr}`);
const manifest = releaseCandidateManifestSchema.parse(
JSON.parse(
await readFile(
path.join(repositoryRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
"utf8",
),
await readFile(path.join(fixtureRoot, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"),
) as unknown,
);
const actualAssessment = localEvidenceAssessmentArtifactSchema.parse(
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(
await readFile(path.join(repositoryRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
await readFile(path.join(fixtureRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
) as unknown,
);
const actualProviderEnvironment = absoluteProviderEnvironment(
fixtureRoot,
await writeProviderEnvironment(
fixtureRoot,
"actual",
actualCandidate,
{
revision: actualAssessment.source.revision,
sourceSetSha256: actualAssessment.source.sourceSetSha256,
},
),
const archivePath = path.join(fixtureRoot, "candidate.tar.gz");
const tar = spawnSync(
"/usr/bin/tar",
[
"--sort=name",
"--mtime=@0",
"--owner=0",
"--group=0",
"--numeric-owner",
"-czf",
archivePath,
"dist",
...RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
],
{ cwd: fixtureRoot, encoding: "utf8" },
);
const actualDefaultVerifier = await verifyPromotionInputs({
artifactType: "provider-verification",
environment: actualProviderEnvironment,
nowEpochMs: () => NOW,
});
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,
FIXTURE_SOURCE,
);
const wrongEnvironment = await writeProviderEnvironment(
fixtureRoot,
"wrong",
candidate,
FIXTURE_SOURCE,
{ distSha256: "3".repeat(64) },
);
const acceptLocalEvidence = async () => ({
status: "PASS" as const,
identity: FIXTURE_LOCAL_IDENTITY,
failures: [] as const,
});
const fixtures = {
absent: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: {},
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
validImmutable: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
wrongDigest: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: wrongEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
postAttestationMutation: null as Awaited<
ReturnType<typeof verifyPromotionInputs>
> | null,
};
await writeFile(path.join(fixtureRoot, "dist/app.js"), "mutated\n");
fixtures.postAttestationMutation = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
const passed =
actualDefaultVerifier.status === "PASS" &&
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,
actualDefaultVerifier: {
status: actualDefaultVerifier.status,
failures: actualDefaultVerifier.failures,
},
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: actual default verifier and valid immutable fixture PASS\n",
);
}
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
function absoluteProviderEnvironment(
repositoryRoot: string,
environment: NodeJS.ProcessEnv,
): NodeJS.ProcessEnv {
const absolute = { ...environment };
for (const key of [
"CANDIDATE_ARCHIVE_PATH",
"VULNERABILITY_REPORT_PATH",
"PROVENANCE_ATTESTATION_PATH",
"VULNERABILITY_PUBLIC_KEY_PATH",
"PROVENANCE_PUBLIC_KEY_PATH",
] as const) {
const value = absolute[key];
if (value) absolute[key] = path.join(repositoryRoot, value);
}
return absolute;
}
async function writeProviderEnvironment(
repositoryRoot: string,
name: string,
candidate: Awaited<ReturnType<typeof createReleaseCandidateManifest>>,
source: Readonly<{ revision: string; sourceSetSha256: string }>,
overrides: Readonly<{ distSha256?: string }> = {},
): Promise<NodeJS.ProcessEnv> {
if (tar.status !== 0) throw new Error(`provider fixture real tar failed: ${tar.stderr}`);
const archiveBytes = await readFile(archivePath);
const archiveSha256 = sha256(archiveBytes);
const now = Date.parse("2026-08-02T01:00:00.000Z");
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const directory = `provider/${name}`;
const archiveBytes = `fixture archive ${name}\n`;
const archiveSha256 = createHash("sha256").update(archiveBytes).digest("hex");
const candidateIdentity = {
archiveSha256,
bundleSha256: candidate.bundleSha256,
distSha256: overrides.distSha256 ?? candidate.distSha256,
lockfileSha256: candidate.lockfileSha256,
};
const sourceIdentity = {
revision: source.revision,
sourceSetSha256: source.sourceSetSha256,
};
await mkdir(path.join(repositoryRoot, directory), { recursive: true });
const vulnerabilityKeyId = "fixture-vulnerability-key";
const provenanceKeyId = "fixture-provenance-key";
const vulnerabilityNonce = "1".repeat(64);
const provenanceNonce = "2".repeat(64);
const context = {
run: { id: "fixture-run", attempt: 1 },
source: {
revision: assessment.source.revision,
sourceSetSha256: assessment.source.sourceSetSha256,
},
candidate: {
archiveSha256,
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
},
} as const;
const vulnerability = signedEvidence(
{
schemaVersion: 2,
evidenceType: "vulnerability-report",
provider: "fixture-vulnerability-provider",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { id: "fixture-run", attempt: 1, invocationNonce: "1".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + 60 * 60 * 1_000).toISOString(),
run: { ...context.run, invocationNonce: vulnerabilityNonce },
source: context.source,
candidate: context.candidate,
secretScanAttestation: {
status: "PASS",
localEvidenceAssessmentSha256: sha256(
await readFile(path.join(fixtureRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH)),
),
sourceSetSha256: assessment.source.sourceSetSha256,
policySha256: assessment.secretScan.policySha256,
sarifSha256: assessment.secretScan.sarifSha256,
scanInputSha256: assessment.secretScan.scanInputSha256,
},
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeyId,
vulnerabilityKeys.publicKey,
vulnerabilityKeys.privateKey,
);
@@ -252,65 +165,175 @@ async function writeProviderEnvironment(
evidenceType: "provenance-attestation",
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { id: "fixture-run", attempt: 1, invocationNonce: "2".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
subject: { name: "dist", digest: { sha256: candidateIdentity.distSha256 } },
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + 60 * 60 * 1_000).toISOString(),
run: { ...context.run, invocationNonce: provenanceNonce },
source: context.source,
candidate: context.candidate,
subject: { name: "dist", digest: { sha256: manifest.distSha256 } },
},
"fixture-provenance-key",
provenanceKeyId,
provenanceKeys.publicKey,
provenanceKeys.privateKey,
);
await Promise.all([
writeFile(
path.join(repositoryRoot, directory, "candidate.tar.gz"),
archiveBytes,
write(fixtureRoot, "provider/vulnerability.json", `${JSON.stringify(vulnerability)}\n`),
write(fixtureRoot, "provider/provenance.json", `${JSON.stringify(provenance)}\n`),
write(
fixtureRoot,
"provider/vulnerability.pem",
vulnerabilityKeys.publicKey.export({ type: "spki", format: "pem" }),
),
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(),
write(
fixtureRoot,
"provider/provenance.pem",
provenanceKeys.publicKey.export({ type: "spki", format: "pem" }),
),
]);
return {
CANDIDATE_ARCHIVE_PATH: `${directory}/candidate.tar.gz`,
CANDIDATE_ARCHIVE_SHA256: archiveSha256,
CI_RUN_ID: "fixture-run",
CI_RUN_ATTEMPT: "1",
EXPECTED_SOURCE_REVISION: source.revision,
VULNERABILITY_INVOCATION_NONCE: "1".repeat(64),
PROVENANCE_INVOCATION_NONCE: "2".repeat(64),
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",
};
const runnerTempRoot = path.join(fixtureRoot, "runner-temp");
await mkdir(runnerTempRoot, { mode: 0o700 });
const finalized = await finalizeVerifiedPromotion(
{
repositoryRoot: fixtureRoot,
archivePath,
expectedArchiveSha256: archiveSha256,
vulnerabilityReportPath: "provider/vulnerability.json",
provenanceAttestationPath: "provider/provenance.json",
vulnerabilityPublicKeyPath: "provider/vulnerability.pem",
vulnerabilityKeyId,
provenancePublicKeyPath: "provider/provenance.pem",
provenanceKeyId,
expectedRun: { id: context.run.id, attempt: 1, sourceRevision: revision },
vulnerabilityInvocationNonce: vulnerabilityNonce,
provenanceInvocationNonce: provenanceNonce,
runnerTempRoot,
},
{
nowEpochMs: () => now,
randomBytes: (bytes) => Buffer.alloc(bytes, 0x4a),
afterCapture: async () => {
await writeFile(path.join(fixtureRoot, "dist/index.html"), "contradictory external tree\n");
},
},
);
const validImmutable =
finalized.files.length === 5 &&
(await readdir(finalized.stagingRoot)).length === 5;
const exactFiles = Object.fromEntries(
await Promise.all(
PROMOTED_FILE_NAMES.map(async (name) => [
name,
await readFile(path.join(finalized.stagingRoot, name)),
] as const),
),
);
const bundleTrust = {
vulnerabilityTrust: {
keyId: vulnerabilityKeyId,
publicKey: vulnerabilityKeys.publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
},
provenanceTrust: {
keyId: provenanceKeyId,
publicKey: provenanceKeys.publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(provenanceKeys.publicKey),
},
expected: {
run: context.run,
sourceRevision: context.source.revision,
sourceSetSha256: context.source.sourceSetSha256,
archiveSha256: context.candidate.archiveSha256,
bundleSha256: context.candidate.bundleSha256,
distSha256: context.candidate.distSha256,
lockfileSha256: context.candidate.lockfileSha256,
},
nowEpochMs: () => now,
} as const;
const absentFiles = { ...exactFiles } as Partial<typeof exactFiles>;
delete absentFiles["provider-verification.json"];
const absent = await captureRejection(async () => {
await verifyExactPromotionBundle(absentFiles, bundleTrust);
});
const invalidPath = path.join(fixtureRoot, "invalid.tar.gz");
const invalidBytes = Buffer.from("arbitrary non-tar bytes\n");
await writeFile(invalidPath, invalidBytes);
const invalidTar = await captureRejection(async () => {
await finalizeVerifiedPromotion(
{
repositoryRoot: fixtureRoot,
archivePath: invalidPath,
expectedArchiveSha256: sha256(invalidBytes),
vulnerabilityReportPath: "provider/vulnerability.json",
provenanceAttestationPath: "provider/provenance.json",
vulnerabilityPublicKeyPath: "provider/vulnerability.pem",
vulnerabilityKeyId,
provenancePublicKeyPath: "provider/provenance.pem",
provenanceKeyId,
expectedRun: { id: context.run.id, attempt: 1, sourceRevision: revision },
vulnerabilityInvocationNonce: vulnerabilityNonce,
provenanceInvocationNonce: provenanceNonce,
runnerTempRoot,
},
{ nowEpochMs: () => now },
);
});
const wrongDigest = await captureRejection(async () => {
await captureCiCandidateArchive({ archivePath, expectedSha256: "0".repeat(64) });
});
const passed =
validImmutable && absent.rejected && invalidTar.rejected && wrongDigest.rejected;
await cleanupFinalizedPromotion({
runnerTempRoot,
stagingRoot: finalized.stagingRoot,
cleanupToken: finalized.cleanupToken,
runnerTempIdentity: finalized.runnerTempIdentity,
stagingIdentity: finalized.stagingIdentity,
});
await mkdir(path.join(repositoryRoot, "artifacts/security"), { recursive: true });
await writeValidatedJsonArtifact({
path: path.join(repositoryRoot, "artifacts/security/supply-chain-provider-fixtures.json"),
schema: supplyChainProviderFixturesArtifactSchema,
value: {
schemaVersion: 1,
actualDefaultVerifier: {
status: validImmutable ? "PASS" : "FAIL_UNVERIFIED",
failures: validImmutable ? [] : ["canonical exact-five finalizer failed"],
},
fixtures: {
absent: {
status: absent.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [absent.failure],
},
validImmutable: { status: validImmutable ? "PASS" : "FAIL_UNVERIFIED", failures: [] },
wrongDigest: {
status: wrongDigest.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [wrongDigest.failure],
},
invalidTar: {
status: invalidTar.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [invalidTar.failure],
},
},
externalTreeCanary: {
status: validImmutable ? "PASS" : "FAIL_UNVERIFIED",
failures: validImmutable ? [] : ["external tree canary influenced captured archive"],
},
passingFixtureCount: validImmutable ? 1 : 0,
status: passed ? "PASS" : "FAIL",
},
});
if (!passed) throw new Error("canonical exact-five provider fixtures failed closed incorrectly");
process.stdout.write("Supply-chain provider real-tar signed exact-five fixtures: PASS\n");
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
function signedEvidence(
value: Record<string, unknown>,
keyId: string,
publicKey: ReturnType<typeof generateKeyPairSync>["publicKey"],
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
publicKey: KeyObject,
privateKey: KeyObject,
) {
return {
...value,
@@ -326,3 +349,27 @@ function signedEvidence(
},
};
}
async function write(root: string, relative: string, value: string | Buffer): Promise<void> {
const absolute = path.join(root, relative);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, value);
}
async function captureRejection(
operation: () => Promise<void>,
): Promise<Readonly<{ rejected: boolean; failure: string }>> {
try {
await operation();
return Object.freeze({ rejected: false, failure: "fixture unexpectedly passed" });
} catch (error) {
return Object.freeze({
rejected: true,
failure: error instanceof Error ? error.message : String(error),
});
}
}
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}