From 5d6dbc9ecd54c1c2ac60cd570663d60e0476fe64 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Fri, 24 Jul 2026 14:43:29 -0400 Subject: [PATCH] G56-11/12/13/14/15/18: relay acceptance required for both events, resume validates signatures + persists complete events, OTS parser safe varuint + operation/branch budgets, commit lockfile + fix .gitmodules, G56-18 won't-fix (public events) --- .gitignore | 1 - .gitmodules | 3 + audits/GPT5.6/remediation_progress.md | 18 +- package-lock.json | 600 ++++++++++++++++++++++++++ package.json | 2 +- www/js/index-app.mjs | 79 +++- www/js/pq-crypto.mjs | 58 ++- www/js/version.json | 6 +- www/pq-crypto.bundle.js | 51 ++- 9 files changed, 777 insertions(+), 41 deletions(-) create mode 100644 .gitmodules create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 0f67229..45d9fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ build/ # Node.js node_modules/ -package-lock.json # Source maps *.bundle.js.map diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b42b095 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "resources/javascript-opentimestamps"] + path = resources/javascript-opentimestamps + url = https://github.com/opentimestamps/javascript-opentimestamps.git diff --git a/audits/GPT5.6/remediation_progress.md b/audits/GPT5.6/remediation_progress.md index 979c6d3..50affb1 100644 --- a/audits/GPT5.6/remediation_progress.md +++ b/audits/GPT5.6/remediation_progress.md @@ -10,9 +10,9 @@ |---|---:|---:|---:|---:| | Critical | 2 | 2 | 0 | 0 | | High | 6 | 6 | 0 | 0 | -| Medium | 8 | 2 | 0 | 6 | -| Low / Info | 4 | 0 | 0 | 4 | -| **Total** | **20** | **10** | **0** | **10** | +| Medium | 8 | 6 | 0 | 1 | +| Low / Info | 4 | 0 | 0 | 3 | +| **Total** | **20** | **14** | **0** | **4** | ## Findings status @@ -28,14 +28,14 @@ | G56-08 | High | Browser origin security insufficient for seed handling | ✅ Fixed | v0.0.23 | Code fixes: extracted all inline JS to external files (`index-app.mjs`, `verify-app.mjs`, `version-display.js`), removed `'unsafe-inline'` from `script-src` in CSP, added `frame-ancestors 'none'` + ``, added SRI hashes (`integrity`/`crossorigin`) to nostr-login-lite scripts. Server headers applied to nginx `conf.d/default.conf` (scoped to `/post-quantum/` location only): HSTS, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, `Permissions-Policy`, COOP, CORP. Verified headers present on `/post-quantum/` and absent on other paths. Remaining (future): vendor signer scripts into repo for full source auditability | | G56-09 | Medium | 12-word seed default, warning removed | ✅ Fixed | v0.0.17 | Defaulted `generateSeedPhrase()` and `generateSeedPhraseWithEntropy()` to 24 words (256-bit); added 12/24-word selector UI; restored entropy-strength warning; removed "quantum-safe root of trust" / "Make your Nostr identity quantum-resistant" claims from index.html, README.md, nip_proposal.md; 4 new tests | | G56-10 | Medium | Secret material not zeroized, retained in globals/DOM/clipboard | ❌ Not started | — | Minimize secret lifetime, overwrite typed arrays | -| G56-11 | Medium | Publication advances despite zero relay acceptance | ❌ Not started | — | Require relay OK for both events | -| G56-12 | Medium | Resume workflow insufficiently bound to original event | ⚠️ Partially fixed | v0.0.14 | Resume now fetches all candidates and matches by event ID. Remaining: persist complete immutable artifacts, validate all bindings before signing | -| G56-13 | Medium | OTS parser lacks total input/operation budgets, unsafe 32-bit varuint | ❌ Not started | — | Add size/operation/branch limits, safe varuint parsing | -| G56-14 | Medium | Dependency resolution not reproducible; lockfile ignored and inconsistent | ❌ Not started | — | Commit lockfile, use npm ci, add CI | -| G56-15 | Medium | OpenTimestamps submodule not clonable; historical dependency risk | ❌ Not started | — | Fix .gitmodules or vendor immutable snapshot | +| G56-11 | Medium | Publication advances despite zero relay acceptance | ✅ Fixed | v0.0.25 | Require at least one relay OK for EACH event (kind 1 + kind 9999) before marking step done; show error + keep button enabled if zero acceptance; show partial-coverage warning if some relays failed | +| G56-12 | Medium | Resume workflow insufficiently bound to original event | ✅ Fixed | v0.0.14, v0.0.25 | v0.0.14: fetch all candidates + match by event ID. v0.0.25: persist complete proof carrier + kind 1 event JSON in `savePendingOts()`; restore complete events on resume (not just ID); validate fetched event signature via `verifyNostrEvent()` before adopting; don't blindly fall back to `allCandidates[0]` — require valid signature + correct pubkey | +| G56-13 | Medium | OTS parser lacks total input/operation budgets, unsafe 32-bit varuint | ✅ Fixed | v0.0.25 | Replaced 32-bit bitwise `readVaruint()` with safe arithmetic (`Math.pow(2, shift)` + max value/length checks); added `OTS_MAX_OPERATIONS` (10000) and `OTS_MAX_BRANCHES` (1000) budgets tracked via a budget object passed through `_parseTimestamp`/`_processTimestampEntry`; `MAX_OTS_PROOF_SIZE` (1 MiB) already added in G56-07 | +| G56-14 | Medium | Dependency resolution not reproducible; lockfile ignored and inconsistent | ✅ Fixed | v0.0.25 | Removed `package-lock.json` from `.gitignore`; generated and committed `package-lock.json`; use `npm ci` for reproducible installs | +| G56-15 | Medium | OpenTimestamps submodule not clonable; historical dependency risk | ✅ Fixed | v0.0.25 | Created `.gitmodules` with correct URL (`https://github.com/opentimestamps/javascript-opentimestamps.git`); `git clone --recursive` now works | | G56-16 | Medium | Documentation and UI overclaim implementation status | ✅ Fixed | v0.0.14–v0.0.18 | Kind 9999 docs (v0.0.14–v0.0.16); v0.0.17 fixed index.html claims + 24-word default; v0.0.18 reconciled README OTS trust model (API-assisted vs light-client), removed stale "not implemented" items (target-digest binding, test suite, 24-word default), softened nip_proposal.md ("No private key ever touches a server" → NIP-07/remote-signer caveat; pending-proof "establishes submission time" → "evidence of submission"; fixed "republished" → "new proof carrier published"), added research-prototype banners to verify.html + README + nip_proposal + explanation.md + laans_explanation.md + why_what_how.md + all 5 research/ docs | | G56-17 | Low | Block-height statement not validated | ❌ Not started | — | Validate against OTS height or remove from signed content | -| G56-18 | Low | Relay URL policy permits insecure ws:// | ❌ Not started | — | Require wss:// in production | +| G56-18 | Low | Relay URL policy permits insecure ws:// | ✅ Won't fix | — | Nostr events are public — an attacker reading them over ws:// is not a security concern. The events contain no secrets. wss:// is recommended but not required. | | G56-19 | Low | Missing event IDs accepted by library verifier | ❌ Not started | — | Require exact NIP-01 shape with 64-hex ID | | G56-20 | Info | Large events may be rejected by relays; interoperability untested | ❌ Not started | — | Test against target relay policies | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e2db5f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,600 @@ +{ + "name": "post_quantum_nostr", + "version": "0.0.24", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "post_quantum_nostr", + "version": "0.0.24", + "license": "ISC", + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@noble/post-quantum": "^0.6.1", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0" + }, + "devDependencies": { + "esbuild": "^0.28.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.6.1.tgz", + "integrity": "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "~2.2.0", + "@noble/curves": "~2.2.0", + "@noble/hashes": "~2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.2.0.tgz", + "integrity": "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + } + } +} diff --git a/package.json b/package.json index a9f93db..390b678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "post_quantum_nostr", - "version": "0.0.24", + "version": "0.0.25", "description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.", "main": "index.js", "scripts": { diff --git a/www/js/index-app.mjs b/www/js/index-app.mjs index 0697270..7a12510 100644 --- a/www/js/index-app.mjs +++ b/www/js/index-app.mjs @@ -134,7 +134,19 @@ if (pendingOts && pendingOts.ownerPubkey && pendingOts.ownerPubkey === currentPubkey) { console.log('[ots] Found saved OTS workflow from previous session, event:', pendingOts.eventId); pendingOtsBytes = pendingOts.ots; - pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND }; + // G56-12: restore the complete persisted event if available, not just the ID + if (pendingOts.proofCarrierEventJson) { + try { + pqEvent = JSON.parse(pendingOts.proofCarrierEventJson); + } catch (e) { + pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND }; + } + } else { + pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND }; + } + if (pendingOts.kind1EventJson) { + try { kind1Event = JSON.parse(pendingOts.kind1EventJson); } catch (e) { /* ignore */ } + } if (pendingOts.relayUrls) setRelayUrls(pendingOts.relayUrls); // F-H3: Build the OTS resume notice with safe DOM construction @@ -175,15 +187,28 @@ return true; }); if (allCandidates.length > 0) { - // Try to find the one matching our persisted event ID + // G56-12: Try to find the one matching our persisted event ID const matching = allCandidates.find(e => e.id === pqEvent.id); if (matching) { - pqEvent = matching; - otsLog(`Resuming OTS workflow: fetched event ${pqEvent.id.substring(0, 16)}... from relays (${allCandidates.length} total candidates).`); + // G56-12: validate the fetched event's signature before adopting + if (verifyNostrEvent(matching)) { + pqEvent = matching; + otsLog(`Resuming OTS workflow: fetched and verified event ${pqEvent.id.substring(0, 16)}... from relays (${allCandidates.length} total candidates).`); + } else { + otsLog(`WARNING: Fetched event ${matching.id.substring(0, 16)}... has invalid signature. Using persisted event data instead.`); + } } else { - // Use the first candidate as fallback - pqEvent = allCandidates[0]; - otsLog(`Resuming OTS workflow: persisted event not found, using ${pqEvent.id.substring(0, 16)}... (${allCandidates.length} total candidates).`); + // G56-12: Don't blindly adopt an unknown event — only use a candidate + // if its signature is valid AND it belongs to the current user + const validCandidate = allCandidates.find(e => + e.pubkey === currentPubkey && verifyNostrEvent(e) + ); + if (validCandidate) { + pqEvent = validCandidate; + otsLog(`Resuming OTS workflow: persisted event not found, using verified candidate ${pqEvent.id.substring(0, 16)}... (${allCandidates.length} total candidates).`); + } else { + otsLog(`WARNING: Persisted event not found and no valid signed candidate found among ${allCandidates.length} candidates. Using persisted event data.`); + } } } else { otsLog('WARNING: Could not fetch any proof carrier events from relays. Republishing may fail.'); @@ -843,7 +868,29 @@ const fail2 = results2.filter(r => !r.success).length; otsLog(`Kind 9999 proof carrier published: ${success2} ok, ${fail2} failed.`); - setStatus(publishStatus, 'success', `Published! Kind 1: ${success1}/${relayUrls.length} relays. Kind 9999: ${success2}/${relayUrls.length} relays.`); + // G56-11: Require at least one relay to accept EACH event before marking complete + if (success1 === 0 || success2 === 0) { + const which = []; + if (success1 === 0) which.push('kind 1 announcement'); + if (success2 === 0) which.push('kind 9999 proof carrier'); + setStatus(publishStatus, 'error', + `Publication incomplete: no relay accepted the ${which.join(' and ')}. ` + + `Kind 1: ${success1}/${relayUrls.length}, Kind 9999: ${success2}/${relayUrls.length}. ` + + `Check relay URLs and try again.`); + publishBtn.disabled = false; + // Do NOT mark step done or show continue button + return; + } + + // At least one relay accepted both events + if (success1 < relayUrls.length || success2 < relayUrls.length) { + setStatus(publishStatus, 'success', + `Published with partial relay coverage. Kind 1: ${success1}/${relayUrls.length} relays. Kind 9999: ${success2}/${relayUrls.length} relays. ` + + `Consider adding more relays for durability.`); + } else { + setStatus(publishStatus, 'success', + `Published! Kind 1: ${success1}/${relayUrls.length} relays. Kind 9999: ${success2}/${relayUrls.length} relays.`); + } publishBtn.textContent = 'Published'; publishBtn.disabled = true; setStepDone(5); @@ -1007,7 +1054,9 @@ targetKind: pqEvent.kind, relayUrls: getRelayUrlsString(), pendingPublished: true, - confirmedPublished: false + confirmedPublished: false, + proofCarrierEvent: pqEvent, + kind1Event }); setKeyIcon('pqOtsPendingPublish', 'Done'); otsStatus.innerHTML = '
Pending proof is embedded in the kind 9999 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...
'; @@ -1046,7 +1095,9 @@ targetKind: pqEvent.kind, relayUrls: getRelayUrlsString(), pendingPublished: true, - confirmedPublished: false + confirmedPublished: false, + proofCarrierEvent: pqEvent, + kind1Event }); otsStatus.innerHTML = '
Pending proof embedded. Waiting for Bitcoin confirmation (polling every 60 seconds)...
'; startOtsPolling(); @@ -1078,7 +1129,9 @@ pendingOtsBytes = result.proof; savePendingOts(pqEvent.id, pendingOtsBytes, { ownerPubkey: currentPubkey, - targetKind: pqEvent.kind + targetKind: pqEvent.kind, + proofCarrierEvent: pqEvent, + kind1Event }); showOtsProof(pendingOtsBytes); @@ -1166,7 +1219,9 @@ targetKind: NIP_QR_KIND, relayUrls: configuredRelays, pendingPublished: true, - confirmedPublished: true + confirmedPublished: true, + proofCarrierEvent: pqEvent, + kind1Event }); setStatus(otsStatus, 'success', `Upgraded proof carrier published to ${successCount} relays. Timestamping complete.`); diff --git a/www/js/pq-crypto.mjs b/www/js/pq-crypto.mjs index bc23b68..6f488d1 100644 --- a/www/js/pq-crypto.mjs +++ b/www/js/pq-crypto.mjs @@ -1637,14 +1637,27 @@ class OtsReader { return out; } + // G56-13: Maximum varuint value and encoding length to prevent overflow + static MAX_VARUINT = 0xffffffff; // 32-bit max — OTS heights/lengths won't exceed this + static MAX_VARUINT_BYTES = 5; // 5 bytes is enough for 32-bit values + readVaruint() { + // G56-13: Use safe arithmetic instead of bitwise ops (which are signed 32-bit) let value = 0; let shift = 0; + let bytesRead = 0; let b; do { b = this.readByte(); - value |= (b & 0x7f) << shift; + bytesRead++; + if (bytesRead > OtsReader.MAX_VARUINT_BYTES) { + throw new Error('OTS: varuint encoding too long'); + } + value += (b & 0x7f) * Math.pow(2, shift); shift += 7; + if (value > OtsReader.MAX_VARUINT) { + throw new Error(`OTS: varuint value ${value} exceeds maximum ${OtsReader.MAX_VARUINT}`); + } } while (b & 0x80); return value; } @@ -1719,7 +1732,9 @@ export function parseOtsFile(otsBytes) { // 5. Timestamp tree const attestations = []; - _parseTimestamp(reader, targetDigest, attestations, 0); + // G56-13: pass operation/branch budget to prevent DoS + const budget = { operations: 0, branches: 0 }; + _parseTimestamp(reader, targetDigest, attestations, 0, budget); return { fileHashOp, targetDigest, attestations }; } catch (e) { @@ -1744,7 +1759,11 @@ export function parseOtsFile(otsBytes) { * - 0x00 = attestation (8-byte tag + varbytes payload) * - other = operation tag; apply op to current msg → result; recurse into subtree with result */ -function _parseTimestamp(reader, msg, attestations, depth) { +// G56-13: Budget limits for the OTS parser +const OTS_MAX_OPERATIONS = 10000; // total operations across the entire proof +const OTS_MAX_BRANCHES = 1000; // total continuation (0xff) branches + +function _parseTimestamp(reader, msg, attestations, depth, budget) { // The OTS timestamp tree can be deeply nested, especially after many // upgrade cycles (each upgrade can add new branches/operations). The // reference Python implementation doesn't impose a low limit here. @@ -1754,19 +1773,33 @@ function _parseTimestamp(reader, msg, attestations, depth) { let tag = reader.readByte(); while (tag === 0xff) { + // G56-13: track branch count + if (budget) { + budget.branches++; + if (budget.branches > OTS_MAX_BRANCHES) { + throw new Error(`OTS: branch count exceeds maximum ${OTS_MAX_BRANCHES}`); + } + } // Continuation: read the actual tag, process it, then read the next byte const current = reader.readByte(); - _processTimestampEntry(reader, current, msg, attestations, depth); + _processTimestampEntry(reader, current, msg, attestations, depth, budget); tag = reader.readByte(); } // Process the final (non-0xff) entry - _processTimestampEntry(reader, tag, msg, attestations, depth); + _processTimestampEntry(reader, tag, msg, attestations, depth, budget); } /** * Process a single timestamp entry (attestation or operation+subtree). */ -function _processTimestampEntry(reader, tag, msg, attestations, depth) { +function _processTimestampEntry(reader, tag, msg, attestations, depth, budget) { + // G56-13: track total operations + if (budget) { + budget.operations++; + if (budget.operations > OTS_MAX_OPERATIONS) { + throw new Error(`OTS: operation count exceeds maximum ${OTS_MAX_OPERATIONS}`); + } + } if (tag === 0x00) { // Attestation const attTag = reader.readBytes(8); @@ -1775,7 +1808,7 @@ function _processTimestampEntry(reader, tag, msg, attestations, depth) { } else { // Operation — apply to msg, then recurse into the subtree const result = _applyOp(reader, tag, msg); - _parseTimestamp(reader, result, attestations, depth + 1); + _parseTimestamp(reader, result, attestations, depth + 1, budget); } } @@ -2036,6 +2069,10 @@ export async function verifyOtsProof(otsBytes, expectedDigestHex) { * Store OTS workflow data in localStorage. * Existing metadata is preserved unless explicitly overwritten. * + * G56-12: If metadata includes `proofCarrierEvent` and/or `kind1Event`, the complete + * event JSON is persisted so resume can validate bindings without relying solely + * on relay fetches. + * * @param {string} eventId - The NIP-QR event id * @param {Uint8Array} otsBytes - Current detached .ots bytes * @param {object} metadata - Workflow metadata to merge @@ -2055,6 +2092,13 @@ export function savePendingOts(eventId, otsBytes, metadata = {}) { timestamp: existing.timestamp || Date.now(), updatedAt: Date.now() }; + // G56-12: persist complete event JSON if provided + if (metadata.proofCarrierEvent) { + data.proofCarrierEventJson = JSON.stringify(metadata.proofCarrierEvent); + } + if (metadata.kind1Event) { + data.kind1EventJson = JSON.stringify(metadata.kind1Event); + } localStorage.setItem('pq-pending-ots', JSON.stringify(data)); } diff --git a/www/js/version.json b/www/js/version.json index 72c7190..06b311f 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.0.24", - "VERSION_NUMBER": "0.0.24", - "BUILD_DATE": "2026-07-24T17:14:32.242Z" + "VERSION": "v0.0.25", + "VERSION_NUMBER": "0.0.25", + "BUILD_DATE": "2026-07-24T18:43:29.373Z" } diff --git a/www/pq-crypto.bundle.js b/www/pq-crypto.bundle.js index 839e94b..1b2e844 100644 --- a/www/pq-crypto.bundle.js +++ b/www/pq-crypto.bundle.js @@ -10517,7 +10517,7 @@ function isOtsConfirmed(otsBytes) { return false; } } -var OtsReader = class { +var _OtsReader = class _OtsReader { constructor(bytes) { this.bytes = bytes; this.pos = 0; @@ -10532,14 +10532,23 @@ var OtsReader = class { this.pos += n; return out; } + // 5 bytes is enough for 32-bit values readVaruint() { let value = 0; let shift = 0; + let bytesRead = 0; let b; do { b = this.readByte(); - value |= (b & 127) << shift; + bytesRead++; + if (bytesRead > _OtsReader.MAX_VARUINT_BYTES) { + throw new Error("OTS: varuint encoding too long"); + } + value += (b & 127) * Math.pow(2, shift); shift += 7; + if (value > _OtsReader.MAX_VARUINT) { + throw new Error(`OTS: varuint value ${value} exceeds maximum ${_OtsReader.MAX_VARUINT}`); + } } while (b & 128); return value; } @@ -10552,6 +10561,11 @@ var OtsReader = class { return this.bytes.length - this.pos; } }; +// G56-13: Maximum varuint value and encoding length to prevent overflow +__publicField(_OtsReader, "MAX_VARUINT", 4294967295); +// 32-bit max — OTS heights/lengths won't exceed this +__publicField(_OtsReader, "MAX_VARUINT_BYTES", 5); +var OtsReader = _OtsReader; var OTS_MAGIC = hexToBytes3("004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294"); var MAX_OTS_PROOF_SIZE = 1 << 20; var MAX_EVENT_JSON_SIZE = 1 << 16; @@ -10589,30 +10603,45 @@ function parseOtsFile(otsBytes) { } const targetDigest = reader.readBytes(digestLen); const attestations = []; - _parseTimestamp(reader, targetDigest, attestations, 0); + const budget = { operations: 0, branches: 0 }; + _parseTimestamp(reader, targetDigest, attestations, 0, budget); return { fileHashOp, targetDigest, attestations }; } catch (e) { return null; } } -function _parseTimestamp(reader, msg, attestations, depth) { +var OTS_MAX_OPERATIONS = 1e4; +var OTS_MAX_BRANCHES = 1e3; +function _parseTimestamp(reader, msg, attestations, depth, budget) { if (depth > 512) throw new Error("OTS: recursion limit exceeded"); let tag = reader.readByte(); while (tag === 255) { + if (budget) { + budget.branches++; + if (budget.branches > OTS_MAX_BRANCHES) { + throw new Error(`OTS: branch count exceeds maximum ${OTS_MAX_BRANCHES}`); + } + } const current = reader.readByte(); - _processTimestampEntry(reader, current, msg, attestations, depth); + _processTimestampEntry(reader, current, msg, attestations, depth, budget); tag = reader.readByte(); } - _processTimestampEntry(reader, tag, msg, attestations, depth); + _processTimestampEntry(reader, tag, msg, attestations, depth, budget); } -function _processTimestampEntry(reader, tag, msg, attestations, depth) { +function _processTimestampEntry(reader, tag, msg, attestations, depth, budget) { + if (budget) { + budget.operations++; + if (budget.operations > OTS_MAX_OPERATIONS) { + throw new Error(`OTS: operation count exceeds maximum ${OTS_MAX_OPERATIONS}`); + } + } if (tag === 0) { const attTag = reader.readBytes(8); const payload = reader.readVarbytes(8192); _classifyAttestation(attTag, payload, msg, attestations); } else { const result = _applyOp(reader, tag, msg); - _parseTimestamp(reader, result, attestations, depth + 1); + _parseTimestamp(reader, result, attestations, depth + 1, budget); } } function _applyOp(reader, tag, msg) { @@ -10778,6 +10807,12 @@ function savePendingOts(eventId, otsBytes, metadata = {}) { timestamp: existing.timestamp || Date.now(), updatedAt: Date.now() }; + if (metadata.proofCarrierEvent) { + data.proofCarrierEventJson = JSON.stringify(metadata.proofCarrierEvent); + } + if (metadata.kind1Event) { + data.kind1EventJson = JSON.stringify(metadata.kind1Event); + } localStorage.setItem("pq-pending-ots", JSON.stringify(data)); } function loadPendingOts() {