def crack_prv_key_( msg_hash1: Octets, sig1: Union[Sig, Octets], msg_hash2: Octets, sig2: Union[Sig, Octets], hf: HashF = sha256, ) -> Tuple[int, int]: if isinstance(sig1, Sig): sig1.assert_valid() else: sig1 = Sig.parse(sig1) if isinstance(sig2, Sig): sig2.assert_valid() else: sig2 = Sig.parse(sig2) ec = sig2.ec if sig1.ec != ec: raise BTClibValueError("not the same curve in signatures") if sig1.r != sig2.r: raise BTClibValueError("not the same r in signatures") if sig1.s == sig2.s: raise BTClibValueError("identical signatures") c_1 = challenge_(msg_hash1, ec, hf) c_2 = challenge_(msg_hash2, ec, hf) nonce = (c_1 - c_2) * mod_inv(sig1.s - sig2.s, ec.n) % ec.n q = (sig2.s * nonce - c_2) * mod_inv(sig1.r, ec.n) % ec.n return q, nonce
def recover_pub_key_( key_id: int, msg_hash: Octets, sig: Union[Sig, Octets], lower_s: bool = True, hf: HashF = sha256, ) -> Point: """ECDSA public key recovery (SEC 1 v.2 section 4.1.6). See also: https://crypto.stackexchange.com/questions/18105/how-does-recovering-the-public-key-from-an-ecdsa-signature-work/18106#18106 """ if isinstance(sig, Sig): sig.assert_valid() else: sig = Sig.parse(sig) # The message msg_hash: a hf_len array hf_len = hf().digest_size msg_hash = bytes_from_octets(msg_hash, hf_len) c = challenge_(msg_hash, sig.ec, hf) # 1.5 QJ = _recover_pub_key_(key_id, c, sig.r, sig.s, lower_s, sig.ec) return sig.ec.aff_from_jac(QJ)
def test_der_size() -> None: sig8 = 1, 1 sig72 = ec.n - 2, ec.n - 1 sig71 = 2**255 - 4, ec.n - 1 sig70 = 2**255 - 4, 2**255 - 1 sig70b = 2**255 - 4, 2**248 - 1 sig69 = 2**255 - 4, 2**247 - 1 sig68 = 2**247 - 1, 2**247 - 1 sigs = [sig8, sig72, sig71, sig70, sig70b, sig69, sig68] lenghts = [8, 72, 71, 70, 70, 69, 68] for length, (r, s) in zip(lenghts, sigs): sig = Sig(r, s) assert r == sig.r assert s == sig.s assert ec == sig.ec sig_bin = sig.serialize() assert len(sig_bin) == length assert sig == Sig.parse(sig_bin)
def assert_as_valid_( msg_hash: Octets, key: Key, sig: Union[Sig, Octets], lower_s: bool = True, hf: HashF = sha256, ) -> 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.parse(sig) c = challenge_(msg_hash, sig.ec, hf) # 2, 3 Q = point_from_key(key, sig.ec) QJ = Q[0], Q[1], 1 # second part delegated to helper function _assert_as_valid_(c, QJ, sig.r, sig.s, lower_s, sig.ec)
print("** Verify malleated signature") print(verify(msg1, Q, Sig(sig1.r, sm))) print(verify(msg1, Q, Sig(sig1.r, sm), False)) print("\n1. Another message to sign") msg2 = "and Paolo is right to be afraid".encode() print(msg2.decode()) print("2. Sign message") sig2 = sign(msg2, q) print(f" r2: {hex(sig2.r).upper()}") print(f" s2: {hex(sig2.s).upper()}") print("3. Verify signature") print(verify(msg2, Q, sig2)) print("4. Recover keys") keys = recover_pub_keys(msg2, sig2) for i, key in enumerate(keys): print( f" key#{i}: {'02' if key[1] % 2 == 0 else '03'} {hex(key[0]).upper()}") print("\n** Serialize signature") dersig = sig2.serialize() print(" bytes:", dersig) print("hex-string:", dersig.hex().upper()) sig3 = Sig.parse(dersig) if sig2.r == sig3.r and sig2.s == sig3.s: print("Succesfully parsed!")
def test_der_deserialize() -> None: err_msg = "non-hexadecimal number found " with pytest.raises(ValueError, match=err_msg): Sig.parse("not a sig") sig = Sig(2**255 - 4, 2**247 - 1) sig_bin = sig.serialize() r_size = sig_bin[3] bad_sig_bin = b"\x31" + sig_bin[1:] err_msg = "invalid compound header: " with pytest.raises(BTClibValueError, match=err_msg): Sig.parse(bad_sig_bin) bad_sig_bin = sig_bin[:1] + b"\x41" + sig_bin[2:] err_msg = "not enough binary data" with pytest.raises(BTClibRuntimeError, match=err_msg): Sig.parse(bad_sig_bin) # r and s scalars for offset in (4, 6 + r_size): bad_sig_bin = sig_bin[:offset - 2] + b"\x00" + sig_bin[offset - 1:] err_msg = "invalid value header: " with pytest.raises(BTClibValueError, match=err_msg): Sig.parse(bad_sig_bin) bad_sig_bin = sig_bin[:offset - 1] + b"\x00" + sig_bin[offset:] err_msg = "zero size" with pytest.raises(BTClibRuntimeError, match=err_msg): Sig.parse(bad_sig_bin) bad_sig_bin = sig_bin[:offset - 1] + b"\x80" + sig_bin[offset:] err_msg = "not enough binary data" with pytest.raises(BTClibRuntimeError, match=err_msg): Sig.parse(bad_sig_bin) bad_sig_bin = sig_bin[:offset] + b"\x80" + sig_bin[offset + 1:] err_msg = "invalid negative scalar" with pytest.raises(BTClibValueError, match=err_msg): Sig.parse(bad_sig_bin) bad_sig_bin = sig_bin[:offset] + b"\x00\x7f" + sig_bin[offset + 2:] err_msg = "invalid 'highest bit set' padding" with pytest.raises(BTClibValueError, match=err_msg): Sig.parse(bad_sig_bin) data_size = sig_bin[1] malleated_size = (data_size + 1).to_bytes(1, byteorder="big", signed=False) bad_sig_bin = sig_bin[:1] + malleated_size + sig_bin[2:] + b"\x01" err_msg = "invalid DER sequence length" with pytest.raises(BTClibValueError, match=err_msg): Sig.parse(bad_sig_bin)