segwit_addr: bech32 decode without checksum option
What changed, and why it matters
This commit adds an optional way to decode Bech32 strings without verifying their checksum. It is a small, controlled API change in Electrum's address-decoding helper, not a fix for a known vulnerability. The new option is off by default and only used in tests, so it does not appear to weaken normal wallet behavior on its own.
Review future commits that consume this new option to ensure checksum-less decoding is only used in safe contexts (e.g., parsing BOLT12 offers where checksum validation is handled separately). No immediate action is required for this commit alone.
Security signals we found
New API surface that bypasses checksum verification
Default behavior remains checksum-verified
No production callers shown in the diff
Test-only usage of the new option
No mention of vulnerability, CVE, or security fix in commit message
Evidence from the diff
The patch extends bech32_decode() in electrum/segwit_addr.py with a with_checksum=True keyword argument. When set to False, the function skips bech32_verify_checksum() and returns the raw HRP and 5-bit data. It also introduces an INVALID_BECH32 sentinel and adds a unit test using a BOLT12 offer string. There is no change to default behavior; callers must explicitly opt into checksum-less decoding.
Changed components
electrum/segwit_addr.pytests/test_bitcoin.pyInspect captured patch +18 / −6
diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py
index 2249470..d94f627 100644
--- a/electrum/segwit_addr.py
+++ b/electrum/segwit_addr.py
@@ -43,6 +43,9 @@ class DecodedBech32(NamedTuple):
data: Optional[Sequence[int]] # 5-bit ints
+INVALID_BECH32 = DecodedBech32(None, None, None)
+
+
def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
@@ -85,26 +88,28 @@ def bech32_encode(encoding: Encoding, hrp: str, data: List[int]) -> str:
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
-def bech32_decode(bech: str, *, ignore_long_length=False) -> DecodedBech32:
+def bech32_decode(bech: str, *, ignore_long_length=False, with_checksum=True) -> DecodedBech32:
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
bech_lower = bech.lower()
if bech_lower != bech and bech.upper() != bech:
- return DecodedBech32(None, None, None)
+ return INVALID_BECH32
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech) or (not ignore_long_length and len(bech) > 90):
- return DecodedBech32(None, None, None)
+ return INVALID_BECH32
# check that HRP only consists of sane ASCII chars
if any(ord(x) < 33 or ord(x) > 126 for x in bech[:pos+1]):
- return DecodedBech32(None, None, None)
+ return INVALID_BECH32
bech = bech_lower
hrp = bech[:pos]
try:
data = [CHARSET_INVERSE[x] for x in bech[pos + 1:]]
except KeyError:
- return DecodedBech32(None, None, None)
+ return INVALID_BECH32
+ if not with_checksum:
+ return DecodedBech32(encoding=None, hrp=hrp, data=data)
encoding = bech32_verify_checksum(hrp, data)
if encoding is None:
- return DecodedBech32(None, None, None)
+ return INVALID_BECH32
return DecodedBech32(encoding=encoding, hrp=hrp, data=data[:-6])
diff --git a/tests/test_bitcoin.py b/tests/test_bitcoin.py
index 5673dc2..78a6b52 100644
--- a/tests/test_bitcoin.py
+++ b/tests/test_bitcoin.py
@@ -660,6 +660,13 @@ class Test_bitcoin(ElectrumTestCase):
self.assertEqual(DecodedBech32(None, None, None),
segwit_addr.bech32_decode('1p2gdwpf'))
+ # without checksum
+ bolt12_str = 'lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg'
+ self.assertEqual(DecodedBech32(None, None, None),
+ segwit_addr.bech32_decode(bolt12_str, with_checksum=True, ignore_long_length=True))
+ self.assertEqual(DecodedBech32(None, 'lno', [1, 0, 1, 16, 30, 16, 18, 0, 1, 8, 11, 4, 2, 27, 17, 0, 12, 21, 28, 6, 2, 27, 11, 16, 13, 17, 18, 18, 0, 25, 3, 5, 14, 13, 17, 23, 4, 26, 11, 16, 14, 17, 20, 22, 30, 27, 16, 18, 2, 9, 1, 4, 30, 19, 2, 20, 4, 0, 24, 19, 4, 8, 3, 9, 13, 25, 18, 7, 10, 28, 27, 20, 14, 9, 20, 22, 10, 28, 24, 22, 4, 4, 1, 14, 29, 17, 25, 4, 11, 21, 21, 23, 26, 11, 6, 11, 6, 0, 28, 0, 23, 30, 31, 2, 20, 13, 18, 8, 25, 21, 29, 9, 8, 9, 18, 19, 30, 22, 21, 3, 8, 3, 22, 28, 29, 8, 15, 18, 16, 13, 20, 6, 12, 6, 8]),
+ segwit_addr.bech32_decode(bolt12_str, with_checksum=False, ignore_long_length=True))
+
class Test_xprv_xpub(ElectrumTestCase):
Why this scored 18/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.