Files
zeekay 0403d39c62 harden(verify): defense-in-depth fail-closed across all 7 SIWx chains
Every verifier + the dispatcher now fails closed under any malformed/
adversarial input, never throws/panics, and resists DoS. No new dependency;
prod runtime tree stays 4 deps, all MIT (zero copyleft).

- src/limits.ts <-> go/walletconnect/limits.go: single bounds module —
  size caps (message/sig/pubkey/address/extra/lines) + chain<->scheme table
  (chainAllowsScheme) that rejects every illegitimate (chain,scheme) pair up
  front, closing cross-chain scheme confusion by construction.
  sig cap = 20 KiB so it clears a Cardano COSE_Sign1 (embeds the payload, ~2x
  the message) for a large-but-legal message.
- dispatcher gate() + total backstop (TS try/catch, Go defer recover) around
  verifyProof / verifyProofAsync so any throw/panic below -> rejected proof.
- per-verifier input bounds before any hash/decode (verifiers are exported,
  so each is independently safe, not just via dispatch).
- caip122 parse: bound message size + line count before split.
- strict base64 in bytes.ts (was lenient atob) — also kills a latent TS/Go
  divergence (Go StdEncoding is strict).
- CBOR depth cap (MAX_CBOR_DEPTH=16) + declared-length<=remaining guard both
  langs; in Go a stack overflow is fatal/non-recoverable so this is load-bearing.
- TON workchain range-checked to int32 in TS (Go already did).

Tests: 160 TS (was 112) incl fuzz.test.ts (47, 30k+ random inputs) +
Go property/corpus + 3 native Fuzz* (610k/1.2M/1.0M execs clean). Invariants:
(A) never throws/panics, (B) never ok for a non-valid proof, with a ranCrypto>0
gate proving the fuzzers reach the crypto layer. All prior tests stay green.

Bump @luxwallet/connect 0.1.0 -> 0.1.1.
2026-06-24 16:03:51 -07:00

72 lines
3.4 KiB
Go
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.
package walletconnect
// Defense-in-depth input bounds + chain↔scheme binding for the SIWx verify core.
//
// Every field of a Proof is attacker-controlled. The cryptographic verifiers
// already length-check the DECODED material (32-byte keys, 64/65-byte sigs, …),
// but they hash/decode the raw string FIRST — so an unbounded Message,
// Signature, PublicKey, or Extra string lets a malicious client force
// arbitrarily large allocations and hashing before any check runs (a CPU/memory
// DoS on the login path). These caps reject oversized inputs up front, so the
// work any single verify call performs is bounded regardless of input.
//
// The bounds are deliberately generous — far above any legitimate value — so
// they never reject a real wallet, only abuse. They are NOT a security control
// on their own (the crypto checks are); they cap the blast radius of a hostile
// caller. Mirrors src/limits.ts byte-for-byte so TS and Go reject the identical
// set of inputs.
const (
// maxMessageLen bounds the signed CAIP-122 message (bytes). Mirrors
// MAX_MESSAGE_LEN.
maxMessageLen = 8 * 1024
// maxSignatureLen bounds an encoded signature string. Cardano CIP-8 embeds
// the signed payload (the message) inside the COSE_Sign1 envelope, so an
// encoded signature is ≈ 2 × the message bytes; this cap must exceed
// 2 × maxMessageLen to never reject a legitimate large-message Cardano proof.
// Mirrors MAX_SIGNATURE_LEN.
maxSignatureLen = 20 * 1024
// maxPubKeyLen bounds an encoded public-key string. Mirrors MAX_PUBKEY_LEN.
maxPubKeyLen = 1024
// maxAddressLen bounds an address string. Mirrors MAX_ADDRESS_LEN.
maxAddressLen = 512
// maxExtraStringLen bounds any single string field read out of Extra.
// Mirrors MAX_EXTRA_STRING_LEN.
maxExtraStringLen = 8 * 1024
// maxMessageLines bounds the line count of a CAIP-122 message. Mirrors
// MAX_MESSAGE_LINES.
maxMessageLines = 64
)
// withinLen reports whether s is non-empty and no longer than max. Used at every
// boundary so the guard logic lives in exactly one place. Mirrors the TS
// withinLen (which also rejects empty). Length is measured in bytes, matching
// the TS .length on the ASCII/encoded fields these guard.
func withinLen(s string, max int) bool {
return len(s) > 0 && len(s) <= max
}
// chainSchemes is the set of schemes each chain is allowed to present. The
// dispatcher verifies on Scheme, but Chain is also attacker-controlled, so a
// proof could pair an arbitrary Chain with any Scheme. The downstream verifier
// would reject a mismatch anyway, but relying on a decode failure to catch a
// category error is fragile. This table rejects every (chain, scheme) mismatch
// deterministically and up front. Mirrors the TS CHAIN_SCHEMES.
var chainSchemes = map[Chain]map[SignatureScheme]bool{
ChainEVM: {SchemeSecp256k1EIP191: true},
ChainSolana: {SchemeEd25519: true},
ChainBitcoin: {SchemeBIP322: true},
ChainTON: {SchemeTonProof: true},
ChainXRP: {SchemeSecp256k1XRPL: true, SchemeEd25519XRPL: true},
ChainPolkadot: {SchemeSr25519: true, SchemeEd25519Substr: true, SchemeEcdsaSubstr: true},
ChainCardano: {SchemeEd25519Cardano: true},
}
// chainAllowsScheme reports whether scheme is a legitimate signing scheme for
// chain. Fail-closed: an unknown chain returns false. Mirrors the TS
// chainAllowsScheme.
func chainAllowsScheme(chain Chain, scheme SignatureScheme) bool {
allowed, ok := chainSchemes[chain]
return ok && allowed[scheme]
}