def test_recover_pub_key_input_type() -> None: msg = "test message".encode() wif, _ = bms.gen_keys() bms_sig = bms.sign(msg, wif) key_id = bms_sig.rf - 27 & 0b11 magic_msg = magic_message(msg) Q = dsa.recover_pub_key(key_id, magic_msg, bms_sig.dsa_sig.serialize(), True, sha256) Q2 = dsa.recover_pub_key(key_id, magic_msg, bms_sig.dsa_sig, True, sha256) assert Q == Q2
def assert_as_valid(msg: Octets, addr: String, sig: Union[Sig, String], lower_s: bool = True) -> None: # Private function for test/dev purposes # It raises Errors, while verify should always return True or False if isinstance(sig, Sig): sig.assert_valid() else: sig = Sig.b64decode(sig) # first two bits in rf are reserved for key_id # key_id = 00; key_id = 01; key_id = 10; key_id = 11 # 27-27 = 000000; 28-27 = 000001; 29-27 = 000010; 30-27 = 000011 # 31-27 = 000100; 32-27 = 000101; 33-27 = 000110; 34-27 = 000111 # 35-27 = 001000; 36-27 = 001001; 37-27 = 001010; 38-27 = 001011 # 39-27 = 001100; 40-27 = 001101; 41-27 = 001110; 42-27 = 001111 key_id = sig.rf - 27 & 0b11 magic_msg = magic_message(msg) Q = dsa.recover_pub_key(key_id, magic_msg, sig.dsa_sig, lower_s, sha256) compressed = sig.rf > 30 # signature is valid only if the provided address is matched pub_key = bytes_from_point(Q, compressed=compressed) if has_segwit_prefix(addr): wit_ver, h160, _ = witness_from_address(addr) if wit_ver != 0 or len(h160) != 20: raise BTClibValueError(f"not a p2wpkh address: {addr!r}") if not (30 < sig.rf < 35 or sig.rf > 38): raise BTClibValueError( f"invalid p2wpkh address recovery flag: {sig.rf}") if hash160(pub_key) != h160: raise BTClibValueError(f"invalid p2wpkh address: {addr!r}") return script_type, h160, _ = h160_from_address(addr) if script_type == "p2pkh": if sig.rf > 34: raise BTClibValueError( f"invalid p2pkh address recovery flag: {sig.rf}") if hash160(pub_key) != h160: raise BTClibValueError(f"invalid p2pkh address: {addr!r}") return # must be P2WPKH-P2SH if not 30 < sig.rf < 39: raise BTClibValueError( f"invalid p2wpkh-p2sh address recovery flag: {sig.rf}") script_pk = b"\x00\x14" + hash160(pub_key) if hash160(script_pk) != h160: raise BTClibValueError(f"invalid p2wpkh-p2sh address: {addr!r}")
def sign(msg: Octets, prv_key: PrvKey, addr: Optional[String] = None) -> Sig: "Generate address-based compact signature for the provided message." # first sign the message magic_msg = magic_message(msg) q, network, compressed = prv_keyinfo_from_prv_key(prv_key) dsa_sig = dsa.sign(magic_msg, q) # now calculate the key_id # TODO do the match in Jacobian coordinates avoiding mod_inv pub_keys = dsa.recover_pub_keys(magic_msg, dsa_sig) Q = mult(q) # key_id is in [0, 3] # first two bits in rf are reserved for it key_id = pub_keys.index(Q) pub_key = bytes_from_point(Q, compressed=compressed) if isinstance(addr, str): addr = addr.strip() elif isinstance(addr, bytes): addr = addr.decode("ascii") # finally, calculate the recovery flag if addr is None or addr == p2pkh(pub_key, network, compressed): rf = key_id + 27 # third bit in rf is reserved for the 'compressed' boolean rf += 4 if compressed else 0 # BIP137 elif addr == p2wpkh_p2sh(pub_key, network): rf = key_id + 35 elif addr == p2wpkh(pub_key, network): rf = key_id + 39 else: raise BTClibValueError("mismatch between private key and address") return Sig(rf, dsa_sig)
def test_ledger() -> None: """Hybrid ECDSA Bitcoin message signature generated by Ledger""" mnemonic = ( "barely sun snack this snack relief pipe attack disease boss enlist lawsuit" ) # non-standard leading 31 in DER serialization derivation_path = "m/1" msg = b"\xfb\xa3\x1f\x8cd\x85\xe29#K\xb3{\xfd\xa7<?\x95oL\xee\x19\xb2'oh\xa7]\xd9A\xfeU\xd8" dersig_hex_str = "3144022012ec0c174936c2a46dc657252340b2e6e6dd8c31dd059b6f9f33a90c21af2fba022030e6305b3ccf88009d419bf7651afcfcc0a30898b93ae9de9aa6ac03cf8ec56b" # pub_key derivation rprv = bip39.mxprv_from_mnemonic(mnemonic) xprv = bip32.derive(rprv, derivation_path) # the actual message being signed magic_msg = magic_message(msg) # save key_id and patch dersig dersig = bytes.fromhex(dersig_hex_str) key_id = dersig[0] dsa_sig = dsa.Sig.parse(b"\x30" + dersig[1:]) # ECDSA signature verification of the patched dersig dsa.assert_as_valid(magic_msg, xprv, dsa_sig) assert dsa.verify(magic_msg, xprv, dsa_sig) # compressed address addr = b58.p2pkh(xprv) # equivalent Bitcoin Message Signature rec_flag = 27 + 4 + (key_id & 0x01) bms_sig = bms.Sig(rec_flag, dsa_sig) # Bitcoin Message Signature verification bms.assert_as_valid(msg, addr, bms_sig) assert bms.verify(msg, addr, bms_sig) assert not bms.verify(magic_msg, addr, bms_sig) bms.sign(msg, xprv) # standard leading 30 in DER serialization derivation_path = "m/0/0" msg_str = "hello world".encode() dersig_hex_str = "3045022100967dac3262b4686e89638c8219c5761017f05cd87a855edf034f4a3ec6b59d3d0220108a4ef9682b71a45979d8c75c393382d9ccb8eb561d73b8c5fc0b87a47e7d27" # pub_key derivation rprv = bip39.mxprv_from_mnemonic(mnemonic) xprv = bip32.derive(rprv, derivation_path) # the actual message being signed magic_msg = magic_message(msg_str) # save key_id and patch dersig dersig = bytes.fromhex(dersig_hex_str) key_id = dersig[0] dsa_sig = dsa.Sig.parse(b"\x30" + dersig[1:]) # ECDSA signature verification of the patched dersig dsa.assert_as_valid(magic_msg, xprv, dsa_sig, lower_s=True) assert dsa.verify(magic_msg, xprv, dsa_sig) # compressed address addr = b58.p2pkh(xprv) # equivalent Bitcoin Message Signature rec_flag = 27 + 4 + (key_id & 0x01) bms_sig = bms.Sig(rec_flag, dsa_sig) # Bitcoin Message Signature verification bms.assert_as_valid(msg_str, addr, bms_sig) assert bms.verify(msg_str, addr, bms_sig) assert not bms.verify(magic_msg, addr, bms_sig)