def test_msgsign_p2pkh() -> None: msg = "test message".encode() # sigs are taken from (Electrum and) Bitcoin Core q = "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" # uncompressed wif1u = b58.wif_from_prv_key(q, "mainnet", False) assert wif1u == "5KMWWy2d3Mjc8LojNoj8Lcz9B1aWu8bRofUgGwQk959Dw5h2iyw" add1u = b58.p2pkh(wif1u) assert add1u == "1HUBHMij46Hae75JPdWjeZ5Q7KaL7EFRSD" bms_sig1u = bms.sign(msg, wif1u) assert bms.verify(msg, add1u, bms_sig1u) assert bms_sig1u.rf == 27 exp_sig1u = "G/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=" assert bms_sig1u.b64encode() == exp_sig1u # compressed wif1c = b58.wif_from_prv_key(q, "mainnet", True) assert wif1c == "L41XHGJA5QX43QRG3FEwPbqD5BYvy6WxUxqAMM9oQdHJ5FcRHcGk" add1c = b58.p2pkh(wif1c) assert add1c == "14dD6ygPi5WXdwwBTt1FBZK3aD8uDem1FY" bms_sig1c = bms.sign(msg, wif1c) assert bms.verify(msg, add1c, bms_sig1c) assert bms_sig1c.rf == 31 exp_sig1c = "H/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=" assert bms_sig1c.b64encode() == exp_sig1c assert not bms.verify(msg, add1c, bms_sig1u) assert not bms.verify(msg, add1u, bms_sig1c) bms_sig = bms.Sig(bms_sig1c.rf + 1, bms_sig1c.dsa_sig) assert not bms.verify(msg, add1c, bms_sig) # malleate s s = ec.n - bms_sig1c.dsa_sig.s dsa_sig = dsa.Sig(bms_sig1c.dsa_sig.r, s, bms_sig1c.dsa_sig.ec) # without updating rf verification will fail, even with lower_s=False bms_sig = bms.Sig(bms_sig1c.rf, dsa_sig) assert not bms.verify(msg, add1c, bms_sig, lower_s=False) # update rf to satisfy above malleation i = 1 if bms_sig1c.rf % 2 else -1 bms_sig = bms.Sig(bms_sig1c.rf + i, dsa_sig) assert bms.verify(msg, add1c, bms_sig, lower_s=False) # anyway, with lower_s=True malleation does fail verification err_msg = "not a low s" with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, add1c, bms_sig, lower_s=True)
def test_signature() -> None: msg = "test message".encode() wif, addr = bms.gen_keys() bms_sig = bms.sign(msg, wif) bms.assert_as_valid(msg, addr, bms_sig) assert bms.verify(msg, addr, bms_sig) assert bms_sig == bms.Sig.parse(bms_sig.serialize()) assert bms_sig == bms.Sig.parse(bms_sig.serialize().hex()) assert bms_sig == bms.Sig.b64decode(bms_sig.b64encode()) assert bms_sig == bms.Sig.b64decode(bms_sig.b64encode().encode("ascii")) assert bms_sig == bms.sign(msg, wif.encode("ascii")) # malleated signature dsa_sig = dsa.Sig(bms_sig.dsa_sig.r, bms_sig.dsa_sig.ec.n - bms_sig.dsa_sig.s) # without updating rf verification will fail, even with lower_s=False bms_sig = bms.Sig(bms_sig.rf, dsa_sig) err_msg = "invalid p2pkh address: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, addr, bms_sig, lower_s=False) # update rf to satisfy above malleation i = 1 if bms_sig.rf % 2 else -1 bms_sig = bms.Sig(bms_sig.rf + i, dsa_sig) bms.assert_as_valid(msg, addr, bms_sig, lower_s=False) assert bms.verify(msg, addr, bms_sig, lower_s=False) # anyway, with lower_s=True malleation does fail verification err_msg = "not a low s" with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, addr, bms_sig, lower_s=True) # bms_sig taken from (Electrum and) Bitcoin Core wif, addr = bms.gen_keys( "5KMWWy2d3Mjc8LojNoj8Lcz9B1aWu8bRofUgGwQk959Dw5h2iyw") bms_sig = bms.sign(msg, wif) bms.assert_as_valid(msg, addr, bms_sig) assert bms.verify(msg, addr, bms_sig) exp_sig = "G/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=" assert bms_sig.b64encode() == exp_sig bms.assert_as_valid(msg, addr, exp_sig) bms.assert_as_valid(msg, addr, exp_sig.encode("ascii")) dsa_sig = dsa.Sig(bms_sig.dsa_sig.r, bms_sig.dsa_sig.s, CURVES["secp256r1"]) err_msg = "invalid curve: " with pytest.raises(BTClibValueError, match=err_msg): bms_sig = bms.Sig(bms_sig.rf, dsa_sig)
def test_vector_python_bitcoinlib() -> None: """Test python-bitcoinlib test vectors https://github.com/petertodd/python-bitcoinlib/blob/master/bitcoin/tests/test_data/bms.json """ fname = "bms.json" filename = path.join(path.dirname(__file__), "_data", fname) with open(filename, "r") as file_: test_vectors = json.load(file_) for vector in test_vectors[:10]: msg = vector["address"].encode() # btclib self-consistency check bms_sig = bms.sign(msg, vector["wif"]) assert bms.verify(msg, vector["address"], bms_sig) bms_sig_encoded = bms_sig.b64encode() assert bms.verify(msg, vector["address"], bms_sig_encoded) # Core/Electrum/btclib provide identical signature # they use "low-s" canonical signature assert bms_sig.dsa_sig.s < ec.n - bms_sig.dsa_sig.s assert bms.verify(msg, vector["address"], bms_sig_encoded, lower_s=True) # python-bitcoinlib provides a valid signature # but does not respect low-s assert bms.verify(msg, vector["address"], vector["signature"], lower_s=False) # python-bitcoinlib has a signature different from Core/Electrum/btclib assert bms_sig_encoded != vector["signature"] # but the reason is not the low-s # here's the malleated Core/Electrum/btclib signature s = ec.n - bms_sig.dsa_sig.s dsa_sig = dsa.Sig(bms_sig.dsa_sig.r, s, bms_sig.dsa_sig.ec) # properly malleated fixing also rf i = 1 if bms_sig.rf % 2 else -1 bms_sig_malleated = bms.Sig(bms_sig.rf + i, dsa_sig) assert bms.verify(msg, vector["address"], bms_sig_malleated, lower_s=False) bms_sig_encoded = bms_sig_malleated.b64encode() assert bms.verify(msg, vector["address"], bms_sig_encoded, lower_s=False) # the malleated signature is still not equal to the python-bitcoinlib one assert bms_sig_encoded != vector["signature"] # python-bitcoinlib does not use RFC6979 deterministic nonce # as proved by different r compared to Core/Electrum/btclib test_vector_sig = bms.Sig.b64decode(vector["signature"]) assert bms_sig.dsa_sig.r != test_vector_sig.dsa_sig.r
def test_exceptions() -> None: msg = "test".encode() wif = "KwELaABegYxcKApCb3kJR9ymecfZZskL9BzVUkQhsqFiUKftb4tu" address = b58.p2pkh(wif) exp_sig = "IHdKsFF1bUrapA8GMoQUbgI+Ad0ZXyX1c/yAZHmJn5hSNBi7J+TrI1615FG3g9JEOPGVvcfDWIFWrg2exLNtoVc=" bms.assert_as_valid(msg, address, exp_sig) bms_sig = bms.Sig.b64decode(exp_sig) err_msg = "not a p2wpkh address: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, b32.p2wsh(32 * b"\x00"), exp_sig) err_msg = "invalid recovery flag: " with pytest.raises(BTClibValueError, match=err_msg): bms.Sig(26, bms_sig.dsa_sig) exp_sig = "IHdKsFF1bUrapA8GMoQUbgI+Ad0ZXyX1c/yAZHmJn5hNBi7J+TrI1615FG3g9JEOPGVvcfDWIFWrg2exLoVc=" err_msg = "invalid decoded length: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, address, exp_sig) assert not bms.verify(msg, address, exp_sig) exp_sig = "GpNLHqEKSzwXV+KwwBfQthQ848mn5qSkmGDXpqshDuPYJELOnSuRYGQQgBR4PpI+w2tJdD4v+hxElvAaUSqv2eU=" err_msg = "invalid recovery flag: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, address, exp_sig) assert not bms.verify(msg, address, exp_sig) exp_sig = "QpNLHqEKSzwXV+KwwBfQthQ848mn5qSkmGDXpqshDuPYJELOnSuRYGQQgBR4PpI+w2tJdD4v+hxElvAaUSqv2eU=" with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, address, exp_sig) assert not bms.verify(msg, address, exp_sig) # compressed wif, uncompressed address wif = "Ky1XfDK2v6wHPazA6ECaD8UctEoShXdchgABjpU9GWGZDxVRDBMJ" address = "19f7adDYqhHSJm2v7igFWZAqxXHj1vUa3T" err_msg = "mismatch between private key and address" with pytest.raises(BTClibValueError, match=err_msg): bms.sign(msg, wif, address) # uncompressed wif, compressed address wif = "5JDopdKaxz5bXVYXcAnfno6oeSL8dpipxtU1AhfKe3Z58X48srn" address = "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5" err_msg = "not a private or compressed public key for mainnet: " # FIXME puzzling error message with pytest.raises(BTClibValueError, match=err_msg): bms.sign(msg, wif, address) msg = "test".encode() wif = "L4xAvhKR35zFcamyHME2ZHfhw5DEyeJvEMovQHQ7DttPTM8NLWCK" b58_p2pkh = b58.p2pkh(wif) b32_p2wpkh = b32.p2wpkh(wif) b58_p2wpkh_p2sh = b58.p2wpkh_p2sh(wif) wif = "Ky1XfDK2v6wHPazA6ECaD8UctEoShXdchgABjpU9GWGZDxVRDBMJ" err_msg = "mismatch between private key and address" with pytest.raises(BTClibValueError, match=err_msg): bms.sign(msg, wif, b58_p2pkh) with pytest.raises(BTClibValueError, match=err_msg): bms.sign(msg, wif, b32_p2wpkh) with pytest.raises(BTClibValueError, match=err_msg): bms.sign(msg, wif, b58_p2wpkh_p2sh) # Invalid recovery flag (39) for base58 p2pkh address exp_sig = "IHdKsFF1bUrapA8GMoQUbgI+Ad0ZXyX1c/yAZHmJn5hSNBi7J+TrI1615FG3g9JEOPGVvcfDWIFWrg2exLNtoVc=" bms_sig = bms.Sig.b64decode(exp_sig) bms_sig = bms.Sig(39, bms_sig.dsa_sig, check_validity=False) sig_encoded = bms_sig.b64encode(check_validity=False) err_msg = "invalid p2pkh address recovery flag: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, b58_p2pkh, sig_encoded) # Invalid recovery flag (35) for bech32 p2wpkh address exp_sig = "IBFyn+h9m3pWYbB4fBFKlRzBD4eJKojgCIZSNdhLKKHPSV2/WkeV7R7IOI0dpo3uGAEpCz9eepXLrA5kF35MXuU=" bms_sig = bms.Sig.b64decode(exp_sig) bms_sig = bms.Sig(35, bms_sig.dsa_sig, check_validity=False) err_msg = "invalid p2wpkh address recovery flag: " with pytest.raises(BTClibValueError, match=err_msg): bms.assert_as_valid(msg, b32_p2wpkh, bms_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)