From cc66a5fa3eac52365bd7d8f2e1623a51b7dad673 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sat, 25 Jul 2026 07:36:25 -0400 Subject: [PATCH] G56-17/19/20: remove unverified block_height tag from signed content, require 64-hex event id in verifyNostrEvent, show event size + relay limit warning in publish step, document relay limits in README --- README.md | 1 + audits/GPT5.6/remediation_progress.md | 10 ++++---- package.json | 2 +- test/pq-crypto.test.mjs | 36 +++++++++++++++++++++++++++ www/index.html | 1 + www/js/index-app.mjs | 13 ++++++++++ www/js/pq-crypto.mjs | 11 ++++---- www/js/version.json | 6 ++--- www/pq-crypto.bundle.js | 6 ++--- 9 files changed, 69 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1f3e964..df9c11a 100644 --- a/README.md +++ b/README.md @@ -829,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 diff --git a/audits/GPT5.6/remediation_progress.md b/audits/GPT5.6/remediation_progress.md index 40a7a94..e73b6dc 100644 --- a/audits/GPT5.6/remediation_progress.md +++ b/audits/GPT5.6/remediation_progress.md @@ -11,8 +11,8 @@ | Critical | 2 | 2 | 0 | 0 | | High | 6 | 6 | 0 | 0 | | Medium | 8 | 7 | 0 | 0 | -| Low / Info | 4 | 0 | 0 | 3 | -| **Total** | **20** | **15** | **0** | **3** | +| Low / Info | 4 | 3 | 0 | 0 | +| **Total** | **20** | **18** | **0** | **0** | ## Findings status @@ -34,10 +34,10 @@ | 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-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 | ❌ 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-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 diff --git a/package.json b/package.json index a37648a..604dcff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "post_quantum_nostr", - "version": "0.0.26", + "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": { diff --git a/test/pq-crypto.test.mjs b/test/pq-crypto.test.mjs index 9180e59..1a698ba 100644 --- a/test/pq-crypto.test.mjs +++ b/test/pq-crypto.test.mjs @@ -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'); + }); }); // ============================================================================ diff --git a/www/index.html b/www/index.html index caffcf9..77c8fc3 100644 --- a/www/index.html +++ b/www/index.html @@ -516,6 +516,7 @@
Kind 9999 proof carrier event:
+
diff --git a/www/js/index-app.mjs b/www/js/index-app.mjs index 468bd6a..560e453 100644 --- a/www/js/index-app.mjs +++ b/www/js/index-app.mjs @@ -585,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 = `⚠ Event size: ${sizeKB} KiB. 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) { diff --git a/www/js/pq-crypto.mjs b/www/js/pq-crypto.mjs index 6f488d1..f5eac5b 100644 --- a/www/js/pq-crypto.mjs +++ b/www/js/pq-crypto.mjs @@ -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)], diff --git a/www/js/version.json b/www/js/version.json index 778fed2..5133d4e 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.0.26", - "VERSION_NUMBER": "0.0.26", - "BUILD_DATE": "2026-07-25T11:30:14.286Z" + "VERSION": "v0.0.27", + "VERSION_NUMBER": "0.0.27", + "BUILD_DATE": "2026-07-25T11:36:25.146Z" } diff --git a/www/pq-crypto.bundle.js b/www/pq-crypto.bundle.js index 1b2e844..c71d21e 100644 --- a/www/pq-crypto.bundle.js +++ b/www/pq-crypto.bundle.js @@ -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)],