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

50 KiB
Raw Permalink Blame History

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 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

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, verify.html, and nip_proposal.md, 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() and its unconditional aggregate at results.every().

Worse, any algorithm tag lacking a fourth field is marked valid as a KEM, regardless of its algorithm identifier:

if (algorithm === 'ml-kem-768' || !sigBase64) {
    results.push({ algorithm, valid: true, note: 'KEM (no signature to verify)' });
    continue;
}

This logic is at verifyNIPQRContent().

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.

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. The implementation instead queries kind 11112 with limit: 1 and describes it as “latest” at verify.html. Across relays, it chooses the largest created_at value at verify.html. The resume path also requests one event at index.html and selects newest created_at across relays at index.html.

Kind 11112 is replaceable, as documented in pq-crypto.mjs. 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() validates the embedded kind 1 event and tags but receives no outer event object and therefore cannot require outer.pubkey === embedded.pubkey. verifyEvent() 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() 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. verifyOtsProof() 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, 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. It does not check:

  • signer-returned pubkey equals the authenticated currentPubkey,
  • returned ID equals computeEventId() 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 and upgraded events at index.html. The verify page republishes signer output directly at verify.html, 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() 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.

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() 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() to throw,
  • null or wrong-shaped events threw.

Relay-driven verification at verify.html 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. 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 and verify.html, materially weakening XSS protection.

Two large same-origin signer scripts are loaded outside this repository at index.html and verify.html. 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() and generateSeedPhraseWithEntropy() always produce 128-bit entropy/12 words. The project itself estimates only about 64-bit security under an idealized Grover model at README.md. 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. The mnemonic is rendered in the DOM at index.html and can be copied to the system clipboard at index.html. Sign-out nulls references at index.html, 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. 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(), not the complete immutable original wrapper. On resume, the code queries relays for the latest kind 11112 and adopts it at index.html. 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() 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. 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, but has no overall proof-size limit, total operation count, total branches, or total hash-work budget. readVaruint() 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 uses caret ranges for all dependencies. package-lock.json exists locally but is explicitly ignored by .gitignore 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/ 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 says full client-side OTS verification is not implemented, but code now implements an API-trusting Merkle-root check.
  • README.md still lists full OTS verification, target binding, and the test suite as not implemented, although all exist in some form.
  • nip_proposal.md 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 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 says it verifies all signatures, despite G56-01.
  • The claim that a pending OTS proof establishes submission time at nip_proposal.md 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. Failure returns zero, yet signing continues at index.html. 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. The verify page does not validate schemes before new WebSocket() at verify.html.

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() checks ID equality only when event.id is truthy at pq-crypto.mjs. 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 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().
  • User entropy is mixed with fresh CSPRNG bytes through SHA-256 rather than replacing the CSPRNG at generateSeedPhraseWithEntropy().
  • PQ derivation is deterministic and domain-separated by child paths at PQ_DERIVATION_PATHS.
  • The first-48-byte concatenation rule is explicit at derivePQSeedFromBIP32().
  • Four PQ sign/verify round trips and tampering cases passed.
  • NIP-01 event serialization is conventional at computeEventId().
  • Full-event hashing consistently uses JSON.stringify of the embedded signed object at hashFullEvent(), and the exact JSON string is embedded in the wrapper.
  • OTS target digest binding is enforced before Bitcoin-attestation processing at verifyOtsProof().
  • 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().
  • Sign-out calls localStorage.clear() and sessionStorage.clear() at index.html, 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; 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.
  • 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:

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(). 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 63882474ff056e46bfcf9194f9a0eb8d56662f0042b3692b35c8e1b8d2ce62e3
local ignored package-lock.json 60cfaa1dce7de67763a8fae3695cc173f8910a3de7ca53890ae9269f4ead00b9
index.html 0ae7436b621526662f8315b892ddc921c3e00b6dcac7e08c2d5dba1c7d75717b
verify.html 70069d0f3998864a411ce4a52269de9212b2121888bf40c93e3e71edd58494a3
pq-crypto.mjs 241cb3fb2d4ef3b7b05c7aa1757c67361a4e0a1c809c3dbafe3153d84e3bd7e5
pq-crypto.bundle.js c14d40088d02e89f0d9aa8f1643ce31d687a4837a2eb0ab1b19406b69ecbfc32
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

  1. Decide and formally specify mnemonic-existing-identity versus raw-nsec migration modes (G56-06).
  2. Replace API-trusting “Bitcoin cryptographic verification” claims or implement a real node/light-client trust model (G56-04).
  3. Move seed/PQ private-key operations into a signer boundary; until then, label the web origin risk prominently (G56-08/G56-10).

Operational hardening

  1. Require relay acceptance for both events and verify read-back (G56-11).
  2. Persist immutable complete workflow artifacts with exact bindings (G56-12).
  3. Default to 24 words and restore warning/confirmation UX (G56-09).
  4. Enforce WSS and privacy-aware endpoint validation (G56-18).
  5. Add deployment headers, strict CSP, asset pinning, reviewed /ots-upgrade source, and release integrity.

Reproducibility and assurance

  1. Commit a correct lockfile and repair the OpenTimestamps submodule (G56-14/G56-15).
  2. Add independent derivation vectors, adversarial tests, browser integration tests, relay tests, and fuzzing.
  3. Reconcile all documentation and UI claims with actual behavior (G56-16).
  4. 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.