0def51186dd4
fix: package the Chrome archive with the deterministic ZIP writer instead of system zip (audit PB-26-611, v5.1.20)
- time
- 2026-09-06 15:51 UTC · 1 month ago
- machine
- koh2
- agent
- openrouter/tencent/hy4-preview
- session
- PB-26-601..611 audit closeout
- parent
- 261aa512aa67
↓ peach-browser-0def5118.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1scripts/release-lib.test.ts+44 −3scripts/release-lib.ts+35 −31manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.1.19",4+ "version": "5.1.20",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.1.19",5+ "version": "5.1.20",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.1.19",3+ "version": "5.1.20",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",scripts/release-lib.test.ts+44 −3
@@ -1,6 +1,7 @@11 // scripts/release-lib.test.ts22 // PB-26-211: release tool hardening — security tests for the argv-based runner.3-import { readFileSync } from "node:fs";3+import { createHash } from "node:crypto";4+import { existsSync, readFileSync, rmSync } from "node:fs";45 import { fileURLToPath } from "node:url";56 import { dirname, join, resolve } from "node:path";67 import { afterEach, describe, expect, it } from "vitest";@@ -85,11 +86,13 @@8586 8687 for (const name of ALL_SECRET_NAMES) setEnv(name, "secret-value");8788 88- packageChromeZip("9.9.9", fake);89+ packageChromeZip("9.9.9");8990 uploadChrome("9.9.9", fake);9091 signFirefox("/tmp/fake-source.zip", fake);9192 92- expect(captured.length).toBeGreaterThanOrEqual(3);93+ // packageChromeZip no longer shells out at all (PB-26-611), so only the94+ // upload and the AMO sign remain.95+ expect(captured.length).toBeGreaterThanOrEqual(2);9396 for (const spec of captured) {9497 expect(spec.executable).not.toBe("npx");9598 expect(spec.args).not.toContain("npx");@@ -267,3 +270,41 @@267270 }268271 });269272 });273+274+// ─── 7. PB-26-611 reproducible Chrome archive ──────────────────────────────275+276+describe.skipIf(!existsSync(join(ROOT, "dist-chrome", "manifest.json")))(277+ "PB-26-611: Chrome extension archive is reproducible",278+ () => {279+ afterEach(() => {280+ const zipPath = join(ROOT, "artifacts", "peach-chrome-9.9.9.zip");281+ if (existsSync(zipPath)) rmSync(zipPath);282+ });283+284+ it("two archives built from the same dist are byte-identical", () => {285+ const first = readFileSync(packageChromeZip("9.9.9"));286+ const second = readFileSync(packageChromeZip("9.9.9"));287+288+ expect(first.length).toBeGreaterThan(0);289+ expect(createHash("sha256").update(first).digest("hex")).toBe(290+ createHash("sha256").update(second).digest("hex"),291+ );292+ });293+294+ it("embeds no source maps, .DS_Store or __MACOSX entries", () => {295+ const zip = packageChromeZip("9.9.9");296+ const bytes = readFileSync(zip);297+ const text = bytes.toString("latin1");298+299+ expect(text).not.toContain(".map");300+ expect(text).not.toContain(".DS_Store");301+ expect(text).not.toContain("__MACOSX");302+ });303+304+ it("release-lib no longer shells out to the system zip binary", () => {305+ const source = readFileSync(join(ROOT, "scripts", "release-lib.ts"), "utf8");306+ expect(source).not.toContain('"zip"');307+ expect(source).toContain("createDeterministicZip");308+ });309+ },310+);scripts/release-lib.ts+35 −31
@@ -3,9 +3,11 @@33 // No shell, no online resolution, no inherited store credentials.44 55 import { spawnSync } from "node:child_process";6-import { existsSync, mkdirSync, readFileSync } from "node:fs";7-import { basename, join } from "node:path";6+import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";7+import { basename, join, relative, sep } from "node:path";88 9+import { createDeterministicZip } from "./package-source";10+911 const ROOT = process.cwd();1012 const BIN_DIR = join(ROOT, "node_modules", ".bin");1113 @@ -208,35 +210,37 @@208210 return dir;209211 }210212 211-export function packageChromeZip(212- version: string,213- runner: Runner = runCommand,214-): string {215- const artifactsDir = ensureArtifactsDir();216- const zipPath = join(artifactsDir, `peach-chrome-${version}.zip`);217- if (existsSync(zipPath)) {218- runner("trash", [zipPath], {219- label: "Moving existing Chrome archive to Trash",220- });221- }222- runner(223- "zip",224- [225- "-r",226- zipPath,227- ".",228- "--exclude",229- "*.map",230- "--exclude",231- ".DS_Store",232- "--exclude",233- "__MACOSX/*",234- ],235- {236- cwd: join(ROOT, "dist-chrome"),237- label: `Packaging Chrome extension → peach-chrome-${version}.zip`,238- },213+/** Matches the exclusions the previous system-`zip` call applied. */214+function isPackagingNoise(relativePath: string): boolean {215+ return (216+ relativePath.endsWith(".map") ||217+ basename(relativePath) === ".DS_Store" ||218+ relativePath.startsWith(`__MACOSX${sep}`) ||219+ relativePath.startsWith("__MACOSX/")239220 );221+}222+223+function collectDirectory(dir: string): Array<{ path: string; data: Buffer }> {224+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {225+ const full = join(dir, entry.name);226+ if (entry.isDirectory()) return collectDirectory(full);227+ return [{ path: relative(dir, full).split(sep).join("/"), data: readFileSync(full) }];228+ });229+}230+231+/**232+ * PB-26-611: the system `zip` embedded filesystem timestamps, so two builds of233+ * the same commit produced different archives and the shipped Chrome artifact234+ * could not be verified by hash. Reuses the deterministic writer the AMO235+ * source archive already uses — fixed DOS timestamps, deflate level 9, sorted236+ * entries.237+ */238+export function packageChromeZip(version: string): string {239+ const zipPath = join(ensureArtifactsDir(), `peach-chrome-${version}.zip`);240+ const entries = collectDirectory(join(ROOT, "dist-chrome")).filter(241+ (entry) => !isPackagingNoise(entry.path),242+ );243+ writeFileSync(zipPath, createDeterministicZip(entries));240244 return zipPath;241245 }242246 @@ -244,7 +248,7 @@244248 version: string,245249 runner: Runner = runCommand,246250 ): void {247- const zipPath = packageChromeZip(version, runner);251+ const zipPath = packageChromeZip(version);248252 const extensionId = requireEnv("CWS_EXTENSION_ID");249253 runner(250254 resolveBin("chrome-webstore-upload"),