fix: harden provider and promotion evidence
This commit is contained in:
+201
-195
@@ -23,6 +23,7 @@ import { checkSecurityFixtures } from "../../scripts/lib/security-fixture-check.
|
||||
import {
|
||||
evaluatePromotionEvidence,
|
||||
providerEvidenceSignaturePayload,
|
||||
providerPublicKeyFingerprint,
|
||||
providerVerificationArtifactSchema,
|
||||
} from "../../scripts/lib/provider-evidence.ts";
|
||||
import {
|
||||
@@ -51,17 +52,41 @@ const dependency = {
|
||||
|
||||
const candidateDistSha256 = "1".repeat(64);
|
||||
const lockfileSha256 = "2".repeat(64);
|
||||
const NOW = Date.parse("2026-08-02T01:00:00.000Z");
|
||||
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,
|
||||
candidate: Object.freeze({
|
||||
archiveSha256: "3".repeat(64),
|
||||
bundleSha256: "4".repeat(64),
|
||||
distSha256: candidateDistSha256,
|
||||
lockfileSha256,
|
||||
}),
|
||||
vulnerabilityInvocationNonce: "5".repeat(64),
|
||||
provenanceInvocationNonce: "6".repeat(64),
|
||||
});
|
||||
|
||||
function signedProviderEvidence(
|
||||
value: Record<string, unknown>,
|
||||
keyId: string,
|
||||
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
|
||||
publicKeyFingerprint: string,
|
||||
) {
|
||||
return {
|
||||
...value,
|
||||
signature: {
|
||||
algorithm: "Ed25519",
|
||||
keyId,
|
||||
publicKeyFingerprint,
|
||||
value: sign(
|
||||
null,
|
||||
providerEvidenceSignaturePayload(value),
|
||||
@@ -71,6 +96,53 @@ function signedProviderEvidence(
|
||||
};
|
||||
}
|
||||
|
||||
function providerPair(input: Readonly<{
|
||||
vulnerabilityKeys: ReturnType<typeof generateKeyPairSync>;
|
||||
provenanceKeys: ReturnType<typeof generateKeyPairSync>;
|
||||
candidate?: typeof expectedProviderContext.candidate;
|
||||
vulnerabilityFingerprint?: string;
|
||||
provenanceFingerprint?: string;
|
||||
}>) {
|
||||
const candidate = input.candidate ?? expectedProviderContext.candidate;
|
||||
const vulnerabilityFingerprint = input.vulnerabilityFingerprint ??
|
||||
providerPublicKeyFingerprint(input.vulnerabilityKeys.publicKey);
|
||||
const provenanceFingerprint = input.provenanceFingerprint ??
|
||||
providerPublicKeyFingerprint(input.provenanceKeys.publicKey);
|
||||
return {
|
||||
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: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.vulnerabilityInvocationNonce },
|
||||
source: expectedProviderContext.source,
|
||||
candidate,
|
||||
findings: [],
|
||||
}, "fixture-vulnerability-key", input.vulnerabilityKeys.privateKey, vulnerabilityFingerprint),
|
||||
provenanceAttestation: signedProviderEvidence({
|
||||
schemaVersion: 2,
|
||||
evidenceType: "provenance-attestation",
|
||||
provider: "fixture-provenance-provider",
|
||||
issuedAt: "2026-08-02T01:00:00.000Z",
|
||||
expiresAt: "2026-08-02T02:00:00.000Z",
|
||||
run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.provenanceInvocationNonce },
|
||||
source: expectedProviderContext.source,
|
||||
candidate,
|
||||
signer: "fixture-workload-identity",
|
||||
subject: { name: "dist", digest: { sha256: candidate.distSha256 } },
|
||||
}, "fixture-provenance-key", input.provenanceKeys.privateKey, provenanceFingerprint),
|
||||
};
|
||||
}
|
||||
|
||||
function providerTrust(
|
||||
keyId: string,
|
||||
publicKey: ReturnType<typeof generateKeyPairSync>["publicKey"],
|
||||
publicKeyFingerprint = providerPublicKeyFingerprint(publicKey),
|
||||
) {
|
||||
return { keyId, publicKey, publicKeyFingerprint };
|
||||
}
|
||||
|
||||
async function createMinimalCandidateTree(root: string) {
|
||||
const rawLockfile = "lockfileVersion: '9.0'\n";
|
||||
const rawLockfileSha256 = createHash("sha256")
|
||||
@@ -100,37 +172,54 @@ async function createMinimalCandidateTree(root: string) {
|
||||
|
||||
async function writeProviderEnvironment(
|
||||
root: string,
|
||||
distDigest: string,
|
||||
candidateLockfileSha256: 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: 1,
|
||||
schemaVersion: 2,
|
||||
evidenceType: "vulnerability-report",
|
||||
provider: "fixture-vulnerability-provider",
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
scannedLockfileSha256: candidateLockfileSha256,
|
||||
scannedDistSha256: distDigest,
|
||||
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: 1,
|
||||
schemaVersion: 2,
|
||||
evidenceType: "provenance-attestation",
|
||||
provider: "fixture-provenance-provider",
|
||||
signer: "fixture-workload-identity",
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
subject: { name: "dist", digest: { sha256: distDigest } },
|
||||
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"), "fixture archive\n"),
|
||||
writeFile(path.join(root, "provider/candidate.tar.gz"), archiveBytes),
|
||||
writeFile(
|
||||
path.join(root, "provider/vulnerability.json"),
|
||||
`${JSON.stringify(vulnerabilityReport)}\n`,
|
||||
@@ -155,8 +244,13 @@ async function writeProviderEnvironment(
|
||||
return {
|
||||
CANDIDATE_ARCHIVE_PATH: "provider/candidate.tar.gz",
|
||||
CANDIDATE_ARCHIVE_SHA256: createHash("sha256")
|
||||
.update("fixture archive\n")
|
||||
.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",
|
||||
@@ -167,32 +261,37 @@ async function writeProviderEnvironment(
|
||||
}
|
||||
|
||||
describe("supply-chain policy", () => {
|
||||
it("emits a strict role-bound v2 verification record from exact input bytes", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-verification-v2-"));
|
||||
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.distSha256,
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
const environment = await writeProviderEnvironment(root, manifest);
|
||||
const report = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
verifyLocalEvidence: async () => ({ status: "PASS" as const, failures: [] }),
|
||||
} as Parameters<typeof verifyPromotionInputs>[0]);
|
||||
verifyLocalEvidence: async () => ({
|
||||
status: "PASS" as const,
|
||||
identity: localIdentity,
|
||||
failures: [] as const,
|
||||
}),
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
expect(providerVerificationArtifactSchema.parse(report)).toEqual(
|
||||
expect.objectContaining({
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
artifactType: "provider-verification",
|
||||
candidateArchiveSha256: environment.CANDIDATE_ARCHIVE_SHA256,
|
||||
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"),
|
||||
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 {
|
||||
@@ -204,13 +303,10 @@ describe("supply-chain policy", () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-wiring-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const validEnvironment = await writeProviderEnvironment(
|
||||
root,
|
||||
manifest.distSha256,
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
const validEnvironment = await writeProviderEnvironment(root, manifest);
|
||||
const acceptLocalEvidence = async () => ({
|
||||
status: "PASS" as const,
|
||||
identity: localIdentity,
|
||||
failures: [] as const,
|
||||
});
|
||||
const valid = await verifyPromotionInputs({
|
||||
@@ -218,23 +314,34 @@ describe("supply-chain policy", () => {
|
||||
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 wrongEnvironment = await writeProviderEnvironment(
|
||||
root,
|
||||
"3".repeat(64),
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
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({
|
||||
@@ -242,19 +349,23 @@ describe("supply-chain policy", () => {
|
||||
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 });
|
||||
}
|
||||
@@ -264,11 +375,7 @@ describe("supply-chain policy", () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-local-status-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const environment = await writeProviderEnvironment(
|
||||
root,
|
||||
manifest.distSha256,
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
const environment = await writeProviderEnvironment(root, manifest);
|
||||
const localVerificationPath = path.join(
|
||||
root,
|
||||
"artifacts/security/supply-chain-verification.json",
|
||||
@@ -278,12 +385,13 @@ describe("supply-chain policy", () => {
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL_UNVERIFIED");
|
||||
expect(result.failures).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringMatching(/executable schema mismatch/u),
|
||||
"local evidence assessment is missing or invalid",
|
||||
"local supply-chain evidence is not PASS",
|
||||
]),
|
||||
);
|
||||
@@ -393,16 +501,13 @@ describe("supply-chain policy", () => {
|
||||
|
||||
it("fails promotion when external provider evidence is absent", () => {
|
||||
const result = evaluatePromotionEvidence({
|
||||
candidate: {
|
||||
distSha256: candidateDistSha256,
|
||||
lockfileSha256,
|
||||
},
|
||||
currentDistSha256: candidateDistSha256,
|
||||
expected: expectedProviderContext,
|
||||
localStatus: "PASS",
|
||||
vulnerabilityReport: null,
|
||||
provenanceAttestation: null,
|
||||
vulnerabilityTrust: null,
|
||||
provenanceTrust: null,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL_UNVERIFIED");
|
||||
@@ -411,50 +516,25 @@ describe("supply-chain policy", () => {
|
||||
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 { vulnerabilityReport, provenanceAttestation } = providerPair({
|
||||
vulnerabilityKeys,
|
||||
provenanceKeys,
|
||||
});
|
||||
|
||||
const result = evaluatePromotionEvidence({
|
||||
candidate: {
|
||||
distSha256: candidateDistSha256,
|
||||
lockfileSha256,
|
||||
},
|
||||
currentDistSha256: candidateDistSha256,
|
||||
expected: expectedProviderContext,
|
||||
localStatus: "PASS",
|
||||
vulnerabilityReport,
|
||||
provenanceAttestation,
|
||||
vulnerabilityTrust: {
|
||||
keyId: "fixture-vulnerability-key",
|
||||
publicKey: vulnerabilityKeys.publicKey,
|
||||
},
|
||||
provenanceTrust: {
|
||||
keyId: "fixture-provenance-key",
|
||||
publicKey: provenanceKeys.publicKey,
|
||||
},
|
||||
vulnerabilityTrust: providerTrust(
|
||||
"fixture-vulnerability-key",
|
||||
vulnerabilityKeys.publicKey,
|
||||
),
|
||||
provenanceTrust: providerTrust(
|
||||
"fixture-provenance-key",
|
||||
provenanceKeys.publicKey,
|
||||
),
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
@@ -469,54 +549,27 @@ describe("supply-chain policy", () => {
|
||||
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 { vulnerabilityReport, provenanceAttestation } = providerPair({
|
||||
vulnerabilityKeys,
|
||||
provenanceKeys,
|
||||
candidate: { ...expectedProviderContext.candidate, distSha256: wrongDistSha256 },
|
||||
});
|
||||
|
||||
const result = evaluatePromotionEvidence({
|
||||
candidate: {
|
||||
distSha256: candidateDistSha256,
|
||||
lockfileSha256,
|
||||
},
|
||||
currentDistSha256: candidateDistSha256,
|
||||
expected: expectedProviderContext,
|
||||
localStatus: "PASS",
|
||||
vulnerabilityReport,
|
||||
provenanceAttestation,
|
||||
vulnerabilityTrust: {
|
||||
keyId: "fixture-vulnerability-key",
|
||||
publicKey: vulnerabilityKeys.publicKey,
|
||||
},
|
||||
provenanceTrust: {
|
||||
keyId: "fixture-provenance-key",
|
||||
publicKey: provenanceKeys.publicKey,
|
||||
},
|
||||
vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey),
|
||||
provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey),
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL_UNVERIFIED");
|
||||
expect(result.failures).toEqual(
|
||||
expect.arrayContaining([
|
||||
"vulnerability report dist digest mismatch",
|
||||
"provenance attestation dist digest mismatch",
|
||||
"vulnerability report candidate identity mismatch",
|
||||
"provenance attestation candidate identity mismatch",
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -524,103 +577,56 @@ describe("supply-chain policy", () => {
|
||||
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 { vulnerabilityReport, provenanceAttestation } = providerPair({
|
||||
vulnerabilityKeys,
|
||||
provenanceKeys,
|
||||
});
|
||||
|
||||
const result = evaluatePromotionEvidence({
|
||||
candidate: {
|
||||
distSha256: candidateDistSha256,
|
||||
lockfileSha256,
|
||||
expected: {
|
||||
...expectedProviderContext,
|
||||
candidate: { ...expectedProviderContext.candidate, distSha256: "4".repeat(64) },
|
||||
},
|
||||
currentDistSha256: "4".repeat(64),
|
||||
localStatus: "PASS",
|
||||
vulnerabilityReport,
|
||||
provenanceAttestation,
|
||||
vulnerabilityTrust: {
|
||||
keyId: "fixture-vulnerability-key",
|
||||
publicKey: vulnerabilityKeys.publicKey,
|
||||
},
|
||||
provenanceTrust: {
|
||||
keyId: "fixture-provenance-key",
|
||||
publicKey: provenanceKeys.publicKey,
|
||||
},
|
||||
vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey),
|
||||
provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey),
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL_UNVERIFIED");
|
||||
expect(result.failures).toContain(
|
||||
"candidate dist bytes changed after immutable build",
|
||||
);
|
||||
expect(result.failures).toContain("vulnerability report candidate identity mismatch");
|
||||
});
|
||||
|
||||
it("rejects Ed448 keys mislabeled as Ed25519 evidence", () => {
|
||||
const vulnerabilityKeys = generateKeyPairSync("ed448");
|
||||
const provenanceKeys = generateKeyPairSync("ed448");
|
||||
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 fakeFingerprint = `sha256:${"7".repeat(64)}`;
|
||||
const { vulnerabilityReport, provenanceAttestation } = providerPair({
|
||||
vulnerabilityKeys,
|
||||
provenanceKeys,
|
||||
vulnerabilityFingerprint: fakeFingerprint,
|
||||
provenanceFingerprint: fakeFingerprint,
|
||||
});
|
||||
|
||||
expect(
|
||||
evaluatePromotionEvidence({
|
||||
candidate: { distSha256: candidateDistSha256, lockfileSha256 },
|
||||
currentDistSha256: candidateDistSha256,
|
||||
expected: expectedProviderContext,
|
||||
localStatus: "PASS",
|
||||
vulnerabilityReport,
|
||||
provenanceAttestation,
|
||||
vulnerabilityTrust: {
|
||||
keyId: "fixture-vulnerability-key",
|
||||
publicKey: vulnerabilityKeys.publicKey,
|
||||
publicKeyFingerprint: fakeFingerprint,
|
||||
},
|
||||
provenanceTrust: {
|
||||
keyId: "fixture-provenance-key",
|
||||
publicKey: provenanceKeys.publicKey,
|
||||
publicKeyFingerprint: fakeFingerprint,
|
||||
},
|
||||
nowEpochMs: () => NOW,
|
||||
}).status,
|
||||
).toBe("FAIL_UNVERIFIED");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user