Files
2026-07-20 20:17:22 -04:00

672 lines
50 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GPT-5.6 Comprehensive Security and Usability Audit
**Audit date:** 2026-07-20
**Audited workspace:** `post_quantum_nostr` at root commit `9a975fcbc56e455a68ae16d734feeed76ba304b6`
**Application version:** `0.0.11`
**Auditor:** GPT-5.6
**Scope:** first-party application code, cryptographic and Nostr/OTS protocol design, browser security, dependencies, tests, documentation claims, live public deployment, and the OpenTimestamps submodule boundary.
## Executive verdict
**Do not use this implementation to protect a real Nostr identity yet.** It is a useful research prototype with working PQ key generation/signature primitives and a promising OTS proof parser, but its verifier currently accepts events with **no PQ signatures**, treats missing signatures on declared signature algorithms as successful KEM-style entries, does not bind the outer wrapper identity to the embedded announcement identity, and does not implement its defining **earliest valid OTS anchor wins** selection rule. These are protocol-critical failures, not polish issues.
**Conceptual verdict:** the project is **conditionally sound as a pre-quantum key-commitment and identity-link protocol**, but it is **not, by itself, a complete post-quantum migration for Nostr**. Its central idea—have the current identity authorize PQ keys while secp256k1 is still trustworthy, anchor that authorization in Bitcoin, and later prefer the earliest valid anchor—is defensible if the protocol is redesigned to enforce immutable discovery, strict verification, key possession/continuity, and PQ-authorized rotation. Fixing only the implementation bugs would not be enough. Fixing the architectural requirements in the “Conceptual soundness assessment” below could yield a valid and useful project, but the result should initially be described as a **PQ key pre-commitment/link registry**, not as making the existing Nostr identity fully quantum-resistant.
The project did well in several areas:
- All 38 existing tests passed, including two live checks against Bitcoin API data.
- A fresh browser bundle was byte-for-byte identical to the committed bundle.
- `npm audit --omit=dev` reported no advisories for the currently installed root dependency graph.
- The OTS target digest is bound to the expected event hash during full verification.
- The live bundle exactly matched the audited local bundle.
- A live calendar submission succeeded to all five configured calendars and parsed as a detached multi-calendar proof.
Those strengths do not compensate for fail-open acceptance in the key-link verifier. A user-facing statement such as “All signatures verified successfully” can currently be produced for a signed wrapper whose embedded announcement has zero PQ keys. The application also presents a latest replaceable event from relays, whereas the proposal says the canonical link is the event with the earliest verified Bitcoin anchor. That mismatch defeats the core post-quantum migration rule.
## Severity summary
| Severity | Count | IDs |
|---|---:|---|
| Critical | 2 | G56-01, G56-02 |
| High | 6 | G56-03 through G56-08 |
| Medium | 8 | G56-09 through G56-16 |
| Low / informational | 4 | G56-17 through G56-20 |
## Findings at a glance
| ID | Severity | Finding | Confidence |
|---|---|---|---|
| G56-01 | Critical | Verifier accepts a migration with no PQ signatures and accepts missing signature fields as valid | Confirmed dynamically |
| G56-02 | Critical | Earliest-valid-anchor rule is not implemented; relay discovery selects the latest replaceable event | Confirmed |
| G56-03 | High | Outer wrapper identity is not bound to the embedded announcement identity | Confirmed dynamically |
| G56-04 | High | OTS “verification” trusts public HTTPS APIs and does not validate Bitcoin headers or proof of work | Confirmed |
| G56-05 | High | Signer output is reconstructed without validating returned identity, event ID, signature, or exact signed fields | Confirmed |
| G56-06 | High | The system creates a new seed for any signed-in identity without proving seed continuity or a successor key relationship | Confirmed design limitation |
| G56-07 | High | Verification is not robust against malformed untrusted relay/paste input and can throw | Confirmed dynamically |
| G56-08 | High | Browser origin is the key-generation trust boundary, but deploy hardening and signer assets are unaudited/unpinned | Confirmed |
| G56-09 | Medium | Twelve-word seed is the only generated option and its warning is removed in the audited workspace/live page | Confirmed |
| G56-10 | Medium | Secret material is retained in ordinary JavaScript objects/DOM/clipboard and is not zeroized | Confirmed |
| G56-11 | Medium | Publish workflow reports success and advances even when no relay accepts one or both events | Confirmed |
| G56-12 | Medium | Resume/upgrade state can fetch an attacker-controlled latest wrapper and lacks robust event/workflow binding | Confirmed |
| G56-13 | Medium | OTS parser lacks total input/operation budgets and uses unsafe 32-bit varuint arithmetic | Confirmed |
| G56-14 | Medium | Dependency resolution is not reproducible from the repository; lockfile is ignored and inconsistent | Confirmed |
| G56-15 | Medium | OpenTimestamps submodule is not clonable normally because `.gitmodules` is absent; historical dependency risk is unresolved | Confirmed |
| G56-16 | Medium | Documentation and UI overclaim implementation status and security properties | Confirmed |
| G56-17 | Low | Block-height statement is not validated and can be zero/untrusted | Confirmed |
| G56-18 | Low | Relay URL policy permits insecure `ws://` and arbitrary endpoints | Confirmed |
| G56-19 | Low | Missing event IDs may be accepted by the library verifier | Confirmed dynamically |
| G56-20 | Informational | Large events may be rejected by relays; interoperability testing is absent | Confirmed size; relay behavior contextual |
---
## Conceptual soundness assessment
### Direct answer
**Is the idea flawed from the start? No—but its valid scope is narrower than the current product language suggests.**
The project starts from a real migration problem: after secp256k1 is broken, a fresh secp signature cannot distinguish the legitimate owner from an attacker. Creating and timestamping an authorization **before** that break is a legitimate way to establish historical precedence. A PQ key that signs the same canonical authorization provides durable proof that the corresponding PQ private key participated. Bitcoin/OpenTimestamps can provide an externally ordered existence proof without changing Nostr relays.
That gives a conceptually useful primitive:
> Before quantum compromise, identity A authorized PQ key set K; the authorization existed no later than Bitcoin block H; future clients can use a deterministic conflict rule to resolve later forged claims.
However, that primitive does **not** automatically make identity A quantum-resistant. A complete migration also needs a standardized way to authenticate future events with K, rotate/revoke keys after secp256k1 is distrusted, encrypt to PQ keys, discover all historical claims despite relay replacement/deletion, and recover from lost or compromised PQ keys. Those pieces are currently outside the implementation.
### What remains valid after remediation
If all audit recommendations and the architectural requirements in [`audit_mitigation.md`](audit_mitigation.md:1) are implemented, the project can validly provide:
1. **Pre-quantum commitment:** proof that a Nostr identity authorized a canonical PQ key set before a verified Bitcoin block.
2. **PQ proof of possession:** proof that each linked signature key participated in the canonical authorization, and an explicit KEM possession mechanism if ML-KEM ownership is claimed.
3. **Deterministic conflict resolution:** selection of the earliest valid immutable anchor rather than the newest relay event.
4. **A migration root:** a trustworthy starting key set for a separate PQ-event, PQ-encryption, and key-rotation protocol.
5. **Backward-compatible publication:** legacy relays can carry opaque events without understanding PQ cryptography, subject to size and retention limits.
### What remediation cannot make true without additional protocols
Even a perfectly implemented key-link protocol does not itself provide:
- PQ authentication for ordinary kind 0/1/3/etc. events;
- PQ confidentiality for NIP-04 or NIP-44 messages;
- automatic preservation of the social graph in clients that do not adopt identity-alias/successor semantics;
- revocation or rotation after secp256k1 becomes untrustworthy;
- availability if all relays and user archives lose the immutable announcement/proof;
- protection from seed theft, malicious signers, compromised web origins, or broken PQ implementations;
- consensus across Nostr clients about algorithm policy or canonical identity succession.
The project therefore needs companion specifications for PQ-signed events, key lifecycle, encryption, and client identity mapping before it can accurately claim a complete Nostr PQ migration.
### Architectural conditions for a sound protocol
The concept is sound only if all of these invariants are made normative and implemented:
1. **Canonical signed object:** PQ signatures and the current secp identity must cover one domain-separated, versioned, deterministic structure containing the old identity, complete ordered key set, algorithm identifiers, protocol version, creation context, and rotation policy. Human-readable prose may be displayed but must not be the normative signed representation.
2. **Strict proof policy:** a verifier must distinguish structural validity, cryptographic validity, algorithm-policy sufficiency, OTS validity, and canonical-selection status. Missing signatures can never become successful KEM entries.
3. **Identity equality:** queried author, wrapper author, embedded author, and canonical old-identity field must match.
4. **Immutable candidates:** the initial authorization must not depend on a replaceable event that can hide prior history. An upgraded proof may reference the same immutable authorization digest, but cannot replace the candidate itself.
5. **Complete discovery:** clients must collect candidates across relays/archives and apply a deterministic earliest-valid-anchor rule, including tie-breaks and confirmation-depth policy.
6. **Possession and recoverability:** every signature key must prove possession. If the project promises seed recovery, the canonical statement must bind a seed-derived successor proof or the UI must explicitly avoid claiming common seed origin. KEM possession requires a defined challenge/decapsulation proof or must be described only as an authorized encryption key, not proven possession.
7. **Post-break authority:** after the transition cutoff, secp-only updates must not supersede the canonical PQ root. Rotation/revocation must require a threshold or policy over already-authorized PQ keys.
8. **Future event semantics:** a separate specification must define exactly how PQ keys authenticate future Nostr events and how those events bind to NIP-01 IDs, kinds, tags, replay domains, and old identities.
9. **Availability:** users and clients must export, mirror, and validate the immutable authorization plus OTS proof independently of any one relay or web service.
10. **Trust-explicit Bitcoin verification:** implementations must state whether they use a local node, a validated header chain, or trusted explorer APIs. Security labels must match that model.
11. **Algorithm agility with policy:** multiple algorithms may hedge cryptanalytic risk, but clients need mandatory minimum sets, deprecation rules, downgrade resistance, and deterministic handling of unknown schemes.
12. **Secure key boundary:** production derivation/signing should occur in a dedicated signer or hardware boundary, not an updateable general-purpose web origin holding a long-lived seed.
### Bottom line
With those conditions and the finding-specific fixes, this can become a valid **PQ migration foundation**. Without them, repairing individual code defects would leave a system that generates valid PQ signatures but lacks a trustworthy rule for what those signatures mean over time. The current project is therefore **salvageable and conceptually promising, but architecturally incomplete**.
## Scope, baseline, and trust boundaries
### First-party runtime surface
- Cryptographic module: [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1)
- Generated browser bundle: [`pq-crypto.bundle.js`](../../www/pq-crypto.bundle.js)
- Migration page and state machine: [`index.html`](../../www/index.html:1)
- Verification page and relay query flow: [`verify.html`](../../www/verify.html:1)
- Tests: [`pq-crypto.test.mjs`](../../test/pq-crypto.test.mjs:1)
- Build script: [`build-pq-bundle.js`](../../build-pq-bundle.js:1)
- Protocol proposal: [`nip_proposal.md`](../../nip_proposal.md:1)
- User-facing claims: [`README.md`](../../README.md:1)
### External trust boundaries
1. **Web origin / deployment operator** controls the JavaScript that receives or generates a BIP39 mnemonic and PQ private keys.
2. **nostr-login-lite / NIP-07/NIP-46 signer** supplies the current identity and signs events.
3. **Nostr relays** receive, retain, filter, and return untrusted events.
4. **OTS calendars** receive event digests and return pending timestamp fragments.
5. **Same-origin `/ots-upgrade` helper** receives OTS proof bytes and returns upgraded bytes; its implementation is absent from the repository.
6. **Blockstream and mempool.space APIs** supply the purported Bitcoin block hash, Merkle root, and timestamp.
7. **npm dependencies and committed generated bundle** implement all cryptographic primitives in the browser.
8. **OpenTimestamps submodule** supplies example proofs used by tests, but is not runtime-integrated.
### Audit limitations
- No browser automation framework or complete signer fixture is present, so the real NIP-07/NIP-46 modal flow was reviewed statically rather than driven end to end.
- The `/ots-upgrade` server source and nginx/deployment configuration are not in the repository. Only black-box HTTP behavior could be checked.
- No real Nostr identity was used and no migration event was published during the audit.
- The workspace was already dirty: user changes existed in [`index.html`](../../www/index.html:1), [`verify.html`](../../www/verify.html:1), and [`nip_proposal.md`](../../nip_proposal.md:1), plus unrelated deleted audit files. The report evaluates the working tree, not a pristine commit.
- Root advisory results describe the installed graph at audit time, not every graph permitted by semver ranges.
---
## Detailed findings
### G56-01 — Verifier accepts no PQ signatures and missing signatures as successful
**Severity:** Critical
**Confidence:** Confirmed dynamically
The central verifier initializes results with only secp256k1, `e`, and SHA-256 checks, then loops over whatever `algorithm` tags happen to exist. It never requires any algorithm tag, never requires the four expected signature algorithms, and never validates uniqueness or policy. See [`verifyNIPQRContent()`](../../www/js/pq-crypto.mjs:606) and its unconditional aggregate at [`results.every()`](../../www/js/pq-crypto.mjs:706).
Worse, any algorithm tag lacking a fourth field is marked valid as a KEM, regardless of its algorithm identifier:
```js
if (algorithm === 'ml-kem-768' || !sigBase64) {
results.push({ algorithm, valid: true, note: 'KEM (no signature to verify)' });
continue;
}
```
This logic is at [`verifyNIPQRContent()`](../../www/js/pq-crypto.mjs:683).
Dynamic probes established:
- A validly secp-signed embedded kind 1 event with `tags: []` returned `valid: true`.
- `ml-dsa-44`, `ml-dsa-65`, `slh-dsa-128s`, and `falcon-512` tags containing only a public-key field each returned `valid: true` with note `KEM (no signature to verify)`.
- An arbitrary unknown algorithm with no signature also returned valid.
The UI then presents `All signatures verified successfully!` when this incomplete result is true at [`verify.html`](../../www/verify.html:710).
**Impact:** The verifier can bless an event that provides no post-quantum authentication at all. Such an event does not establish the advertised migration and cannot support post-quantum continuity.
**Remediation:** Define and enforce a verification policy. At minimum:
1. Require exactly one tag for each mandatory supported algorithm.
2. Require exact field counts and decoded byte lengths.
3. Permit no-signature handling only for the exact `ml-kem-768` identifier.
4. Reject duplicate known algorithms.
5. Treat unknown algorithms as ignored—not valid evidence—and report them separately.
6. Return distinct structural validity, cryptographic validity, policy sufficiency, and OTS validity fields.
7. Add adversarial tests for empty tags, missing signatures, duplicate algorithms, unknown algorithms, and malformed encodings.
**Verification criterion:** No event can receive a migration-valid result unless the configured required PQ signature set verifies successfully.
### G56-02 — The defining earliest-valid-anchor rule is not implemented
**Severity:** Critical
**Confidence:** Confirmed
The proposal makes earliest verified Bitcoin anchor selection the mechanism that defeats a later quantum forgery at [`nip_proposal.md`](../../nip_proposal.md:204). The implementation instead queries kind 11112 with `limit: 1` and describes it as “latest” at [`verify.html`](../../www/verify.html:487). Across relays, it chooses the largest `created_at` value at [`verify.html`](../../www/verify.html:660). The resume path also requests one event at [`index.html`](../../www/index.html:1201) and selects newest `created_at` across relays at [`index.html`](../../www/index.html:758).
Kind 11112 is replaceable, as documented in [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:425). A later forged or accidental replacement can therefore hide the historical event from ordinary relay queries. Even before a quantum break, a compromised current key can publish a newer wrapper. After a break, the attacker can do so trivially. The application does not gather all candidate announcements/proofs, verify them, compare Bitcoin heights, or preserve a stable immutable discovery record.
**Impact:** The implemented discovery behavior can select the exact later event that the OTS precedence design says must lose. This breaks the central migration security argument.
**Remediation:** Redesign discovery and storage before production use:
- Use a non-replaceable canonical announcement or an append-only indexed form for immutable candidates.
- Fetch all candidates from multiple relays, not `limit: 1`.
- Fully validate each candidate and select the earliest valid Bitcoin anchor according to a precise tie-break rule.
- Define how upgraded OTS proofs refer to an immutable announcement without replacing or obscuring history.
- Cache/export the canonical proof so relay retention does not determine identity continuity.
- Implement PQ-authorized rotation separately.
**Verification criterion:** Given multiple candidates—including a later forged secp-valid candidate—the client deterministically chooses the candidate with the earliest valid Bitcoin anchor.
### G56-03 — Outer wrapper identity is not bound to embedded identity
**Severity:** High
**Confidence:** Confirmed dynamically
[`verifyNIPQRContent()`](../../www/js/pq-crypto.mjs:606) validates the embedded kind 1 event and tags but receives no outer event object and therefore cannot require `outer.pubkey === embedded.pubkey`. [`verifyEvent()`](../../www/verify.html:530) separately validates the outer signature and calls the content verifier without performing this identity equality check.
A dynamic probe built a valid embedded event signed by key A, wrapped it in a valid kind 11112 event signed by key B, and observed:
- outer signature valid: `true`
- embedded verification valid: `true`
**Impact:** A wrapper authored by one identity can carry another identity’s announcement and appear as a collection of passing checks. Relay query by author B can therefore display A’s embedded link under B’s result page. OTS upgrading/republishing may further re-sign mismatched content.
**Remediation:** Require equality among:
- requested relay-query author,
- outer wrapper `pubkey`,
- embedded kind 1 `pubkey`,
- identity named in the human-readable content (or preferably a canonical structured identity field).
Also require correct outer kind and exact tag cardinality.
### G56-04 — Bitcoin verification trusts public API JSON and does not validate block headers or proof of work
**Severity:** High
**Confidence:** Confirmed
[`fetchBitcoinBlockHeader()`](../../www/js/pq-crypto.mjs:1235) fetches a block hash by height and then a block JSON object from one of two public APIs. It extracts only a Merkle root and timestamp at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1244). [`verifyOtsProof()`](../../www/js/pq-crypto.mjs:1323) compares the OTS commitment to that API-provided Merkle root.
Despite comments calling this “independently verifiable against the Bitcoin PoW chain” at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1226), the implementation does not fetch raw 80-byte headers, hash a header, check its hash against target, verify chain linkage, validate accumulated work, or pin checkpoints. Both providers are contacted over HTTPS and are simply trusted to report honest data.
**Impact:** Compromise, interception at a trusted origin, coordinated false responses, or API semantic changes can cause a false “Bitcoin verified” result. Two fallback URLs are resilience, not trust minimization, because acceptance requires only one response.
**Remediation:** Accurately label the current mode as “verified against a trusted block explorer.” For stronger verification, use a local Bitcoin node or a real light client with headers, chainwork, checkpoints, and consensus validation. At minimum compare multiple independent providers and fail on disagreement, but do not call that trustless cryptographic Bitcoin verification.
### G56-05 — Signer output is trusted and reconstructed without validation
**Severity:** High
**Confidence:** Confirmed
After `window.nostr.signEvent`, the migration flow copies `id`, `pubkey`, and `sig` from the signer but takes `created_at`, `kind`, `content`, and `tags` from the local template at [`index.html`](../../www/index.html:1078). It does not check:
- signer-returned pubkey equals the authenticated `currentPubkey`,
- returned ID equals [`computeEventId()`](../../www/js/pq-crypto.mjs:510) for the final reconstructed event,
- returned signature verifies,
- signer did not alter fields,
- the wrapper signer matches the announcement signer.
The same trust is repeated for the wrapper at [`index.html`](../../www/index.html:1118) and upgraded events at [`index.html`](../../www/index.html:1713). The verify page republishes signer output directly at [`verify.html`](../../www/verify.html:784), but also does not validate it before publishing.
**Impact:** A buggy, malicious, or incompatible signer can cause invalid or wrong-identity events to be displayed and published as successful. Reconstructing a hybrid object can pair a signer’s signature with different fields.
**Remediation:** Treat signer output as untrusted. Validate complete returned events, exact field equality against approved templates, pubkey equality, event ID, and Schnorr signature before any display, persistence, or publish.
### G56-06 — New seed continuity is asserted, not proven
**Severity:** High
**Confidence:** Confirmed design limitation
The app generates or accepts an arbitrary seed after signing into an existing identity. It derives a seed-based secp key at [`deriveSecp256k1FromSeed()`](../../www/js/pq-crypto.mjs:198) but does not use that key to sign, and does not verify that an entered seed derives the currently signed-in Nostr identity. The documentation acknowledges no successor signature at [`README.md`](../../README.md:298).
For a raw-nsec identity, an old-identity signature plus pre-quantum OTS can establish that the old identity authorized listed PQ keys. It cannot prove those keys are recoverable from the mnemonic shown to the user or that all listed keys share one seed. For a mnemonic-based existing identity, failing to compare the derived secp public key wastes a strong continuity check.
**Impact:** A compromised browser can show one mnemonic while publishing attacker-chosen PQ keys; the old signer approves only a very large event whose key material is hard for a human to inspect. The user may believe the written mnemonic is a recoverable root when it is not cryptographically bound as such.
**Remediation:** Separate two modes:
1. **Existing mnemonic identity:** derive the NIP-06 secp key and require it to equal the signer pubkey before proceeding.
2. **Raw-nsec migration:** define a canonical structured migration statement and add a proof-of-possession/successor signature from a seed-derived key, plus recovery self-test before publishing.
A hardware signer or dedicated PQ signer should ideally derive/sign without exposing the mnemonic to a web origin.
### G56-07 — Untrusted verifier input can throw instead of failing closed
**Severity:** High
**Confidence:** Confirmed dynamically
The verification page accepts pasted JSON and relay events. [`verifyNIPQRContent()`](../../www/js/pq-crypto.mjs:606) assumes arrays and valid base64. Examples confirmed during testing:
- malformed base64 threw `InvalidCharacterError`,
- non-array wrapper tags threw `TypeError: kind11112Tags.find is not a function`,
- missing signatures caused [`verifyNostrEvent()`](../../www/js/pq-crypto.mjs:400) to throw,
- `null` or wrong-shaped events threw.
Relay-driven verification at [`verify.html`](../../www/verify.html:679) invokes the verifier without the paste handler’s `try/catch`; malformed relay content can reject the click callback and leave inconsistent UI. PQ decoders can also receive attacker-selected large or malformed data without preflight size checks.
**Impact:** Remote relays can cause denial of service, misleading partial results, or expensive parsing/cryptographic work.
**Remediation:** Add strict schemas and byte-size limits before cryptography. Every exported verification function should return structured failure for untrusted input, never throw for ordinary malformed data. Add fuzz/property tests and maximum event/proof sizes.
### G56-08 — Browser origin security is insufficient for handling a root seed
**Severity:** High
**Confidence:** Confirmed
The page states the seed is processed entirely in the browser at [`index.html`](../../www/index.html:364). That means the web origin—not merely the reviewed source—is trusted with the seed. The live response lacked HSTS, `X-Frame-Options`/CSP `frame-ancestors`, Referrer-Policy, Permissions-Policy, and other defense-in-depth headers. The meta CSP allows `'unsafe-inline'` scripts at [`index.html`](../../www/index.html:7) and [`verify.html`](../../www/verify.html:7), materially weakening XSS protection.
Two large same-origin signer scripts are loaded outside this repository at [`index.html`](../../www/index.html:577) and [`verify.html`](../../www/verify.html:350). They have no SRI and could not be source-audited here. Live sizes were 328,444 and 230,498 bytes. Any server compromise or update to these assets can exfiltrate the mnemonic and all PQ keys. The source for `/ots-upgrade` and server configuration is also absent.
**Impact:** A single origin compromise defeats the entire root-of-trust design. HTTPS alone does not make web-based seed entry equivalent to a hardware or native signer.
**Remediation:** Do not recommend entering a valuable existing mnemonic into this web app. Move derivation and PQ signing into a dedicated signer/hardware boundary. Also extract inline scripts, enforce nonce/hash CSP without `'unsafe-inline'`, add `frame-ancestors 'none'`, HSTS, Referrer-Policy, Permissions-Policy, pinned/vendored signer assets, reviewed deployment configuration, and release integrity documentation.
### G56-09 — Generated seed defaults to 12 words and the visible warning is absent
**Severity:** Medium
**Confidence:** Confirmed
[`generateSeedPhrase()`](../../www/js/pq-crypto.mjs:76) and [`generateSeedPhraseWithEntropy()`](../../www/js/pq-crypto.mjs:91) always produce 128-bit entropy/12 words. The project itself estimates only about 64-bit security under an idealized Grover model at [`README.md`](../../README.md:172). The audited working tree removed the prior visible warning from the generation screen, and the live page matched this working tree.
**Impact:** Users receive “quantum-safe root of trust” messaging while the only generated option falls below the project’s own preferred PQ security level.
**Remediation:** Default to 24 words for a claimed long-term PQ root, retain a prominent explanation, and provide a deliberate entropy-strength selector. Be careful not to imply that BIP39’s low iteration count protects weak user-created phrases.
### G56-10 — Secret lifetime and exposure are not minimized
**Severity:** Medium
**Confidence:** Confirmed
The mnemonic, BIP39 seed, derived secp private key, and five PQ secret keys are retained in global JavaScript variables at [`index.html`](../../www/index.html:623). The mnemonic is rendered in the DOM at [`index.html`](../../www/index.html:934) and can be copied to the system clipboard at [`index.html`](../../www/index.html:1373). Sign-out nulls references at [`index.html`](../../www/index.html:1477), but does not overwrite typed arrays or clear the seed display/input/clipboard. Ordinary JS garbage collection offers no reliable erasure.
**Impact:** Browser extensions, injected scripts, devtools, clipboard managers, page snapshots, and memory inspection can recover high-value long-lived secrets.
**Remediation:** Prefer signer-contained keys. If browser handling remains, minimize scope/lifetime, avoid deriving an unused secp private key, overwrite typed arrays where possible, clear all DOM/input references immediately after use, avoid clipboard encouragement, add mnemonic confirmation/recovery challenge, and document that zeroization cannot be guaranteed in JavaScript.
### G56-11 — Publication advances despite zero relay acceptance
**Severity:** Medium
**Confidence:** Confirmed
After publishing, the code counts successes and failures but unconditionally displays `Published!`, disables the button, marks step 5 done, and exposes the next step at [`index.html`](../../www/index.html:1391). There is no requirement that either event reach any relay.
**Impact:** Users can believe migration succeeded when the announcement, wrapper, or both are unavailable. The two-event pair can also be split across relays.
**Remediation:** Require policy thresholds for both events, distinguish partial failure, retain retry controls, verify by querying accepted events back, and provide downloadable backups. Do not mark complete unless at least one configured durable relay stores both exact events.
### G56-12 — Resume workflow is insufficiently bound to the original event
**Severity:** Medium
**Confidence:** Confirmed
Only OTS metadata/proof is persisted by [`savePendingOts()`](../../www/js/pq-crypto.mjs:1362), not the complete immutable original wrapper. On resume, the code queries relays for the latest kind 11112 and adopts it at [`index.html`](../../www/index.html:749). It does not require that the fetched event ID equal the persisted ID, that its SHA-256 target matches the persisted proof, or that it passes complete verification before becoming `pqEvent`.
There is also an ID transition bug: after an upgraded replacement is signed, [`savePendingOts()`](../../www/index.html:1726) stores the new wrapper event ID. The OTS proof, however, targets the embedded kind 1 hash, while comments and metadata call `eventId` “The NIP-QR event id” at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1358). This overloaded identifier complicates safe recovery.
**Impact:** A resume can upgrade or republish the wrong wrapper/content, especially after replacements or hostile relay responses.
**Remediation:** Persist the complete signed announcement, wrapper, target digest, owner, and exact relay receipts under separate immutable IDs. On resume, validate every binding before signing or publishing. Never substitute a merely newer relay event.
### G56-13 — OTS parser resource and integer limits are incomplete
**Severity:** Medium
**Confidence:** Confirmed
The parser allows recursion depth 512 at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1126), but has no overall proof-size limit, total operation count, total branches, or total hash-work budget. [`readVaruint()`](../../www/js/pq-crypto.mjs:1032) uses JavaScript bitwise operations, which are signed 32-bit and wrap for sufficiently large encodings. The continuation loop can process many branches, and each operation allocates/hash-processes attacker-controlled data.
**Impact:** Pasted or relay-sourced proofs can consume excessive CPU/memory or create malformed length/height semantics.
**Remediation:** Reject proofs above a small protocol maximum; cap operations, branches, argument bytes, and total hashing; parse varints with safe-number checks or `BigInt`; reject non-canonical and overlong encodings; require complete input consumption; fuzz the parser.
### G56-14 — Root dependency graph is not reproducible from the repository
**Severity:** Medium
**Confidence:** Confirmed
[`package.json`](../../package.json:13) uses caret ranges for all dependencies. [`package-lock.json`](../../package-lock.json) exists locally but is explicitly ignored by [`.gitignore`](../../.gitignore:8) and is not tracked. Its root metadata says version `1.0.0`, whereas the manifest says `0.0.11`, showing it is stale or generated under a different package state.
`npm audit --omit=dev` found zero vulnerabilities for the installed graph, and the bundle rebuilt identically. Those are positive observations for this machine only. A clean install later may resolve different code, and there is no committed integrity map connecting source dependencies to the released bundle.
**Remediation:** Commit an up-to-date lockfile, use `npm ci`, pin release dependencies, add CI audit/build/test jobs, produce an SBOM/provenance attestation, and document how the deployed bundle hash maps to a source revision.
### G56-15 — OpenTimestamps submodule and historical dependencies are not reproducible
**Severity:** Medium
**Confidence:** Confirmed
The root tree records [`resources/javascript-opentimestamps/`](../../resources/javascript-opentimestamps/) as gitlink commit `c07ba8be0dae4e8721a84f32820abf7b2547a6ce`, but the repository has no `.gitmodules` mapping. `git submodule status` failed with “no submodule mapping found.” The already-present nested repository points to the upstream GitHub project, but a fresh clone cannot initialize it normally.
Its package metadata is version 0.4.9 and contains old ranges including `request`, `moment-timezone`, `minimatch`, and other historical packages. No lockfile is present, so its historical dependency graph could not be audited reproducibly. This code is not runtime-integrated, but its example proofs are required by root tests.
**Remediation:** Restore a valid `.gitmodules` entry or vendor a clearly licensed immutable snapshot. Pin and document the commit, remove claims that it is integrated, and either modernize/audit its dependency graph or retain only the test vectors with provenance and hashes.
### G56-16 — Documentation and UI overclaim status and guarantees
**Severity:** Medium
**Confidence:** Confirmed
Examples:
- [`README.md`](../../README.md:372) says full client-side OTS verification is not implemented, but code now implements an API-trusting Merkle-root check.
- [`README.md`](../../README.md:758) still lists full OTS verification, target binding, and the test suite as not implemented, although all exist in some form.
- [`nip_proposal.md`](../../nip_proposal.md:294) says all OTS verification happens in the browser and “No private key ever touches a server,” but signer modes may be remote and the implementation relies on an unreviewed server helper and public APIs.
- [`index.html`](../../www/index.html:323) says “Make your Nostr identity quantum-resistant,” while the project only links keys and does not define PQ signing for routine events or PQ messaging.
- [`verify.html`](../../www/verify.html:255) says it verifies all signatures, despite G56-01.
- The claim that a pending OTS proof establishes submission time at [`nip_proposal.md`](../../nip_proposal.md:208) is too strong: a pending calendar attestation is not a Bitcoin timestamp and depends on calendar behavior.
**Impact:** Users and reviewers may treat the prototype as production-ready or misunderstand its trust model.
**Remediation:** Maintain an explicit security model and implementation matrix generated from tests. Use “research prototype,” distinguish API-assisted verification from Bitcoin light-client verification, distinguish key linkage from identity becoming quantum-resistant, and label design-only research documents prominently.
### G56-17 — Claimed Bitcoin block height is unverified
**Severity:** Low
**Confidence:** Confirmed
Block height comes from one mempool.space request at [`index.html`](../../www/index.html:808). Failure returns zero, yet signing continues at [`index.html`](../../www/index.html:1063). The verifier does not compare the `block_height` tag or content statement to the OTS attested block.
**Remediation:** Remove the claimed height from signed content or validate it against the eventual OTS result. Do not sign a “pre-quantum at height 0” statement.
### G56-18 — Insecure/arbitrary relay endpoints are allowed
**Severity:** Low
**Confidence:** Confirmed
The add-relay validator explicitly accepts `ws://` at [`index.html`](../../www/index.html:1436). The verify page does not validate schemes before `new WebSocket()` at [`verify.html`](../../www/verify.html:474).
**Impact:** On an HTTP/local deployment, users can transmit public metadata to plaintext relays; arbitrary endpoints also increase privacy leakage and browser-network probing surface.
**Remediation:** Require `wss://` outside explicit localhost development, canonicalize URLs, cap count/length, and explain that relay queries reveal identity interest and client IP.
### G56-19 — Event ID is optional in library verification
**Severity:** Low
**Confidence:** Confirmed dynamically
[`verifyNostrEvent()`](../../www/js/pq-crypto.mjs:400) checks ID equality only when `event.id` is truthy at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:412). A dynamically created event with a valid signature but omitted `id` returned true.
The embedded verifier currently requires neither `id` explicitly in its structural condition at [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs:624) nor at the core verifier. While the `e` tag later supplies some binding for wrappers, this exported primitive’s behavior is surprising and unsafe for callers.
**Remediation:** Require exact NIP-01 shape and a 64-hex ID by default. If ID-less template verification is needed, expose a separate clearly named function.
### G56-20 — Event-size interoperability is untested
**Severity:** Informational
**Confidence:** Confirmed size; relay behavior contextual
A fixed-mnemonic unsigned kind 1 announcement was approximately 27,500 bytes before `id`/`sig`; the wrapper embeds that JSON and adds proof data. Relay limits vary, and the application’s default relays may reject large events. Existing tests do not publish/query a representative event or enforce a size budget.
**Remediation:** Test against target relay policies, report exact size before signer approval, select suitable relays, and consider a canonical compact binary or referenced commitment format while preserving independent availability.
---
## Cryptographic and protocol assessment
### Positive observations
- BIP39 generation uses the dependency’s CSPRNG-backed [`generateMnemonic()`](../../www/js/pq-crypto.mjs:76).
- User entropy is mixed with fresh CSPRNG bytes through SHA-256 rather than replacing the CSPRNG at [`generateSeedPhraseWithEntropy()`](../../www/js/pq-crypto.mjs:91).
- PQ derivation is deterministic and domain-separated by child paths at [`PQ_DERIVATION_PATHS`](../../www/js/pq-crypto.mjs:50).
- The first-48-byte concatenation rule is explicit at [`derivePQSeedFromBIP32()`](../../www/js/pq-crypto.mjs:161).
- Four PQ sign/verify round trips and tampering cases passed.
- NIP-01 event serialization is conventional at [`computeEventId()`](../../www/js/pq-crypto.mjs:510).
- Full-event hashing consistently uses `JSON.stringify` of the embedded signed object at [`hashFullEvent()`](../../www/js/pq-crypto.mjs:530), and the exact JSON string is embedded in the wrapper.
- OTS target digest binding is enforced before Bitcoin-attestation processing at [`verifyOtsProof()`](../../www/js/pq-crypto.mjs:1300).
- The known historical OTS proof verified against live API data, and pending proofs were rejected as Bitcoin-unconfirmed.
### Interoperability and design cautions
- There are no independent known-answer values for the BIP32 child private keys or resulting PQ public keys. Existing “determinism” tests compare the implementation with itself, not another implementation.
- Deriving standardized PQ seeds directly from BIP32 secp scalars is bespoke and should receive cryptographic review. Rejection behavior in BIP32 means output is not simply an unconstrained PRF byte stream; HKDF with explicit labels may be easier to specify independently.
- Falcon remains a draft/complex implementation choice. Multi-scheme hedging helps only if verification policy actually requires and distinguishes schemes.
- The attestation signs human-readable content while keys live in tags. The secp signature binds tags, but each PQ signature does not bind its own algorithm identifier, public-key set, wrapper kind, or OTS digest. A canonical structured statement would make cross-client policy safer.
- ML-KEM ownership is not proven by a KEM operation. The old secp event authorizes the public key at publication time, but no decapsulation proof establishes possession.
- The security argument depends on long-term availability of an immutable announcement and proof. Replaceable events and relay retention are poor foundations without export/archive mechanisms.
---
## Browser and operational assessment
### Live deployment observations
At audit time:
- `https://laantungir.net/post-quantum/` and `verify.html` returned HTTP 200.
- Live HTML content lengths matched local working-tree lengths.
- Live bundle SHA-256 exactly matched local: `c14d40088d02e89f0d9aa8f1643ce31d687a4837a2eb0ab1b19406b69ecbfc32`.
- The server exposed nginx `1.24.0 (Ubuntu)` and permissive `Access-Control-Allow-Origin: *`.
- No HSTS, frame restriction, Referrer-Policy, Permissions-Policy, or other reviewed security headers appeared in HEAD responses.
- `/ots-upgrade` rejected an empty POST with HTTP 400 and `{"error": "invalid body length"}`, which is a positive input check. Its implementation and broader limits remain unaudited.
- Public mempool.space and Blockstream endpoints were reachable.
### State-machine concerns
- OTS polling can overlap if an upgrade request lasts longer than the 60-second interval; no in-flight lock exists at [`startOtsPolling()`](../../www/index.html:1636).
- Sign-out calls `localStorage.clear()` and `sessionStorage.clear()` at [`index.html`](../../www/index.html:1490), which can delete unrelated same-origin application data.
- The OTS helper response is accepted as arbitrary base64 proof bytes and then parsed/verified; this is acceptable only if parser budgets are fixed.
- The verify page can offer republishing after OTS verification, but does not require the connected signer identity to equal the original wrapper identity before requesting a signature.
- The relay query accepts the first `EVENT` without checking subscription ID or filter conformance at [`verify.html`](../../www/verify.html:493); a malicious relay can send arbitrary events, relying on later validation that is presently incomplete.
---
## Dependency and supply-chain assessment
### Root graph
Installed direct versions were:
- `@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
- `esbuild` 0.28.1
All reported MIT licenses. `esbuild` has an install script/native platform package, which is normal but relevant to build trust. No root runtime advisory was reported by npm for the installed graph.
### Conclusions
- The committed browser bundle reduces runtime npm resolution risk for the deployed static page, and it rebuilt identically on the audit machine.
- However, ignored/stale lock metadata means future builds are not reproducible.
- The same-origin signer scripts are a major additional supply-chain surface not represented in [`package.json`](../../package.json:1).
- The nested OpenTimestamps repository is a broken gitlink boundary without root submodule metadata.
- No SBOM, signed release, SRI, or CI provenance connects the live assets to a reviewed revision.
---
## Test and execution evidence
### Environment
- Node: `v24.14.1`
- npm: `11.11.0`
- Root commit: `9a975fcbc56e455a68ae16d734feeed76ba304b6`
- Nested OTS commit: `c07ba8be0dae4e8721a84f32820abf7b2547a6ce`
### Existing tests
Command: `npm test`
Result: **38 passed, 0 failed**, duration about 22.7 seconds.
Coverage included:
- BIP39 generation and validation
- deterministic PQ key generation
- four PQ sign/verify/tamper suites
- basic NIP-01 ID handling
- hex input checks
- OTS fixture parsing
- OTS target digest mismatch rejection
- live block-explorer checks for historical Bitcoin block 358391
Important gaps:
- no fully valid kind 1 + wrapper construction/verification round trip,
- no required-algorithm policy tests,
- no malformed/fuzz tests,
- no wrapper/embedded identity mismatch test,
- no signer-return validation test,
- no earliest-anchor candidate selection test,
- no real browser/signer/relay end-to-end test,
- no independent PQ derivation known-answer vectors,
- no OTS calendar-fragment compatibility test in the committed suite.
### Build reproducibility
Commands:
```text
cp www/pq-crypto.bundle.js /tmp/post_quantum_nostr.bundle.before.js
node build-pq-bundle.js
cmp /tmp/post_quantum_nostr.bundle.before.js www/pq-crypto.bundle.js
```
Result: **byte-for-byte identical**. Both SHA-256 values were:
`c14d40088d02e89f0d9aa8f1643ce31d687a4837a2eb0ab1b19406b69ecbfc32`
### Live OTS calendar submission
A synthetic 32-byte digest was submitted through [`timestampEvent()`](../../www/js/pq-crypto.mjs:862). All five configured calendars returned fragments. The merged detached proof was 1,078 bytes, parsed with the exact submitted target digest, and contained five pending branches. This validates current network reachability and basic merge/parser compatibility, not future Bitcoin confirmation or trustlessness.
### Adversarial probes
The following were executed against the module without modifying source:
- empty PQ algorithm set accepted as valid,
- signature algorithms with omitted signatures accepted as KEM/no-signature entries,
- unknown omitted-signature algorithm accepted,
- outer/embedded identity mismatch accepted by separate checks,
- missing event ID accepted if signature was valid,
- malformed base64 and wrong-shaped tags caused exceptions.
These probes are the basis for G56-01, G56-03, G56-07, and G56-19.
### Audited file hashes
| File | SHA-256 |
|---|---|
| [`package.json`](../../package.json) | `63882474ff056e46bfcf9194f9a0eb8d56662f0042b3692b35c8e1b8d2ce62e3` |
| local ignored [`package-lock.json`](../../package-lock.json) | `60cfaa1dce7de67763a8fae3695cc173f8910a3de7ca53890ae9269f4ead00b9` |
| [`index.html`](../../www/index.html) | `0ae7436b621526662f8315b892ddc921c3e00b6dcac7e08c2d5dba1c7d75717b` |
| [`verify.html`](../../www/verify.html) | `70069d0f3998864a411ce4a52269de9212b2121888bf40c93e3e71edd58494a3` |
| [`pq-crypto.mjs`](../../www/js/pq-crypto.mjs) | `241cb3fb2d4ef3b7b05c7aa1757c67361a4e0a1c809c3dbafe3153d84e3bd7e5` |
| [`pq-crypto.bundle.js`](../../www/pq-crypto.bundle.js) | `c14d40088d02e89f0d9aa8f1643ce31d687a4837a2eb0ab1b19406b69ecbfc32` |
| [`pq-crypto.test.mjs`](../../test/pq-crypto.test.mjs) | `107f9d04e4dd77e4f37ab203ace804c64bfa4d84e8334101586d322bdd311351` |
---
## Prioritized remediation roadmap
### Release blocker: protocol validity
1. Fix G56-01 with a strict schema and mandatory algorithm policy.
2. Fix G56-03 by binding all identities and requested authors.
3. Implement G56-02 with immutable candidate discovery and earliest-valid-anchor selection.
4. Validate signer output and exact event fields (G56-05).
5. Make every verifier fail closed under malformed input and add budgets (G56-07/G56-13).
### Release blocker: security model
6. Decide and formally specify mnemonic-existing-identity versus raw-nsec migration modes (G56-06).
7. Replace API-trusting “Bitcoin cryptographic verification” claims or implement a real node/light-client trust model (G56-04).
8. Move seed/PQ private-key operations into a signer boundary; until then, label the web origin risk prominently (G56-08/G56-10).
### Operational hardening
9. Require relay acceptance for both events and verify read-back (G56-11).
10. Persist immutable complete workflow artifacts with exact bindings (G56-12).
11. Default to 24 words and restore warning/confirmation UX (G56-09).
12. Enforce WSS and privacy-aware endpoint validation (G56-18).
13. Add deployment headers, strict CSP, asset pinning, reviewed `/ots-upgrade` source, and release integrity.
### Reproducibility and assurance
14. Commit a correct lockfile and repair the OpenTimestamps submodule (G56-14/G56-15).
15. Add independent derivation vectors, adversarial tests, browser integration tests, relay tests, and fuzzing.
16. Reconcile all documentation and UI claims with actual behavior (G56-16).
17. Obtain independent cryptographic/protocol review before users anchor irreversible identity claims.
---
## Safe-use recommendation
At its current stage, the project is suitable for:
- local experimentation with throwaway identities,
- generating test vectors,
- researching Nostr/PQ migration formats,
- exercising OpenTimestamps parsing and API-assisted checks.
It is **not** suitable for:
- entering an existing valuable BIP39 mnemonic,
- making irreversible migration claims for a real identity,
- relying on the verifier’s green/success result,
- asserting that an identity is now quantum-resistant,
- treating the OTS result as independently validated Bitcoin consensus,
- relying on relay discovery to recover the canonical earliest migration.
A prudent user should wait until the critical/high findings are fixed, the architectural invariants above are implemented, a new adversarial test suite passes, deployment/source provenance is reproducible, and an independent human cryptography/protocol audit validates the revised design.