Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc36c37e7 |
@@ -111,6 +111,24 @@ The strategy uses existing Nostr infrastructure (NIP-06, NIP-03) plus one new ev
|
||||
|
||||
The BIP39 seed is a 64-byte entropy string derived from a mnemonic via PBKDF2-HMAC-SHA512 (2048 iterations). This is a symmetric KDF — no public-key cryptography involved. A quantum computer provides no advantage beyond Grover's quadratic speedup.
|
||||
|
||||
> **⚠️ Continuity is asserted, not proven (G56-06).** The old Nostr identity *authorizes* the PQ
|
||||
> keys by signing an event that contains them. This is an authorization, not a cryptographic proof
|
||||
> that the PQ keys were derived from the seed phrase shown to the user. A compromised browser could
|
||||
> display one mnemonic while publishing attacker-controlled PQ keys — and any in-browser signature
|
||||
> (secp or PQ) could be faked by the same compromised browser. No in-browser cryptographic trick
|
||||
> can prove the seed and the published keys are bound, because the browser is the potential adversary.
|
||||
>
|
||||
> **The only real proof is out-of-band verification:** after the migration, the user should take
|
||||
> their seed phrase to a *different* device or browser, derive the keys independently, and confirm
|
||||
> the public keys match the published event. This is the one step a compromised browser cannot fake.
|
||||
>
|
||||
> **Why not verify the mnemonic against the current identity?** Deriving the NIP-06 secp key from
|
||||
> an entered mnemonic and requiring it to match the signed-in pubkey would provide a strong
|
||||
> continuity proof. However, no current Nostr signers (NIP-07 extensions) accept seed phrases, so
|
||||
> this would require the user to type their valuable existing mnemonic into the web page — an
|
||||
> unacceptable security risk. This mode is deferred until hardware PQ signers or seed-accepting
|
||||
> signers exist.
|
||||
|
||||
**The seed is algorithm-agnostic.** It's just entropy. You can derive any key type from it:
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
| Severity | Total | Fixed | In Progress | Not Started |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Critical | 2 | 2 | 0 | 0 |
|
||||
| High | 6 | 4 | 0 | 2 |
|
||||
| High | 6 | 5 | 0 | 1 |
|
||||
| Medium | 8 | 2 | 0 | 6 |
|
||||
| Low / Info | 4 | 0 | 0 | 4 |
|
||||
| **Total** | **20** | **8** | **0** | **12** |
|
||||
| **Total** | **20** | **9** | **0** | **11** |
|
||||
|
||||
## Findings status
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| G56-03 | High | Outer wrapper identity not bound to embedded identity | ✅ Fixed | v0.0.14 | Added `outerPubkey` and `expectedAuthor` options to `verifyNIPQRContent()`, `identityBound` field in result, `validForMigration` requires identity binding, 4 identity binding tests |
|
||||
| G56-04 | High | Bitcoin verification trusts public API JSON, doesn't validate headers/PoW | ✅ Fixed | v0.0.21 | Tier 1+2: rewrote `fetchBitcoinBlockHeader()` to query ALL providers and cross-check merkle roots (fail on disagreement); added `trustMode` field to `verifyOtsProof()` result (`multi-explorer-checked` / `single-explorer-checked` / `structural-only`); updated verify.html + index.html UI to display trust mode label; removed misleading "lite-client verification" / "independently verifiable" comments. Full light-client PoW verification (Tier 3) remains as future work. 2 test assertions added (trustMode present on verified proof, structural-only on pending) |
|
||||
| G56-05 | High | Signer output reconstructed without validating returned ID/sig/pubkey/fields | ✅ Fixed | v0.0.20 | Added `validateSignerOutput()` helper: checks all required fields present + typed, verifies no template field (content/tags/created_at/kind) changed, verifies pubkey equals expected identity, recomputes + verifies event ID, verifies Schnorr signature, rejects extra fields. Replaced reconstruction at all 4 `signEvent()` call sites (2 in index.html, 2 in verify.html) with validation + retain-exact-signed-object. 15 new tests (mutated content/tags/kind/created_at, wrong pubkey, wrong ID, invalid sig, extra field, missing field, non-object, malformed id/sig, tag element changed) |
|
||||
| G56-06 | High | New seed continuity asserted, not proven — no successor signature, no identity match | ❌ Not started | — | Protocol design decision needed |
|
||||
| G56-06 | High | New seed continuity asserted, not proven — no successor signature, no identity match | ✅ Fixed (design decision: assert-only) | v0.0.22 | Design decision: continuity is *asserted* by the old identity's signature, not *proven* by cryptography. No in-browser cryptographic trick can prove the seed and published PQ keys are bound, because a compromised browser controls both the seed display and all signatures. Mode 1 (mnemonic-match) rejected: no current Nostr signers accept seed phrases, so it would require typing a valuable mnemonic into a web page — unacceptable risk. Mode 2 (successor secp signature) rejected: a malicious browser can fake the secp signature just as easily as PQ signatures — adds no real binding. Fix: documented the honest position in README.md + nip_proposal.md (continuity is asserted not proven; only out-of-band verification on a different device can prove the seed matches the published keys); added UI recommendation in index.html seed step to verify out-of-band after migration |
|
||||
| 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 | ❌ Not started | — | Needs CSP hardening, header fixes, asset pinning |
|
||||
| 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 |
|
||||
@@ -123,6 +123,14 @@
|
||||
- Full light-client PoW/difficulty/chain-linkage verification (Tier 3) remains as future work
|
||||
- 2 new test assertions: trustMode present + explorer-checked on verified proof, structural-only on pending proof
|
||||
|
||||
### v0.0.22 — G56-06 fix (seed continuity: honest claims + out-of-band verification)
|
||||
- Design decision: continuity is *asserted* by the old identity's signature, not *proven* by cryptography
|
||||
- Mode 1 (mnemonic-match) rejected: no current Nostr signers accept seed phrases; would require typing a valuable mnemonic into a web page — unacceptable risk
|
||||
- Mode 2 (successor secp signature) rejected: a malicious browser can fake the secp signature just as easily as PQ signatures — adds no real binding
|
||||
- Documented the honest position in README.md (Component 1 section) and nip_proposal.md: continuity is asserted not proven; only out-of-band verification on a different device can prove the seed matches the published keys
|
||||
- Added UI recommendation in index.html seed step: "Verify out-of-band after migration — take your seed phrase to a different device, derive the keys, confirm the public keys match"
|
||||
- No new tests (documentation + UI only)
|
||||
|
||||
## Test count
|
||||
|
||||
- **Before remediation:** 38 tests
|
||||
@@ -133,4 +141,5 @@
|
||||
- **After G56-07:** 88 tests
|
||||
- **After G56-05:** 103 tests
|
||||
- **After G56-04:** 103 tests (2 assertions added to existing tests)
|
||||
- **After G56-06:** 103 tests (docs + UI only)
|
||||
- **All passing:** ✅
|
||||
|
||||
@@ -39,6 +39,17 @@ A user with an existing Nostr identity (the **attesting identity**) publishes tw
|
||||
|
||||
The PQ keys are derived deterministically from a BIP39 seed phrase via BIP32, at fixed child indices under the NIP-06 base path. This means a user who has backed up their seed phrase can always recover the same PQ keys, on any conforming implementation.
|
||||
|
||||
> **⚠️ Continuity is asserted, not proven.** The old identity *authorizes* the PQ keys by signing an
|
||||
> event that contains them. This is an authorization, not a cryptographic proof that the PQ keys were
|
||||
> derived from the seed phrase shown to the user. A compromised browser could display one mnemonic
|
||||
> while publishing attacker-controlled PQ keys, and any in-browser signature could be faked by the
|
||||
> same browser. The only real proof is **out-of-band verification**: the user takes their seed phrase
|
||||
> to a different device, derives the keys independently, and confirms the public keys match the
|
||||
> published event. Verifying the mnemonic against the current identity (deriving the NIP-06 secp key
|
||||
> and requiring it to match) would provide a stronger proof, but no current Nostr signers accept seed
|
||||
> phrases, so this would require typing a valuable mnemonic into a web page — an unacceptable risk.
|
||||
> This mode is deferred until hardware PQ signers or seed-accepting signers exist.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SEED[BIP39 seed - recovery root] --> SECP[secp256k1 keypair - child 0]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "post_quantum_nostr",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.22",
|
||||
"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": {
|
||||
|
||||
@@ -395,6 +395,13 @@
|
||||
<label><input type="radio" name="pqWordCount" value="128" id="pqWordCount12"> 12 words (testing only)</label>
|
||||
</div>
|
||||
<div class="pq-seed-display" id="pqSeedDisplay"></div>
|
||||
<div class="pq-info-text" style="margin-top: 12px; font-size: 13px; color: var(--accent-color);">
|
||||
<strong>Verify out-of-band after migration:</strong> This seed phrase is your backup — but
|
||||
this browser cannot cryptographically prove the published PQ keys came from it. After completing
|
||||
the migration, take your seed phrase to a <em>different</em> device or browser, derive the keys
|
||||
independently, and confirm the public keys match the published event. This is the only way to
|
||||
catch a compromised browser that might have substituted attacker keys.
|
||||
</div>
|
||||
<div class="pq-info-text" style="margin-top: 15px; font-size: 13px;">
|
||||
<strong>Optional:</strong> Add extra entropy by moving your mouse and typing in the box below.
|
||||
This mixes your input with the browser's random number generator, making the seed unpredictable
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.21",
|
||||
"VERSION_NUMBER": "0.0.21",
|
||||
"BUILD_DATE": "2026-07-24T14:15:01.028Z"
|
||||
"VERSION": "v0.0.22",
|
||||
"VERSION_NUMBER": "0.0.22",
|
||||
"BUILD_DATE": "2026-07-24T14:30:29.001Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user