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)

This commit is contained in:
Laan Tungir
2026-07-24 14:43:29 -04:00
parent 66babc5a7a
commit 5d6dbc9ecd
9 changed files with 777 additions and 41 deletions
-1
View File
@@ -7,7 +7,6 @@ build/
# Node.js
node_modules/
package-lock.json
# Source maps
*.bundle.js.map
+3
View File
@@ -0,0 +1,3 @@
[submodule "resources/javascript-opentimestamps"]
path = resources/javascript-opentimestamps
url = https://github.com/opentimestamps/javascript-opentimestamps.git
+9 -9
View File
@@ -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'` + `<meta name="referrer" content="no-referrer">`, 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.14v0.0.18 | Kind 9999 docs (v0.0.14v0.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 |
+600
View File
@@ -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"
}
}
}
}
+1 -1
View File
@@ -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": {
+65 -10
View File
@@ -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;
// 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) {
// G56-12: validate the fetched event's signature before adopting
if (verifyNostrEvent(matching)) {
pqEvent = matching;
otsLog(`Resuming OTS workflow: fetched event ${pqEvent.id.substring(0, 16)}... from relays (${allCandidates.length} total candidates).`);
otsLog(`Resuming OTS workflow: fetched and verified event ${pqEvent.id.substring(0, 16)}... from relays (${allCandidates.length} total candidates).`);
} 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).`);
otsLog(`WARNING: Fetched event ${matching.id.substring(0, 16)}... has invalid signature. Using persisted event data instead.`);
}
} else {
// 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 = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 9999 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
@@ -1046,7 +1095,9 @@
targetKind: pqEvent.kind,
relayUrls: getRelayUrlsString(),
pendingPublished: true,
confirmedPublished: false
confirmedPublished: false,
proofCarrierEvent: pqEvent,
kind1Event
});
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof embedded. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
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.`);
+51 -7
View File
@@ -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));
}
+3 -3
View File
@@ -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"
}
+43 -8
View File
@@ -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, budget) {
if (budget) {
budget.operations++;
if (budget.operations > OTS_MAX_OPERATIONS) {
throw new Error(`OTS: operation count exceeds maximum ${OTS_MAX_OPERATIONS}`);
}
}
function _processTimestampEntry(reader, tag, msg, attestations, depth) {
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() {