verifyWalletSignature checked a signature only through @pezkuwi/util-crypto,
whose sr25519 uses Pezkuwi's `bizinikiwi` signing context. The browser
extension signs in that context. The Pezkuwi Wallet mobile app does not, for
a message: it picks the signing chain from the address prefix, prefix 42
falls back to Polkadot, and Polkadot gets the standard `substrate` context.
Transactions are unaffected, since their payload carries the genesis hash.
Measured on 2026-10-01 on dks.news, which verifies the same way: every
sign-in from the app was refused with 401, and the captured signature
verifies in the substrate context and in no other. This verifier has the
same shape and its tests sign only with @pezkuwi/keyring, so the mobile path
was never exercised here -- withdrawal and payment confirmations, identity
and citizenship signed from the app would be refused the same way.
The wallet is the released, store-tested build and is not changed for this.
The verifier now also accepts an sr25519 signature in the substrate context,
via @scure/sr25519, the upstream that @pezkuwi/scure-sr25519 is forked from
with only the context string changed. The proof is unchanged: the account's
own key over a canonical, single-use challenge.
Tests: a substrate-context signature passes; the same signature claimed for
another address, or over a different message, is refused. With the fallback
disabled the first fails (26/27); restored, 27/27.
A bounded adversarial re-verification of the new fund-custody auth found two real
bypasses that the fix rounds missed; both fixed here, plus executable proof.
- BYPASS #1 (CRITICAL, external): p2p_fiat_trades/p2p_fiat_offers kept USING(true)
anon UPDATE, and confirm-payment / admin_resolve_dispute read the fund DESTINATION
(buyer_id), amount and token straight from those rows. An anon attacker could
`UPDATE p2p_fiat_trades SET buyer_id=<attacker>` so the honest seller's correctly-
signed release paid the attacker. Fix: BEFORE UPDATE triggers freeze the fund-routing
columns (offer_id, seller_id, buyer_id, crypto_amount, fiat_amount, price_per_unit,
escrow_locked_amount on trades; seller_id, token, amount_crypto on offers) for any
non-service-role writer — trigger-level, independent of RLS/grants; status/proof/chat
stay open. (Corrects the earlier triage that dismissed these tables as non-custodial.)
- BYPASS #2 (double-release TOCTOU): confirm-payment checked status then released
non-atomically, so two concurrent differently-nonced valid requests could both release
and drain other buyers' escrow. Fix: atomic compare-and-swap of payment_sent->completed
BEFORE the escrow move (loser gets 409; revert on release failure).
- Hardening: REVOKE EXECUTE on the dead legacy resolve_p2p_dispute mover from
PUBLIC/anon/authenticated.
- Proof: 34 executable tests (Deno) — identity-auth unit tests (sr25519 sign/verify,
challenge money-param binding, citizen/visa ownership, nonce single-use+concurrency,
freshness) + per-handler authorization-flow tests (bad-sig/cross-id/wrong-state/replay,
CAS double-release). All pass.