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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = `<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) {
|
||||
|
||||
@@ -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)],
|
||||
|
||||
+3
-3
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)],
|
||||
|
||||
Reference in New Issue
Block a user