Compare commits

...
1 Commits
5 changed files with 52 additions and 7 deletions
+15
View File
@@ -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
+3 -3
View File
@@ -10,9 +10,9 @@
|---|---:|---:|---:|---:|
| Critical | 2 | 2 | 0 | 0 |
| High | 6 | 6 | 0 | 0 |
| Medium | 8 | 6 | 0 | 1 |
| Medium | 8 | 7 | 0 | 0 |
| Low / Info | 4 | 0 | 0 | 3 |
| **Total** | **20** | **14** | **0** | **4** |
| **Total** | **20** | **15** | **0** | **3** |
## Findings status
@@ -27,7 +27,7 @@
| 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-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 |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "post_quantum_nostr",
"version": "0.0.25",
"version": "0.0.26",
"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": {
+30
View File
@@ -952,6 +952,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;
@@ -964,6 +983,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();
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.0.25",
"VERSION_NUMBER": "0.0.25",
"BUILD_DATE": "2026-07-24T18:43:29.373Z"
"VERSION": "v0.0.26",
"VERSION_NUMBER": "0.0.26",
"BUILD_DATE": "2026-07-25T11:30:14.286Z"
}