From 4d3df316751da0e63ea5466f158587e7a08b5905 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:40:19 +0900 Subject: [PATCH 1/2] test(acquisition): reject malformed deployment evidence bytes --- ...eployment-evidence-input-integrity.test.ts | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 test/acquisition-deployment-evidence-input-integrity.test.ts diff --git a/test/acquisition-deployment-evidence-input-integrity.test.ts b/test/acquisition-deployment-evidence-input-integrity.test.ts new file mode 100644 index 000000000..a4ef0ee31 --- /dev/null +++ b/test/acquisition-deployment-evidence-input-integrity.test.ts @@ -0,0 +1,196 @@ +import { createHash } from "node:crypto"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +const repository = "ContextualWisdomLab/noema"; +const releaseTag = "v0.1.0"; +const commitSha = "a".repeat(40); +const predicateType = "https://contextualwisdomlab.org/attestations/noema-deployment/v1"; + +function deploymentEvidence() { + return { + schemaVersion: 1, + generatedAt: "2026-08-04T00:00:00.000Z", + source: { + repository, + releaseTag, + releaseRef: `refs/tags/${releaseTag}`, + releaseUrl: `https://github.com/${repository}/releases/tag/${releaseTag}`, + version: "0.1.0", + commitSha, + releaseEvidenceSha256: "1".repeat(64), + }, + deployment: { + environment: "production", + workerName: "noema", + workerVersionId: "worker-version-one", + deploymentId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + deployedAt: "2026-08-04T00:00:01.000Z", + deploymentCreatedAt: "2026-08-04T00:00:02.000Z", + trafficPercentage: 100, + targets: ["https://noema.example.workers.dev"], + workflowRunUrl: `https://github.com/${repository}/actions/runs/123`, + }, + rollback: { + previousDeploymentId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + previousWorkerVersionId: "worker-version-zero", + }, + validation: { + immutableRelease: true, + strictKpi: true, + smokePassed: true, + kpiExecutedAt: "2026-08-03T23:59:50.000Z", + smokeTimestamp: "2026-08-04T00:00:04.000Z", + kpiEvidenceSha256: "2".repeat(64), + smokeEvidenceSha256: "3".repeat(64), + }, + }; +} + +function governanceEvidence() { + return { + schema_version: 1, + repository, + environment: "production", + status: "PASS", + reviewer_count: 1, + reviewers: [{ type: "Team", id: 42, identifier: "production-approvers" }], + checks: [ + { name: "required reviewers rule exists", pass: true, detail: "rule_count=1" }, + { name: "deployment initiator cannot self-approve", pass: true, detail: "prevent_self_review=true" }, + { name: "only protected branches may deploy", pass: true, detail: "protected_branches=true" }, + ], + failures: [], + }; +} + +function attestationBundle() { + return { + mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", + verificationMaterial: { tlogEntries: [{}] }, + dsseEnvelope: { payload: "ZXZpZGVuY2U=", signatures: [{ sig: "c2ln" }] }, + }; +} + +function sha256(value: Buffer | string) { + return createHash("sha256").update(value).digest("hex"); +} + +function writeInputs(root: string) { + const deploymentPath = join(root, "deployment-evidence.json"); + const governancePath = join(root, "production-environment-governance.json"); + const bundlePath = join(root, "deployment-evidence.sigstore.json"); + const receiptPath = join(root, "deployment-attestation-verification.json"); + + const deploymentBytes = Buffer.from(`${JSON.stringify(deploymentEvidence(), null, 2)}\n`, "utf8"); + writeFileSync(deploymentPath, deploymentBytes); + writeFileSync(governancePath, `${JSON.stringify(governanceEvidence(), null, 2)}\n`); + writeFileSync(bundlePath, `${JSON.stringify(attestationBundle())}\n`); + writeFileSync(receiptPath, `${JSON.stringify({ + schemaVersion: 1, + verified: true, + repository, + releaseTag, + commitSha, + deploymentEvidenceSha256: sha256(deploymentBytes), + signerWorkflow: `${repository}/.github/workflows/cd.yml`, + predicateType, + oidcIssuer: "https://token.actions.githubusercontent.com", + denySelfHostedRunners: true, + workflowRunUrl: `https://github.com/${repository}/actions/runs/123`, + }, null, 2)}\n`); + + return { deploymentPath, governancePath, bundlePath, receiptPath }; +} + +function runAudit(root: string, paths: ReturnType) { + return spawnSync(process.execPath, ["scripts/acquisition-deployment-evidence-audit.mjs"], { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + NOEMA_RELEASE_UNDER_DILIGENCE_TAG: releaseTag, + NOEMA_ACQUISITION_AUDIT_OUTPUT_DIR: root, + NOEMA_DEPLOYMENT_EVIDENCE_PATH: paths.deploymentPath, + NOEMA_DEPLOYMENT_ATTESTATION_PATH: paths.bundlePath, + NOEMA_DEPLOYMENT_ATTESTATION_VERIFICATION_PATH: paths.receiptPath, + NOEMA_PRODUCTION_ENVIRONMENT_GOVERNANCE_PATH: paths.governancePath, + }, + }); +} + +function malformedJsonBytes(value: object) { + const text = JSON.stringify({ ...value, note: "MALFORMED_SENTINEL" }, null, 2); + const [prefix, suffix] = text.split("MALFORMED_SENTINEL"); + return Buffer.concat([ + Buffer.from(prefix, "utf8"), + Buffer.from([0xff]), + Buffer.from(`${suffix}\n`, "utf8"), + ]); +} + +describe("acquisition deployment evidence byte integrity", () => { + it("keeps the valid exact-byte fixture passing", () => { + const root = mkdtempSync(join(tmpdir(), "noema-acquisition-deployment-bytes-")); + try { + const paths = writeInputs(root); + const result = runAudit(root, paths); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("acquisition-deployment-evidence-audit: PASS"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("rejects malformed UTF-8 deployment evidence even when the receipt matches replacement-decoded text", () => { + const root = mkdtempSync(join(tmpdir(), "noema-acquisition-deployment-bytes-")); + try { + const paths = writeInputs(root); + const malformed = malformedJsonBytes(deploymentEvidence()); + writeFileSync(paths.deploymentPath, malformed); + + const replacementDecodedDigest = sha256(malformed.toString("utf8")); + writeFileSync(paths.receiptPath, `${JSON.stringify({ + schemaVersion: 1, + verified: true, + repository, + releaseTag, + commitSha, + deploymentEvidenceSha256: replacementDecodedDigest, + signerWorkflow: `${repository}/.github/workflows/cd.yml`, + predicateType, + oidcIssuer: "https://token.actions.githubusercontent.com", + denySelfHostedRunners: true, + workflowRunUrl: `https://github.com/${repository}/actions/runs/123`, + }, null, 2)}\n`); + + const result = runAudit(root, paths); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("deployment_evidence_collection_failed"); + expect(result.stdout).toContain("invalid UTF-8"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("rejects malformed UTF-8 in the retained attestation bundle", () => { + const root = mkdtempSync(join(tmpdir(), "noema-acquisition-deployment-bytes-")); + try { + const paths = writeInputs(root); + writeFileSync(paths.bundlePath, malformedJsonBytes(attestationBundle())); + + const result = runAudit(root, paths); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("deployment_evidence_collection_failed"); + expect(result.stdout).toContain("invalid UTF-8"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); From 10e6636a2e72cc7bfcbc840257fc7eaa3d34d9e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:41:09 +0900 Subject: [PATCH 2/2] fix(acquisition): authenticate exact deployment evidence bytes --- .../acquisition-deployment-evidence-audit.mjs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/acquisition-deployment-evidence-audit.mjs b/scripts/acquisition-deployment-evidence-audit.mjs index 8738d0fc1..973e53b3c 100644 --- a/scripts/acquisition-deployment-evidence-audit.mjs +++ b/scripts/acquisition-deployment-evidence-audit.mjs @@ -9,6 +9,7 @@ import { writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; +import { TextDecoder } from "node:util"; import { evaluateAcquisitionDeploymentEvidence } from "./lib/acquisition-deployment-evidence.mjs"; const MAX_EVIDENCE_BYTES = 16 * 1024 * 1024; @@ -34,7 +35,7 @@ function bounded(value, maximum = 4_000) { return compact.length <= maximum ? compact : `${compact.slice(0, maximum)}…`; } -function readRegularText(path, label) { +function readRegularBytes(path, label) { if (!existsSync(path)) { throw new Error(`${label} is missing: ${path}`); } @@ -43,20 +44,31 @@ function readRegularText(path, label) { if (lstat.isSymbolicLink() || !stat.isFile() || stat.size <= 0 || stat.size > MAX_EVIDENCE_BYTES) { throw new Error(`${label} must be a non-empty regular file no larger than ${MAX_EVIDENCE_BYTES} bytes`); } - return readFileSync(path, "utf8"); + return readFileSync(path); +} + +function decodeUtf8(bytes, label) { + try { + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + throw new Error(`${label} contains invalid UTF-8`); + } } function readJson(path, label) { - const text = readRegularText(path, label); + const bytes = readRegularBytes(path, label); + const text = decodeUtf8(bytes, label); try { - return { text, value: JSON.parse(text) }; + return { bytes, text, value: JSON.parse(text) }; } catch (error) { throw new Error(`${label} is invalid JSON: ${bounded(error?.message || error)}`); } } function readBundle(path) { - const text = readRegularText(path, "deployment attestation bundle"); + const label = "deployment attestation bundle"; + const bytes = readRegularBytes(path, label); + const text = decodeUtf8(bytes, label); try { return JSON.parse(text); } catch { @@ -80,8 +92,8 @@ function readBundle(path) { } } -function sha256(text) { - return createHash("sha256").update(text).digest("hex"); +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex"); } function writeAudit(report) { @@ -123,7 +135,7 @@ function evaluateSelectedRelease() { const evaluation = evaluateAcquisitionDeploymentEvidence({ expectedTag: releaseUnderDiligenceTag, deploymentEvidence: deployment.value, - deploymentEvidenceSha256: sha256(deployment.text), + deploymentEvidenceSha256: sha256(deployment.bytes), governanceEvidence: governance.value, attestationBundle, verificationReceipt: receipt.value,