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_crack_prv_key() -> None: ec = CURVES["secp256k1"] q, _ = dsa.gen_keys(1) k = 1 + secrets.randbelow(ec.n - 1) msg1 = "Paolo is afraid of ephemeral random numbers".encode() m_1 = reduce_to_hlen(msg1) sig1 = dsa.sign_(m_1, q, k) msg2 = "and Paolo is right to be afraid".encode() m_2 = reduce_to_hlen(msg2) sig2 = dsa.sign_(m_2, q, k) q_cracked, k_cracked = dsa.crack_prv_key(msg1, sig1.serialize(), msg2, sig2) # if the lower_s convention has changed only one of s1 and s2 sig2 = dsa.Sig(sig2.r, ec.n - sig2.s) qc2, kc2 = dsa.crack_prv_key(msg1, sig1, msg2, sig2.serialize()) assert (q == q_cracked and k in (k_cracked, ec.n - k_cracked)) or ( q == qc2 and k in (kc2, ec.n - kc2)) with pytest.raises(BTClibValueError, match="not the same r in signatures"): dsa.crack_prv_key(msg1, sig1, msg2, dsa.Sig(16, sig1.s)) with pytest.raises(BTClibValueError, match="identical signatures"): dsa.crack_prv_key(msg1, sig1, msg1, sig1) a = ec._a # pylint: disable=protected-access b = ec._b # pylint: disable=protected-access alt_ec = Curve(ec.p, a, b, ec.double_aff(ec.G), ec.n, ec.cofactor) sig = dsa.Sig(sig1.r, sig1.s, alt_ec) with pytest.raises(BTClibValueError, match="not the same curve in signatures"): dsa.crack_prv_key(msg1, sig, msg2, sig2)
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_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 parse(cls: Type["Sig"], data: BinaryData, check_validity: bool = True) -> "Sig": stream = bytesio_from_binarydata(data) sig_bin = stream.read(_REQUIRED_LENGHT) if check_validity and len(sig_bin) != _REQUIRED_LENGHT: err_msg = f"invalid decoded length: {len(sig_bin)}" err_msg += f" instead of {_REQUIRED_LENGHT}" raise BTClibValueError(err_msg) rf = sig_bin[0] ec = secp256k1 n_size = ec.n_size r = int.from_bytes(sig_bin[1:1 + n_size], "big", signed=False) s = int.from_bytes(sig_bin[1 + n_size:1 + 2 * n_size], "big", signed=False) dsa_sig = dsa.Sig(r, s, ec, check_validity=False) return cls(rf, dsa_sig, check_validity)
def test_signature() -> None: msg = "Satoshi Nakamoto".encode() q, Q = dsa.gen_keys(0x1) sig = dsa.sign(msg, q) dsa.assert_as_valid(msg, Q, sig) assert dsa.verify(msg, Q, sig) assert sig == dsa.Sig.parse(sig.serialize()) assert sig == dsa.Sig.parse(sig.serialize().hex()) # https://bitcointalk.org/index.php?topic=285142.40 # Deterministic Usage of DSA and ECDSA (RFC 6979) r = 0x934B1EA10A4B3C1757E2B0C017D0B6143CE3C9A7E6A4A49860D7A6AB210EE3D8 s = 0x2442CE9D2B916064108014783E923EC36B49743E2FFA1C4496F01A512AAFD9E5 assert sig.r == r assert sig.s in (s, sig.ec.n - s) # malleability malleated_sig = dsa.Sig(sig.r, sig.ec.n - sig.s) assert dsa.verify(msg, Q, malleated_sig, lower_s=False) keys = dsa.recover_pub_keys(msg, sig) assert len(keys) == 2 assert Q in keys keys = dsa.recover_pub_keys(msg, sig.serialize()) assert len(keys) == 2 assert Q in keys msg_fake = "Craig Wright".encode() assert not dsa.verify(msg_fake, Q, sig) err_msg = "signature verification failed" with pytest.raises(BTClibRuntimeError, match=err_msg): dsa.assert_as_valid(msg_fake, Q, sig) _, Q_fake = dsa.gen_keys() assert not dsa.verify(msg, Q_fake, sig) err_msg = "signature verification failed" with pytest.raises(BTClibRuntimeError, match=err_msg): dsa.assert_as_valid(msg, Q_fake, sig) err_msg = "not a valid public key: " with pytest.raises(BTClibValueError, match=err_msg): dsa.assert_as_valid(msg, INF, sig) sig_invalid = dsa.Sig(sig.ec.p, sig.s, check_validity=False) assert not dsa.verify(msg, Q, sig_invalid) err_msg = "scalar r not in 1..n-1: " with pytest.raises(BTClibValueError, match=err_msg): dsa.assert_as_valid(msg, Q, sig_invalid) sig_invalid = dsa.Sig(sig.r, sig.ec.p, check_validity=False) assert not dsa.verify(msg, Q, sig_invalid) err_msg = "scalar s not in 1..n-1: " with pytest.raises(BTClibValueError, match=err_msg): dsa.assert_as_valid(msg, Q, sig_invalid) err_msg = "private key not in 1..n-1: " with pytest.raises(BTClibValueError, match=err_msg): dsa.sign(msg, 0) # ephemeral key not in 1..n-1 err_msg = "private key not in 1..n-1: " with pytest.raises(BTClibValueError, match=err_msg): dsa.sign_(reduce_to_hlen(msg), q, 0) with pytest.raises(BTClibValueError, match=err_msg): dsa.sign_(reduce_to_hlen(msg), q, sig.ec.n)