Skip to content

NIP-98 (kind:27235) sign_event: malformed-params hang + stale-created_at on delayed approval #89

Description

@DocNR

Investigating an App Store review reporting that NIP-98 HTTP-auth signing (sign_event kind:27235 — used by gitworkshop, nsite, and media uploads) did not work: "lacking parity with Amber. wasnt able to HTTP Auth challenge requests." Review is mild (4-star, "seems promising"), no repro steps, and no matching GitHub account/issue exists from the reviewer. This issue reports what live testing against Clave's real signing code actually found — two confirmed bugs, one path cleared, and what's still unconfirmed.

Confirmed via live wire tests

research/nip98-signing-repro/run.sh drives ClaveTests/LightSignerNip98WireSmokeTests — Clave's actual LightSigner.handleRequest — against a local nak serve relay, then decrypts and checks the kind:24133 responses with nak.

Cleared: a spec-compliant sign_event(kind:27235) request auto-signs correctly on medium trust (the default bunker pairing). The signed event passes all four NIP-98 server checks (kind, exact u/method tags, created_at within the suggested 60s window) and the signature verifies. This path is not broken.

Bug A — malformed RPC params hang the client silently, on any trust level. LightSigner.swift:145-158: if a client sends sign_event with params shaped as [{...}] (a nested object) instead of the spec-required [json_stringified_string], decode fails and params degrades to []. Because extractEventKind then returns nil, LightSigner.swift:374-377 forces allowed = false before trust level is even consulted — so the request gets queued for approval even on medium/full trust, and per NIP-46's "no terminal error while a request is pending" rule, Clave sends no response at all. A spec-compliant client (nostr-tools/NDK — what gitworkshop and nsyte are built on) has no client-side timeout; it just hangs. The queued entry doesn't even carry a kind number to show the user (eventKind is nil, not 27235).

Bug B — a delayed low-trust approval delivers an already-stale event. LightEvent.swift:110 (signUnsignedEvent) uses the client-supplied created_at verbatim; Clave never refreshes it to the actual signing moment. If a user on low trust takes more than NIP-98's suggested 60s to approve — plausible, since the prompt shows no kind label explaining what's being approved (see below) — the delivered event is already outside the freshness window the instant Clave signs it. Clave reports "signed" successfully; the failure only surfaces one hop later, at the HTTP server's 401.

Already-known parity gaps (static, not re-litigated here)

  • Approval prompt has no label for kind 27235 — KnownKinds.names (ClientPermissions.swift:261-291) has no entry, so a low-trust prompt shows bare "Kind 27235".
  • Clave ignores the optional perms param clients request at connect (nothing reads it).

Not confirmed

  • The reviewer's exact failure — no repro steps to match against.
  • The push/NSE cold-start wake path. The iOS Simulator has no push, so this couldn't be exercised here. If the reviewer's device was backgrounded when the RPC arrived, this path — not the two bugs above — is the more likely culprit, and it remains untested.
  • An end-to-end run against actual gitworkshop.dev or nsyte. The tests above drive Clave's real signing code with the exact wire shape those clients' underlying libraries produce, but don't exercise gitworkshop's/nsyte's own client code.

Repro

research/nip98-signing-repro/run.sh <simulator-udid>

Starts a local nak serve relay, runs ClaveTests/LightSignerNip98WireSmokeTests, decrypts the wire responses with nak, and checks Bugs A and B live against the actual signed events.

Source: App Store review triage, 2026-08-23.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions