fix(core): Avoid raising IndexError during cash address decoding
What changed, and why it matters
This commit fixes a bug in how Trezor hardware wallets decode Bitcoin Cash-style 'cashaddr' addresses. Previously, a malformed address could cause the device to hit an internal 'IndexError' and show a confusing message. The fix ensures the address is properly validated and returns a clear 'invalid address' error instead. There is no direct evidence in the commit that this could be used to steal funds, but it improves robustness against bad or maliciously crafted addresses.
Treat as a defensive hardening fix. Review whether the previous IndexError could be triggered from untrusted user input paths (e.g., transaction signing UI, QR scans, or USB messages) and ensure the fix is included in the next firmware release. No emergency response is indicated by the diff alone.
Security signals we found
Input validation hardening for address parsing
Exception handling improvement to prevent confusing internal errors
Adds explicit length and checksum checks to cashaddr decoder
Fixes reported issue #7749
Evidence from the diff
The patch hardens cashaddr decoding in core/src/trezor/crypto/cashaddr.py and core/src/apps/bitcoin/scripts.py. It adds a minimum length check before checksum verification, strips the checksum before converting bits, and raises ValueError for too-short, bad-checksum, or empty payloads. The caller in scripts.py now catches ValueError and raises DataError(‘Invalid cashaddr address’) instead of propagating an IndexError. Tests are added for malformed inputs.
Changed components
core/src/trezor/crypto/cashaddr.pycore/src/apps/bitcoin/scripts.pyBitcoin Cash / ecash address decodingInspect captured patch +69 / −9
### core/src/apps/bitcoin/scripts.py
@@ -91,8 +91,11 @@ def output_derive_script(address: str, coin: CoinInfo) -> AnyBytes:
and coin.cashaddr_prefix is not None
and address.startswith(coin.cashaddr_prefix + ":")
):
- prefix, addr = address.split(":")
- version, data = cashaddr.decode(prefix, addr)
+ try:
+ prefix, addr = address.split(":")
+ version, data = cashaddr.decode(prefix, addr)
+ except ValueError:
+ raise DataError("Invalid cashaddr address")
if version == cashaddr.ADDRESS_TYPE_P2KH:
version = coin.address_type
elif version == cashaddr.ADDRESS_TYPE_P2SH:
### core/src/trezor/crypto/cashaddr.py
@@ -27,6 +27,7 @@
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
ADDRESS_TYPE_P2KH = const(0)
ADDRESS_TYPE_P2SH = const(8)
+CASHADDR_CHECKSUM_SIZE = const(8)
def cashaddr_polymod(values: list[int]) -> int:
@@ -51,10 +52,12 @@ def prefix_expand(prefix: str) -> list[int]:
def _calculate_checksum(prefix: str, payload: list[int]) -> list[int]:
- poly = cashaddr_polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
+ poly = cashaddr_polymod(
+ prefix_expand(prefix) + payload + [0] * CASHADDR_CHECKSUM_SIZE
+ )
out = []
- for i in range(8):
- out.append((poly >> 5 * (7 - i)) & 0x1F)
+ for i in range(CASHADDR_CHECKSUM_SIZE):
+ out.append((poly >> 5 * (CASHADDR_CHECKSUM_SIZE - 1 - i)) & 0x1F)
return out
@@ -83,10 +86,16 @@ def decode(prefix: str, addr: str) -> tuple[int, bytes]:
addr = addr.lower()
decoded = _b32decode(addr)
+ # Payload must include the 8-symbol checksum.
+ if len(decoded) < CASHADDR_CHECKSUM_SIZE:
+ raise ValueError # Cashaddr payload too short
+
# verify_checksum
checksum_verified = cashaddr_polymod(prefix_expand(prefix) + decoded) == 0
if not checksum_verified:
- raise ValueError("Bad cashaddr checksum")
+ raise ValueError # Bad cashaddr checksum
- data = bytes(convertbits(decoded, 5, 8))
- return data[0], data[1:-6]
+ data = bytes(convertbits(decoded[:-CASHADDR_CHECKSUM_SIZE], 5, 8, False))
+ if not data:
+ raise ValueError # Empty cashaddr payload
+ return data[0], data[1:]
### core/tests/test_trezor.crypto.cashaddr.py
@@ -34,6 +34,36 @@
"bchreg:555555555555555555555555555555555555555555555udxmlmrz",
]
+INVALID_ADDRESS = [
+ "prefix:x32nx6hz",
+ "prEfix:x64nx6hz",
+ "prefix:x64nx6Hz",
+ "pref1x:6m8cxv73",
+ "prefix:x64nx6hz",
+ "prefix:",
+ ":u9wsx07j",
+ "bchreg:555555555555555555x55555555555555555555555555udxmlmrz",
+ "bchreg:555555555555555555555555555555551555555555555udxmlmrz",
+ "pre:fix:x32nx6hz",
+ "prefixx64nx6hz",
+ "",
+ ":",
+ "p",
+ "p:",
+ "p:g",
+ "p:gp",
+ "p:gpf",
+ "p:gpf8",
+ "p:gpf8m",
+ "p:gpf8m4",
+ "p:gpf8m4h",
+ "p:gpf8m4h7",
+ "rpzrrzpr:",
+ "rqiqkqiqr:",
+ "c:qvdy2z3",
+ "ecash:q9mcgrsqm",
+]
+
VALID_ADDRESS = [
(
"1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu",
@@ -66,12 +96,30 @@ class TestCryptoCashAddr(unittest.TestCase):
def test_valid_checksum(self):
for test in VALID_CHECKSUM:
prefix, addr = test.split(":")
- cashaddr.decode(prefix, addr)
+ decoded = cashaddr._b32decode(addr.lower())
+ self.assertEqual(
+ cashaddr.cashaddr_polymod(cashaddr.prefix_expand(prefix) + decoded),
+ 0,
+ )
def test_invalid_checksum(self):
for test in VALID_CHECKSUM:
test += "xxx"
prefix, addr = test.split(":")
+ decoded = cashaddr._b32decode(addr.lower())
+ self.assertNotEqual(
+ cashaddr.cashaddr_polymod(cashaddr.prefix_expand(prefix) + decoded),
+ 0,
+ )
+ with self.assertRaises(ValueError):
+ cashaddr.decode(prefix, addr)
+
+ def test_invalid_address(self):
+ for test in INVALID_ADDRESS:
+ if ":" in test:
+ prefix, addr = test.split(":", 1)
+ else:
+ prefix, addr = "", test
with self.assertRaises(ValueError):
cashaddr.decode(prefix, addr)
Why this scored 39/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.