Skip to content

Commit 9f74ba4

Browse files
authored
Merge pull request #9070 from SomberNight/202405_ecc_ecdsa_low_s
ecc: ecdsa_verify to enforce low-S rule
2 parents bafd52e + 07c80d2 commit 9f74ba4

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

electrum/ecc.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,13 @@ def ecdsa_verify_recoverable(self, sig65: bytes, msg32: bytes) -> bool:
342342
# check message
343343
return self.ecdsa_verify(sig65[1:], msg32)
344344

345-
def ecdsa_verify(self, sig64: bytes, msg32: bytes) -> bool:
345+
def ecdsa_verify(
346+
self,
347+
sig64: bytes,
348+
msg32: bytes,
349+
*,
350+
enforce_low_s: bool = True, # policy/standardness rule
351+
) -> bool:
346352
assert_bytes(sig64)
347353
if len(sig64) != 64:
348354
return False
@@ -353,7 +359,8 @@ def ecdsa_verify(self, sig64: bytes, msg32: bytes) -> bool:
353359
ret = _libsecp256k1.secp256k1_ecdsa_signature_parse_compact(_libsecp256k1.ctx, sig, sig64)
354360
if 1 != ret:
355361
return False
356-
ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig)
362+
if not enforce_low_s:
363+
ret = _libsecp256k1.secp256k1_ecdsa_signature_normalize(_libsecp256k1.ctx, sig, sig)
357364

358365
pubkey = self._to_libsecp256k1_pubkey_ptr()
359366
if 1 != _libsecp256k1.secp256k1_ecdsa_verify(_libsecp256k1.ctx, sig, msg32, pubkey):
@@ -439,7 +446,8 @@ def verify_usermessage_with_address(address: str, sig65: bytes, message: bytes,
439446
else:
440447
return False
441448
# check message
442-
return public_key.ecdsa_verify(sig65[1:], h)
449+
# note: `$ bitcoin-cli verifymessage` does NOT enforce the low-S rule for ecdsa sigs
450+
return public_key.ecdsa_verify(sig65[1:], h, enforce_low_s=False)
443451

444452

445453
def is_secret_within_curve_range(secret: Union[int, bytes]) -> bool:
@@ -566,6 +574,8 @@ def schnorr_sign(self, msg32: bytes, *, aux_rand32: bytes = None) -> bytes:
566574
return sig64
567575

568576
def ecdsa_sign_recoverable(self, msg32: bytes, *, is_compressed: bool) -> bytes:
577+
assert len(msg32) == 32, len(msg32)
578+
569579
def bruteforce_recid(sig64: bytes):
570580
for recid in range(4):
571581
sig65 = construct_ecdsa_sig65(sig64, recid, is_compressed=is_compressed)

tests/test_bitcoin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ def test_signmessage_legacy_address(self):
246246
self.assertFalse(ecc.verify_usermessage_with_address(addr1, b'wrong', msg1))
247247
self.assertFalse(ecc.verify_usermessage_with_address(addr1, sig2, msg1))
248248

249+
def test_signmessage_low_s(self):
250+
"""`$ bitcoin-cli verifymessage` does NOT enforce the low-S rule for ecdsa sigs. This tests we do the same."""
251+
addr = "15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz"
252+
sig_low_s = b'Hzsu0U/THAsPz/MSuXGBKSULz2dTfmrg1NsAhFp+wH5aKfmX4Db7ExLGa7FGn0m6Mf43KsbEOWpvUUUBTM3Uusw='
253+
sig_high_s = b'IDsu0U/THAsPz/MSuXGBKSULz2dTfmrg1NsAhFp+wH5a1gZoH8kE7O05lE65YLZFzLx3sh/rDzXMbo1dQAJhhnU='
254+
msg = b'Chancellor on brink of second bailout for banks'
255+
self.assertTrue(ecc.verify_usermessage_with_address(address=addr, sig65=base64.b64decode(sig_low_s), message=msg))
256+
self.assertTrue(ecc.verify_usermessage_with_address(address=addr, sig65=base64.b64decode(sig_high_s), message=msg))
257+
249258
def test_signmessage_segwit_witness_v0_address(self):
250259
msg = b'Electrum'
251260
# p2wpkh-p2sh

tests/test_ecc.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,27 @@ def bip340_tagged_hash__from_libsecp(tag: bytes, msg: bytes) -> bytes:
116116
for tag, msg in data:
117117
self.assertEqual(bip340_tagged_hash__from_libsecp(tag, msg),
118118
ecc.bip340_tagged_hash(tag, msg))
119+
120+
121+
class TestEcdsa(ElectrumTestCase):
122+
123+
def test_verify_enforces_low_s(self):
124+
# privkey = ecc.ECPrivkey(bytes.fromhex("d473e2ec218dca8e3508798f01cdfde0135fc79d95526b12e3537fe57e479ac1"))
125+
# r, low_s = privkey.ecdsa_sign(msg32, sigencode=lambda x, y: (x,y))
126+
# pubkey = ecc.ECPubkey(privkey.get_public_key_bytes())
127+
pubkey = ecc.ECPubkey(bytes.fromhex("03befe4f7c92eaed73fb8eddac28c6191c87c6a3546bf8dc09643e1e10bc6f5ab0"))
128+
msg32 = sha256("hello there")
129+
r = 29658118546717807188148256874354333643324863178937517286987684851194094232509
130+
# low-S
131+
low_s = 9695211969150896589566136599751503273246834163278279637071703776634378000266
132+
sig64_low_s = (
133+
int.to_bytes(r, length=32, byteorder="big") +
134+
int.to_bytes(low_s, length=32, byteorder="big"))
135+
self.assertTrue(pubkey.ecdsa_verify(sig64_low_s, msg32))
136+
# high-S
137+
high_s = ecc.CURVE_ORDER - low_s
138+
sig64_high_s = (
139+
int.to_bytes(r, length=32, byteorder="big") +
140+
int.to_bytes(high_s, length=32, byteorder="big"))
141+
self.assertFalse(pubkey.ecdsa_verify(sig64_high_s, msg32))
142+
self.assertTrue(pubkey.ecdsa_verify(sig64_high_s, msg32, enforce_low_s=False))

0 commit comments

Comments
 (0)