Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc66a5fa3e | ||
|
|
5ebc6d8d2a | ||
|
|
5d6dbc9ecd |
@@ -7,7 +7,6 @@ build/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Source maps
|
||||
*.bundle.js.map
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "resources/javascript-opentimestamps"]
|
||||
path = resources/javascript-opentimestamps
|
||||
url = https://github.com/opentimestamps/javascript-opentimestamps.git
|
||||
@@ -206,6 +206,21 @@ testing. Users concerned about quantum attacks on the seed entropy itself should
|
||||
bigger risk to seed phrases is classical (theft, phishing), not quantum. Note that BIP39's PBKDF2
|
||||
iteration count is low; the seed phrase should be treated as a high-value secret and stored offline.
|
||||
|
||||
### Secret Material Lifetime and Zeroization (G56-10)
|
||||
|
||||
The web app derives the BIP39 seed, secp256k1 key, and five PQ secret keys in browser memory.
|
||||
On sign-out, the app **zeroizes all `Uint8Array` secret buffers** (fills with zeros before
|
||||
nulling references), **clears the DOM** (seed display, seed input, entropy hint), and **clears
|
||||
the clipboard** (in case the user copied the seed phrase).
|
||||
|
||||
**Important limitation:** JavaScript garbage collection does **not** guarantee erasure. The
|
||||
browser runtime may copy buffers internally, and zeroizing a `Uint8Array` only overwrites the
|
||||
JS-visible buffer — the engine may retain copies in its own memory management. Browser
|
||||
extensions, devtools memory inspectors, and page snapshots may still recover secrets during
|
||||
the session. The real fix is moving key derivation and signing into a hardware or native signer
|
||||
boundary (see G56-06 and G56-08). The zeroization implemented here is a best-effort reduction
|
||||
of the exposure window, not a guarantee.
|
||||
|
||||
---
|
||||
|
||||
## Component 2: Multi-Scheme Cross-Signed Key-Link Events
|
||||
@@ -814,6 +829,7 @@ The current implementation is a static web app (`www/`) that performs the full m
|
||||
| Quantum-safe self-storage (Component 5) | OTP or symmetric-key encryption for kind 30078 data |
|
||||
| PQ event authentication / rotation / revocation | Companion protocols for signing future events with PQ keys, rotating/revoking keys, PQ encryption |
|
||||
| Independent implementation / test vectors | A second implementation reproducing canonical encoding, derivation, and selection |
|
||||
| Relay event-size interop testing | The kind 1 announcement is ~20–30 KiB; the kind 9999 proof carrier embeds it plus OTS proof data. The UI shows the event size and warns if it exceeds ~60 KiB (some relays reject large events). Testing against target relay policies and a compact binary format remain as future work. |
|
||||
|
||||
### Dependencies
|
||||
|
||||
|
||||
@@ -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 | 7 | 0 | 0 |
|
||||
| Low / Info | 4 | 3 | 0 | 0 |
|
||||
| **Total** | **20** | **18** | **0** | **0** |
|
||||
|
||||
## Findings status
|
||||
|
||||
@@ -27,17 +27,17 @@
|
||||
| G56-07 | High | Verifier throws on malformed relay/paste input | ✅ Fixed | v0.0.12, v0.0.19 | v0.0.12 wrapped base64/hex in try/catch + verifyNIPQRContent fail closed + verify.html call-site wrapping. v0.0.19 added structural validation + try/catch to `verifyNostrEvent()` (returns false on malformed input), wrapped `parseOtsFile()` body in try/catch (returns null), added `MAX_OTS_PROOF_SIZE` (1 MiB) and `MAX_EVENT_JSON_SIZE` (64 KiB) size guards, 23 fuzz/property tests (random JSON, random bytes, truncation, oversized inputs, wrong-typed objects) |
|
||||
| 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-10 | Medium | Secret material not zeroized, retained in globals/DOM/clipboard | ✅ Fixed | v0.0.26 | Sign-out now zeroizes all `Uint8Array` secret buffers (BIP39 seed, secp private key, 5 PQ secret keys) via `fill(0)` before nulling references; clears DOM (seed display, seed input, entropy hint); clears clipboard. Documented the JS zeroization limitation in README (garbage collection doesn't guarantee erasure; real fix is hardware/native signer boundary) |
|
||||
| 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-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 |
|
||||
| G56-17 | Low | Block-height statement not validated | ✅ Fixed | v0.0.27 | Removed unverified `block_height` tag from signed content and "pre-quantum at Bitcoin block height N" from the attestation statement. The OTS proof is the real anchor; the claimed height was redundant and could be wrong (height 0 on API failure) |
|
||||
| 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 | ✅ Fixed | v0.0.27 | `verifyNostrEvent()` now requires `id` to be present and a 64-lowercase-hex string (was optional/truthy-check). 2 new tests: missing id rejected, malformed id rejected |
|
||||
| G56-20 | Info | Large events may be rejected by relays; interoperability untested | ✅ Fixed | v0.0.27 | UI now shows event size (KiB) in the publish step and warns if >60 KiB (some relays reject large events). Documented relay size limits and future work (compact binary format, target relay policy testing) in README "Not Yet Implemented" table |
|
||||
|
||||
## Changes by version
|
||||
|
||||
|
||||
Generated
+600
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "post_quantum_nostr",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.27",
|
||||
"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": {
|
||||
|
||||
@@ -217,6 +217,42 @@ describe('NIP-01 event id and verifyNostrEvent', () => {
|
||||
const wrongIdEvent = { ...event, id: '0'.repeat(64), sig: '0'.repeat(128) };
|
||||
assert.equal(m.verifyNostrEvent(wrongIdEvent), false, 'wrong id should fail verification');
|
||||
});
|
||||
|
||||
// G56-19: missing event id must be rejected (id is now required, not optional)
|
||||
test('G56-19: verifyNostrEvent rejects missing id', () => {
|
||||
const mnemonic = m.generateSeedPhrase();
|
||||
const seed = m.mnemonicToSeed(mnemonic);
|
||||
const kp = m.deriveSecp256k1FromSeed(seed);
|
||||
const pubkeyHex = m.bytesToHex(kp.publicKey);
|
||||
const event = {
|
||||
pubkey: pubkeyHex,
|
||||
created_at: 1700000000,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'test event for missing id check'
|
||||
};
|
||||
// No id field at all — must be rejected
|
||||
const noIdEvent = { ...event, sig: '0'.repeat(128) };
|
||||
assert.equal(m.verifyNostrEvent(noIdEvent), false, 'missing id should fail verification');
|
||||
});
|
||||
|
||||
// G56-19: malformed id (not 64 hex) must be rejected
|
||||
test('G56-19: verifyNostrEvent rejects malformed id', () => {
|
||||
const mnemonic = m.generateSeedPhrase();
|
||||
const seed = m.mnemonicToSeed(mnemonic);
|
||||
const kp = m.deriveSecp256k1FromSeed(seed);
|
||||
const pubkeyHex = m.bytesToHex(kp.publicKey);
|
||||
const event = {
|
||||
id: 'short',
|
||||
pubkey: pubkeyHex,
|
||||
created_at: 1700000000,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'test event for malformed id check',
|
||||
sig: '0'.repeat(128)
|
||||
};
|
||||
assert.equal(m.verifyNostrEvent(event), false, 'malformed id should fail verification');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -516,6 +516,7 @@
|
||||
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 9999 proof carrier event:</strong></div>
|
||||
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
|
||||
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
|
||||
<div class="pq-info-text" id="pqEventSizeWarning" style="display: none; font-size: 13px; margin-top: 8px;"></div>
|
||||
<div class="pq-button-row" style="margin-top: 10px;">
|
||||
<button class="pq-button pq-button-secondary" id="pqCopyProofCarrierBtn">Copy Proof Carrier Event</button>
|
||||
</div>
|
||||
|
||||
+110
-12
@@ -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.');
|
||||
@@ -560,6 +585,19 @@
|
||||
document.getElementById('pqKind1Preview').textContent = JSON.stringify(kind1Event, null, 2);
|
||||
pqEventIdDisplay.textContent = `Event ID: ${pqEvent.id}`;
|
||||
document.getElementById('pqSuccessEventPreview').textContent = eventJsonStr;
|
||||
// G56-20: Show event size and warn if it may exceed relay limits
|
||||
const eventBytes = new Blob([eventJsonStr]).size;
|
||||
const sizeKB = (eventBytes / 1024).toFixed(1);
|
||||
const sizeWarning = document.getElementById('pqEventSizeWarning');
|
||||
if (sizeWarning) {
|
||||
if (eventBytes > 60000) {
|
||||
sizeWarning.innerHTML = `<strong>⚠ Event size: ${sizeKB} KiB.</strong> Some relays may reject events larger than ~50–60 KiB. If publication fails, try adding relays with higher limits.`;
|
||||
sizeWarning.style.display = 'block';
|
||||
} else {
|
||||
sizeWarning.innerHTML = `Event size: ${sizeKB} KiB`;
|
||||
sizeWarning.style.display = 'block';
|
||||
}
|
||||
}
|
||||
setTimeout(() => { renderRelayList(); showView('pqPublishStep'); }, 1000);
|
||||
|
||||
} catch (error) {
|
||||
@@ -843,7 +881,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);
|
||||
@@ -905,6 +965,25 @@
|
||||
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
|
||||
try { await window.NOSTR_LOGIN_LITE.logout(); } catch (e) { console.warn('[post-quantum] NLL logout failed:', e); }
|
||||
}
|
||||
// G56-10: Zeroize secret typed arrays before nulling references.
|
||||
// Note: JavaScript garbage collection does not guarantee erasure —
|
||||
// this is a best-effort reduction of the exposure window. See README
|
||||
// for the full limitation discussion.
|
||||
function zeroize(arr) {
|
||||
if (arr instanceof Uint8Array) {
|
||||
arr.fill(0);
|
||||
}
|
||||
}
|
||||
if (pqSeed) zeroize(pqSeed);
|
||||
if (pqSecpKeys) {
|
||||
if (pqSecpKeys.privateKey) zeroize(pqSecpKeys.privateKey);
|
||||
if (pqSecpKeys.publicKey) zeroize(pqSecpKeys.publicKey);
|
||||
}
|
||||
if (pqKeys) {
|
||||
for (const alg of ['mlDsa44', 'mlDsa65', 'slhDsa', 'falcon512', 'mlKem']) {
|
||||
if (pqKeys[alg] && pqKeys[alg].secretKey) zeroize(pqKeys[alg].secretKey);
|
||||
}
|
||||
}
|
||||
// Clear local state
|
||||
currentPubkey = null;
|
||||
pqMnemonic = null;
|
||||
@@ -917,6 +996,17 @@
|
||||
pendingOtsBytes = null;
|
||||
currentBlockHeight = 0;
|
||||
pqRelays = DEFAULT_RELAYS.slice();
|
||||
|
||||
// G56-10: Clear DOM elements that displayed secret material
|
||||
const seedDisplay = document.getElementById('pqSeedDisplay');
|
||||
if (seedDisplay) seedDisplay.innerHTML = '';
|
||||
const seedInput = document.getElementById('pqSeedInput');
|
||||
if (seedInput) seedInput.value = '';
|
||||
const entropyHint = document.getElementById('pqEntropyHint');
|
||||
if (entropyHint) entropyHint.textContent = 'Optional: click here and type random characters, move your mouse to add entropy...';
|
||||
|
||||
// G56-10: Clear the clipboard (in case the user copied the seed phrase)
|
||||
try { navigator.clipboard.writeText(''); } catch (e) { /* clipboard may not be available */ }
|
||||
// Clear auth/session storage, then restore only the OTS workflow record.
|
||||
try {
|
||||
localStorage.clear();
|
||||
@@ -1007,7 +1097,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 +1138,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 +1172,9 @@
|
||||
pendingOtsBytes = result.proof;
|
||||
savePendingOts(pqEvent.id, pendingOtsBytes, {
|
||||
ownerPubkey: currentPubkey,
|
||||
targetKind: pqEvent.kind
|
||||
targetKind: pqEvent.kind,
|
||||
proofCarrierEvent: pqEvent,
|
||||
kind1Event
|
||||
});
|
||||
showOtsProof(pendingOtsBytes);
|
||||
|
||||
@@ -1166,7 +1262,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.`);
|
||||
|
||||
+57
-12
@@ -429,6 +429,8 @@ export function verifyNostrEvent(event) {
|
||||
if (typeof event.kind !== 'number') return false;
|
||||
if (!Array.isArray(event.tags)) return false;
|
||||
if (typeof event.content !== 'string') return false;
|
||||
// G56-19: Require exact NIP-01 shape — id must be present and 64 lowercase hex
|
||||
if (typeof event.id !== 'string' || !/^[0-9a-f]{64}$/.test(event.id)) return false;
|
||||
|
||||
// Build the event hash (NIP-01 serialization)
|
||||
const serialized = JSON.stringify([
|
||||
@@ -441,8 +443,8 @@ export function verifyNostrEvent(event) {
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex(hash);
|
||||
// F-H2: Verify event.id matches the computed hash
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
// F-H2 / G56-19: Verify event.id matches the computed hash (id is now required)
|
||||
if (event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes(event.sig);
|
||||
@@ -613,7 +615,7 @@ My current identity:
|
||||
npub: ${npub}
|
||||
hex: ${hexPubkey}
|
||||
|
||||
This attestation is established pre-quantum at Bitcoin block height ${blockHeight}.
|
||||
This attestation is established pre-quantum and anchored to the Bitcoin blockchain via OpenTimestamps.
|
||||
|
||||
Post-quantum public keys in tags:
|
||||
ML-DSA-44 (Dilithium, FIPS 204, NIST Level 2)
|
||||
@@ -635,9 +637,8 @@ Created at: https://laantungir.net/post-quantum/`;
|
||||
const slhDsaSig = signWithSLHDSA(statementBytes, pqKeys.slhDsa.secretKey);
|
||||
const falconSig = signWithFalcon(statementBytes, pqKeys.falcon512.secretKey);
|
||||
|
||||
// Tags: block height + each PQ key's pubkey and signature
|
||||
// G56-17: Removed unverified block_height tag — the OTS proof is the real anchor
|
||||
const tags = [
|
||||
['block_height', String(blockHeight)],
|
||||
['algorithm', 'ml-dsa-44', bytesToBase64(pqKeys.mlDsa44.publicKey), bytesToBase64(mlDsa44Sig)],
|
||||
['algorithm', 'ml-dsa-65', bytesToBase64(pqKeys.mlDsa65.publicKey), bytesToBase64(mlDsa65Sig)],
|
||||
['algorithm', 'slh-dsa-128s', bytesToBase64(pqKeys.slhDsa.publicKey), bytesToBase64(slhDsaSig)],
|
||||
@@ -1637,14 +1638,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 +1733,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 +1760,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 +1774,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 +1809,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 +2070,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 +2093,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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.24",
|
||||
"VERSION_NUMBER": "0.0.24",
|
||||
"BUILD_DATE": "2026-07-24T17:14:32.242Z"
|
||||
"VERSION": "v0.0.27",
|
||||
"VERSION_NUMBER": "0.0.27",
|
||||
"BUILD_DATE": "2026-07-25T11:36:25.146Z"
|
||||
}
|
||||
|
||||
+46
-11
@@ -9768,6 +9768,7 @@ function verifyNostrEvent(event) {
|
||||
if (typeof event.kind !== "number") return false;
|
||||
if (!Array.isArray(event.tags)) return false;
|
||||
if (typeof event.content !== "string") return false;
|
||||
if (typeof event.id !== "string" || !/^[0-9a-f]{64}$/.test(event.id)) return false;
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
@@ -9778,7 +9779,7 @@ function verifyNostrEvent(event) {
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex3(hash);
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
if (event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes3(event.sig);
|
||||
@@ -9880,7 +9881,7 @@ My current identity:
|
||||
npub: ${npub}
|
||||
hex: ${hexPubkey}
|
||||
|
||||
This attestation is established pre-quantum at Bitcoin block height ${blockHeight}.
|
||||
This attestation is established pre-quantum and anchored to the Bitcoin blockchain via OpenTimestamps.
|
||||
|
||||
Post-quantum public keys in tags:
|
||||
ML-DSA-44 (Dilithium, FIPS 204, NIST Level 2)
|
||||
@@ -9899,7 +9900,6 @@ Created at: https://laantungir.net/post-quantum/`;
|
||||
const slhDsaSig = signWithSLHDSA(statementBytes, pqKeys.slhDsa.secretKey);
|
||||
const falconSig = signWithFalcon(statementBytes, pqKeys.falcon512.secretKey);
|
||||
const tags = [
|
||||
["block_height", String(blockHeight)],
|
||||
["algorithm", "ml-dsa-44", bytesToBase64(pqKeys.mlDsa44.publicKey), bytesToBase64(mlDsa44Sig)],
|
||||
["algorithm", "ml-dsa-65", bytesToBase64(pqKeys.mlDsa65.publicKey), bytesToBase64(mlDsa65Sig)],
|
||||
["algorithm", "slh-dsa-128s", bytesToBase64(pqKeys.slhDsa.publicKey), bytesToBase64(slhDsaSig)],
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user