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
+9 -264
View File
@@ -24,7 +24,6 @@ import {
evaluatePromotionEvidence,
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
providerVerificationArtifactSchema,
} from "../../scripts/lib/provider-evidence.ts";
import {
createReleaseCandidateManifest,
@@ -32,7 +31,6 @@ import {
verifyReleaseCandidate,
} from "../../scripts/lib/release-candidate.ts";
import { deterministicSupplyChainGeneratedAt } from "../../scripts/lib/supply-chain-time.ts";
import { verifyPromotionInputs } from "../../scripts/lib/promotion-verifier.ts";
import {
indexCiGateContract,
loadCiGateContract,
@@ -57,11 +55,6 @@ const sourceIdentity = Object.freeze({
revision: "a".repeat(40),
sourceSetSha256: "b".repeat(64),
});
const localIdentity = Object.freeze({
sourceRevision: sourceIdentity.revision,
sourceSetSha256: sourceIdentity.sourceSetSha256,
assessmentSha256: "c".repeat(64),
});
const expectedProviderContext = Object.freeze({
run: Object.freeze({ id: "fixture-run", attempt: 1 }),
source: sourceIdentity,
@@ -73,6 +66,14 @@ const expectedProviderContext = Object.freeze({
}),
vulnerabilityInvocationNonce: "5".repeat(64),
provenanceInvocationNonce: "6".repeat(64),
secretScanAttestation: Object.freeze({
status: "PASS" as const,
localEvidenceAssessmentSha256: "7".repeat(64),
sourceSetSha256: sourceIdentity.sourceSetSha256,
policySha256: "8".repeat(64),
sarifSha256: "9".repeat(64),
scanInputSha256: "a".repeat(64),
}),
});
function signedProviderEvidence(
@@ -118,6 +119,7 @@ function providerPair(input: Readonly<{
run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.vulnerabilityInvocationNonce },
source: expectedProviderContext.source,
candidate,
secretScanAttestation: expectedProviderContext.secretScanAttestation,
findings: [],
}, "fixture-vulnerability-key", input.vulnerabilityKeys.privateKey, vulnerabilityFingerprint),
provenanceAttestation: signedProviderEvidence({
@@ -143,264 +145,7 @@ function providerTrust(
return { keyId, publicKey, publicKeyFingerprint };
}
async function createMinimalCandidateTree(root: string) {
const rawLockfile = "lockfileVersion: '9.0'\n";
const rawLockfileSha256 = createHash("sha256")
.update(rawLockfile)
.digest("hex");
await mkdir(path.join(root, "dist"), { recursive: true });
await writeFile(path.join(root, "dist/app.js"), "immutable\n");
await writeFile(path.join(root, "pnpm-lock.yaml"), rawLockfile);
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (file === "pnpm-lock.yaml") continue;
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
const value =
file === "artifacts/release/dependency-inventory.json"
? { lockfileSha256: rawLockfileSha256 }
: file === "artifacts/security/supply-chain-verification.json"
? { localStatus: "PASS" }
: { fixture: file };
await writeFile(path.join(root, file), `${JSON.stringify(value)}\n`);
}
const manifest = await createReleaseCandidateManifest(root);
await writeFile(
path.join(root, "artifacts/release/release-candidate.json"),
`${JSON.stringify(manifest)}\n`,
);
return manifest;
}
async function writeProviderEnvironment(
root: string,
candidate: Awaited<ReturnType<typeof createReleaseCandidateManifest>>,
overrides: Readonly<{ distSha256?: string }> = {},
) {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const archiveBytes = "fixture archive\n";
const candidateIdentity = {
archiveSha256: createHash("sha256").update(archiveBytes).digest("hex"),
bundleSha256: candidate.bundleSha256,
distSha256: overrides.distSha256 ?? candidate.distSha256,
lockfileSha256: candidate.lockfileSha256,
};
const vulnerabilityReport = signedProviderEvidence(
{
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: "5".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
findings: [],
},
"fixture-vulnerability-key",
vulnerabilityKeys.privateKey,
providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
);
const provenanceAttestation = signedProviderEvidence(
{
schemaVersion: 2,
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: "6".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
subject: { name: "dist", digest: { sha256: candidateIdentity.distSha256 } },
},
"fixture-provenance-key",
provenanceKeys.privateKey,
providerPublicKeyFingerprint(provenanceKeys.publicKey),
);
await mkdir(path.join(root, "provider"), { recursive: true });
await Promise.all([
writeFile(path.join(root, "provider/candidate.tar.gz"), archiveBytes),
writeFile(
path.join(root, "provider/vulnerability.json"),
`${JSON.stringify(vulnerabilityReport)}\n`,
),
writeFile(
path.join(root, "provider/provenance.json"),
`${JSON.stringify(provenanceAttestation)}\n`,
),
writeFile(
path.join(root, "provider/vulnerability.pem"),
vulnerabilityKeys.publicKey
.export({ type: "spki", format: "pem" })
.toString(),
),
writeFile(
path.join(root, "provider/provenance.pem"),
provenanceKeys.publicKey
.export({ type: "spki", format: "pem" })
.toString(),
),
]);
return {
CANDIDATE_ARCHIVE_PATH: "provider/candidate.tar.gz",
CANDIDATE_ARCHIVE_SHA256: createHash("sha256")
.update(archiveBytes)
.digest("hex"),
CI_RUN_ID: "fixture-run",
CI_RUN_ATTEMPT: "1",
EXPECTED_SOURCE_REVISION: sourceIdentity.revision,
VULNERABILITY_INVOCATION_NONCE: "5".repeat(64),
PROVENANCE_INVOCATION_NONCE: "6".repeat(64),
VULNERABILITY_REPORT_PATH: "provider/vulnerability.json",
PROVENANCE_ATTESTATION_PATH: "provider/provenance.json",
VULNERABILITY_PUBLIC_KEY_PATH: "provider/vulnerability.pem",
VULNERABILITY_KEY_ID: "fixture-vulnerability-key",
PROVENANCE_PUBLIC_KEY_PATH: "provider/provenance.pem",
PROVENANCE_KEY_ID: "fixture-provenance-key",
} satisfies NodeJS.ProcessEnv;
}
describe("supply-chain policy", () => {
it("emits a strict role-bound v3 verification record from exact input bytes", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-verification-v3-"));
try {
const manifest = await createMinimalCandidateTree(root);
const environment = await writeProviderEnvironment(root, manifest);
const report = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment,
verifyLocalEvidence: async () => ({
status: "PASS" as const,
identity: localIdentity,
failures: [] as const,
}),
nowEpochMs: () => NOW,
});
expect(providerVerificationArtifactSchema.parse(report)).toEqual(
expect.objectContaining({
schemaVersion: 3,
artifactType: "provider-verification",
candidate: expect.objectContaining({
archiveSha256: environment.CANDIDATE_ARCHIVE_SHA256,
}),
providerEvidence: expect.objectContaining({
vulnerabilityReportSha256: createHash("sha256")
.update(await readFile(path.join(root, environment.VULNERABILITY_REPORT_PATH!)))
.digest("hex"),
provenanceAttestationSha256: createHash("sha256")
.update(await readFile(path.join(root, environment.PROVENANCE_ATTESTATION_PATH!)))
.digest("hex"),
}),
}),
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("wires candidate files, PEM trust, env report paths, and mutation checks", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-wiring-"));
try {
const manifest = await createMinimalCandidateTree(root);
const validEnvironment = await writeProviderEnvironment(root, manifest);
const acceptLocalEvidence = async () => ({
status: "PASS" as const,
identity: localIdentity,
failures: [] as const,
});
const valid = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
const absent = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment: {},
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
const replayedNonce = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment: {
...validEnvironment,
VULNERABILITY_INVOCATION_NONCE: "9".repeat(64),
},
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
const wrongEnvironment = await writeProviderEnvironment(root, manifest, {
distSha256: "3".repeat(64),
});
const wrongDigest = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment: wrongEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
await writeFile(path.join(root, "dist/app.js"), "mutated\n");
const postAttestationMutation = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
expect({
valid: valid.status,
absent: absent.status,
wrongDigest: wrongDigest.status,
replayedNonce: replayedNonce.status,
postAttestationMutation: postAttestationMutation.status,
}).toEqual({
valid: "PASS",
absent: "FAIL_UNVERIFIED",
wrongDigest: "FAIL_UNVERIFIED",
replayedNonce: "FAIL_UNVERIFIED",
postAttestationMutation: "FAIL_UNVERIFIED",
});
expect(replayedNonce.failures).toContain("vulnerability report invocation nonce mismatch");
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects a self-consistent candidate that merely claims localStatus PASS", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-local-status-"));
try {
const manifest = await createMinimalCandidateTree(root);
const environment = await writeProviderEnvironment(root, manifest);
const localVerificationPath = path.join(
root,
"artifacts/security/supply-chain-verification.json",
);
const before = await readFile(localVerificationPath, "utf8");
const result = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: root,
environment,
nowEpochMs: () => NOW,
});
expect(result.status).toBe("FAIL_UNVERIFIED");
expect(result.failures).toEqual(
expect.arrayContaining([
"local evidence assessment is missing or invalid",
"local supply-chain evidence is not PASS",
]),
);
expect(await readFile(localVerificationPath, "utf8")).toBe(before);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("derives a stable supply-chain timestamp from the immutable build epoch", () => {
const input = {
generatedAt: "2026-08-01T00:00:00.000Z",