diff --git a/electrum/ecc.py b/electrum/ecc.py index 5ec9625a8..a8faeaf81 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -173,19 +173,34 @@ class ECPubkey(object): return ECPubkey._from_libsecp256k1_pubkey_ptr(pubkey) @classmethod - def from_signature65(cls, sig: bytes, msg_hash: bytes) -> Tuple['ECPubkey', bool]: + def from_signature65(cls, sig: bytes, msg_hash: bytes) -> Tuple['ECPubkey', bool, Optional[str]]: if len(sig) != 65: raise Exception(f'wrong encoding used for signature? len={len(sig)} (should be 65)') nV = sig[0] - if not (27 <= nV <= 34): + # as per BIP-0137: + # 27-30: p2pkh (uncompressed) + # 31-34: p2pkh (compressed) + # 35-38: p2wpkh-p2sh + # 39-42: p2wpkh + # However, the signatures we create do not respect this, and we instead always use 27-34, + # only distinguishing between compressed/uncompressed, so we treat those values as "any". + if not (27 <= nV <= 42): raise Exception("Bad encoding") - if nV >= 31: - compressed = True + txin_type_guess = None + compressed = True + if nV >= 39: + nV -= 12 + txin_type_guess = "p2wpkh" + elif nV >= 35: + nV -= 8 + txin_type_guess = "p2wpkh-p2sh" + elif nV >= 31: nV -= 4 else: compressed = False recid = nV - 27 - return cls.from_sig_string(sig[1:], recid, msg_hash), compressed + pubkey = cls.from_sig_string(sig[1:], recid, msg_hash) + return pubkey, compressed, txin_type_guess @classmethod def from_x_and_y(cls, x: int, y: int) -> 'ECPubkey': @@ -294,7 +309,7 @@ class ECPubkey(object): assert_bytes(message) h = algo(message) try: - public_key, compressed = self.from_signature65(sig65, h) + public_key, compressed, txin_type_guess = self.from_signature65(sig65, h) except Exception: return False # check public key @@ -376,12 +391,13 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, n if net is None: net = constants.net h = sha256d(msg_magic(message)) try: - public_key, compressed = ECPubkey.from_signature65(sig65, h) + public_key, compressed, txin_type_guess = ECPubkey.from_signature65(sig65, h) except Exception as e: return False # check public key using the address pubkey_hex = public_key.get_public_key_hex(compressed) - for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']: + txin_types = (txin_type_guess,) if txin_type_guess else ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh') + for txin_type in txin_types: addr = pubkey_to_address(txin_type, pubkey_hex, net=net) if address == addr: break diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index f9c03e311..e745d08d7 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -491,7 +491,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): sig_string = binascii.unhexlify(reply['sign'][0]['sig']) recid = int(reply['sign'][0]['recid'], 16) sig = ecc.construct_sig65(sig_string, recid, True) - pubkey, compressed = ecc.ECPubkey.from_signature65(sig, msg_hash) + pubkey, compressed, txin_type_guess = ecc.ECPubkey.from_signature65(sig, msg_hash) addr = public_key_to_p2pkh(pubkey.get_public_key_bytes(compressed=compressed)) if ecc.verify_message_with_address(addr, sig, message) is False: raise Exception(_("Could not sign message")) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index f89239c89..3dd85b2df 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -225,6 +225,25 @@ class Test_bitcoin(ElectrumTestCase): self.assertTrue(ecc.verify_message_with_address(addr2, sig2, msg)) self.assertFalse(ecc.verify_message_with_address(addr2, sig2, b'heyheyhey')) + def test_signmessage_segwit_witness_v0_address_test_we_also_accept_sigs_from_trezor(self): + """Trezor and some other projects use a slightly different scheme for message-signing + with p2wpkh and p2wpkh-p2sh addresses. Test that we also accept signatures from them. + see #3861 + tests from https://github.com/trezor/trezor-firmware/blob/2ce1e6ba7dbe5bbaeeb336fff0a038e59cb40ef8/tests/device_tests/bitcoin/test_signmessage.py#L39 + """ + msg = b"This is an example of a signed message." + addr1 = "3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2" + addr2 = "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk" + sig1 = bytes.fromhex("23744de4516fac5c140808015664516a32fead94de89775cec7e24dbc24fe133075ac09301c4cc8e197bea4b6481661d5b8e9bf19d8b7b8a382ecdb53c2ee0750d") + sig2 = bytes.fromhex("28b55d7600d9e9a7e2a49155ddf3cfdb8e796c207faab833010fa41fb7828889bc47cf62348a7aaa0923c0832a589fab541e8f12eb54fb711c90e2307f0f66b194") + self.assertTrue(ecc.verify_message_with_address(address=addr1, sig65=sig1, message=msg)) + self.assertTrue(ecc.verify_message_with_address(address=addr2, sig65=sig2, message=msg)) + # if there is type information in the header of the sig (first byte), enforce that: + sig1_wrongtype = bytes.fromhex("27744de4516fac5c140808015664516a32fead94de89775cec7e24dbc24fe133075ac09301c4cc8e197bea4b6481661d5b8e9bf19d8b7b8a382ecdb53c2ee0750d") + sig2_wrongtype = bytes.fromhex("24b55d7600d9e9a7e2a49155ddf3cfdb8e796c207faab833010fa41fb7828889bc47cf62348a7aaa0923c0832a589fab541e8f12eb54fb711c90e2307f0f66b194") + self.assertFalse(ecc.verify_message_with_address(address=addr1, sig65=sig1_wrongtype, message=msg)) + self.assertFalse(ecc.verify_message_with_address(address=addr2, sig65=sig2_wrongtype, message=msg)) + @needs_test_with_all_aes_implementations def test_decrypt_message(self): key = WalletStorage.get_eckey_from_password('pw123')