def point_from_bip340pub_key(x_Q: BIP340PubKey, ec: Curve = secp256k1) -> Point: """Return a verified-as-valid BIP340 public key as Point tuple. It supports: - BIP32 extended keys (bytes, string, or BIP32KeyData) - SEC Octets (bytes or hex-string, with 02, 03, or 04 prefix) - BIP340 Octets (bytes or hex-string, p-size Point x-coordinate) - native tuple """ # BIP 340 key as integer if isinstance(x_Q, int): return x_Q, ec.y_even(x_Q) # (tuple) Point, (dict or str) BIP32Key, or 33/65 bytes try: x_Q = point_from_pub_key(x_Q, ec)[0] return x_Q, ec.y_even(x_Q) except BTClibValueError: pass # BIP 340 key as bytes or hex-string if isinstance(x_Q, (str, bytes)): Q = bytes_from_octets(x_Q, ec.p_size) x_Q = int.from_bytes(Q, "big", signed=False) return x_Q, ec.y_even(x_Q) raise BTClibTypeError("not a BIP340 public key")
def _recover_pub_key_(c: int, r: int, s: int, ec: Curve) -> int: # Private function provided for testing purposes only. if c == 0: raise BTClibRuntimeError("invalid zero challenge") KJ = r, ec.y_even(r), 1 e1 = mod_inv(c, ec.n) QJ = _double_mult(ec.n - e1, KJ, e1 * s, ec.GJ, ec) # edge case that cannot be reproduced in the test suite if QJ[2] == 0: err_msg = "invalid (INF) key" # pragma: no cover raise BTClibRuntimeError(err_msg) # pragma: no cover return ec.x_aff_from_jac(QJ)
def gen_keys_(prv_key: Optional[PrvKey] = None, ec: Curve = secp256k1) -> Tuple[int, int, JacPoint]: "Return a BIP340 private/public (int, JacPoint) key-pair." if prv_key is None: q = 1 + secrets.randbelow(ec.n - 1) else: q = int_from_prv_key(prv_key, ec) QJ = _mult(q, ec.GJ, ec) x_Q, y_Q = ec.aff_from_jac(QJ) if y_Q % 2: q = ec.n - q QJ = ec.negate_jac(QJ) return q, x_Q, QJ
def second_generator(ec: Curve = secp256k1, hf: HashF = sha256) -> Point: """Second (with respect to G) elliptic curve generator. Second (with respect to G) Nothing-Up-My-Sleeve (NUMS) elliptic curve generator. The hash of G is coerced it to a point (x_H, y_H). If the resulting point is not on the curve, keep on incrementing x_H until a valid curve point (x_H, y_H) is obtained. idea: https://crypto.stackexchange.com/questions/25581/second-generator-for-secp256k1-curve source: https://github.com/ElementsProject/secp256k1-zkp/blob/secp256k1-zkp/src/modules/rangeproof/main_impl.h """ G_bytes = bytes_from_point(ec.G, ec, compressed=False) hash_ = hf() hash_.update(G_bytes) hash_digest = hash_.digest() x_H = int_from_bits(hash_digest, ec.nlen) % ec.n while True: try: y_H = ec.y_even(x_H) return x_H, y_H except BTClibValueError: x_H += 1 x_H %= ec.p
def point_from_octets(pub_key: Octets, ec: Curve = secp256k1) -> Point: """Return a tuple (x_Q, y_Q) that belongs to the curve. Return a tuple (x_Q, y_Q) that belongs to the curve according to SEC 1 v.2, section 2.3.4. """ pub_key = bytes_from_octets(pub_key, (ec.p_size + 1, 2 * ec.p_size + 1)) bsize = len(pub_key) # bytes if pub_key[0] in (0x02, 0x03): # compressed point if bsize != ec.p_size + 1: err_msg = "invalid size for compressed point: " err_msg += f"{bsize} instead of {ec.p_size + 1}" raise BTClibValueError(err_msg) x_Q = int.from_bytes(pub_key[1:], byteorder="big") try: y_Q = ec.y_even(x_Q) # also check x_Q validity return x_Q, y_Q if pub_key[0] == 0x02 else ec.p - y_Q except BTClibValueError as e: msg = f"invalid x-coordinate: '{hex_string(x_Q)}'" raise BTClibValueError(msg) from e elif pub_key[0] == 0x04: # uncompressed point if bsize != 2 * ec.p_size + 1: err_msg = "invalid size for uncompressed point: " err_msg += f"{bsize} instead of {2 * ec.p_size + 1}" raise BTClibValueError(err_msg) x_Q = int.from_bytes(pub_key[1:ec.p_size + 1], byteorder="big", signed=False) Q = x_Q, int.from_bytes(pub_key[ec.p_size + 1:], byteorder="big", signed=False) if Q[1] == 0: # infinity point in affine coordinates raise BTClibValueError( "no bytes representation for infinity point") if ec.is_on_curve(Q): return Q raise BTClibValueError(f"point not on curve: {Q}") else: raise BTClibValueError(f"not a point: {pub_key!r}")
def bytes_from_point(Q: Point, ec: Curve = secp256k1, compressed: bool = True) -> bytes: """Return a point as compressed/uncompressed octet sequence. Return a point as compressed (0x02, 0x03) or uncompressed (0x04) octet sequence, according to SEC 1 v.2, section 2.3.3. """ # check that Q is a point and that is on curve ec.require_on_curve(Q) if Q[1] == 0: # infinity point in affine coordinates raise BTClibValueError("no bytes representation for infinity point") bytes_ = Q[0].to_bytes(ec.p_size, byteorder="big", signed=False) if compressed: return (b"\x03" if (Q[1] & 1) else b"\x02") + bytes_ return b"\x04" + bytes_ + Q[1].to_bytes( ec.p_size, byteorder="big", signed=False)
def gen_keys(prv_key: Optional[PrvKey] = None, ec: Curve = secp256k1) -> Tuple[int, Point]: "Return a private/public (int, Point) key-pair." if prv_key is None: # q in the range [1, ec.n-1] q = 1 + secrets.randbelow(ec.n - 1) else: q = int_from_prv_key(prv_key, ec) QJ = _mult(q, ec.GJ, ec) Q = ec.aff_from_jac(QJ) return q, Q
def output_pubkey( internal_pubkey: Optional[Key] = None, script_tree: Optional[TaprootScriptTree] = None, ec: Curve = secp256k1, ) -> Tuple[bytes, int]: if not internal_pubkey and not script_tree: raise BTClibValueError("Missing data") if internal_pubkey: pubkey = pub_keyinfo_from_key(internal_pubkey, compressed=True)[0][1:] else: h_str = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" pubkey = bytes.fromhex(h_str) if script_tree: _, h = tree_helper(script_tree) else: h = tagged_hash(b"TapTweak", pubkey) t = int.from_bytes(tagged_hash(b"TapTweak", pubkey + h), "big") # edge case that cannot be reproduced in the test suite if t >= ec.n: raise BTClibValueError("Invalid script tree hash") # pragma: no cover x = int.from_bytes(pubkey, "big") Q = ec.add((x, ec.y_even(x)), mult(t)) return Q[0].to_bytes(32, "big"), Q[1] % 2
def _assert_as_valid_(c: int, QJ: JacPoint, r: int, s: int, ec: Curve) -> None: # Private function for test/dev purposes # It raises Errors, while verify should always return True or False # Let K = sG - eQ. # in Jacobian coordinates KJ = _double_mult(ec.n - c, QJ, s, ec.GJ, ec) # Fail if infinite(KJ). # Fail if y_K is odd. if ec.y_aff_from_jac(KJ) % 2: raise BTClibRuntimeError("y_K is odd") # Fail if x_K ≠ r if KJ[0] != KJ[2] * KJ[2] * r % ec.p: raise BTClibRuntimeError("signature verification failed")
def output_prvkey( internal_prvkey: PrvKey, script_tree: Optional[TaprootScriptTree] = None, ec: Curve = secp256k1, ) -> int: internal_prvkey = int_from_prv_key(internal_prvkey) P = mult(internal_prvkey) if script_tree: _, h = tree_helper(script_tree) else: h = b"" has_even_y = ec.y_even(P[0]) == P[1] internal_prvkey = internal_prvkey if has_even_y else ec.n - internal_prvkey t = int.from_bytes(tagged_hash(b"TapTweak", P[0].to_bytes(32, "big") + h), "big") # edge case that cannot be reproduced in the test suite if t >= ec.n: raise BTClibValueError("Invalid script tree hash") # pragma: no cover return (internal_prvkey + t) % ec.n
def point_from_pub_key(pub_key: PubKey, ec: Curve = secp256k1) -> Point: "Return an elliptic curve point tuple from a public key." if isinstance(pub_key, tuple): if ec.is_on_curve(pub_key) and pub_key[1] != 0: return pub_key raise BTClibValueError(f"not a valid public key: {pub_key}") if isinstance(pub_key, BIP32KeyData): return _point_from_xpub(pub_key, ec) try: return _point_from_xpub(pub_key, ec) except (TypeError, BTClibValueError): pass # it must be octets try: return point_from_octets(pub_key, ec) except (TypeError, ValueError) as e: raise BTClibValueError(f"not a public key: {pub_key!r}") from e
def _recover_pub_keys_(c: int, r: int, s: int, lower_s: bool, ec: Curve) -> List[JacPoint]: # Private function provided for testing purposes only. # precomputations r_1 = mod_inv(r, ec.n) r1s = r_1 * s % ec.n r1e = -r_1 * c % ec.n keys: List[JacPoint] = [] # r = K[0] % ec.n # if ec.n < K[0] < ec.p (likely when cofactor ec.cofactor > 1) # then both x_K=r and x_K=r+ec.n must be tested for j in range(ec.cofactor + 1): # 1 # affine x_K-coordinate of K (field element) x_K = (r + j * ec.n) % ec.p # 1.1 # two possible y_K-coordinates, i.e. two possible keys for each cycle try: # even root first for bitcoin message signing compatibility yodd = ec.y_even(x_K) KJ = x_K, yodd, 1 # 1.2, 1.3, and 1.4 # 1.5 has been performed in the recover_pub_keys calling function QJ = _double_mult(r1s, KJ, r1e, ec.GJ, ec) # 1.6.1 try: _assert_as_valid_(c, QJ, r, s, lower_s, ec) # 1.6.2 except (BTClibValueError, BTClibRuntimeError): pass else: keys.append(QJ) # 1.6.2 KJ = x_K, ec.p - yodd, 1 # 1.6.3 QJ = _double_mult(r1s, KJ, r1e, ec.GJ, ec) try: _assert_as_valid_(c, QJ, r, s, lower_s, ec) # 1.6.2 except (BTClibValueError, BTClibRuntimeError): pass else: keys.append(QJ) # 1.6.2 except (BTClibValueError, BTClibRuntimeError): # K is not a curve point pass return keys
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 _recover_pub_key_(key_id: int, c: int, r: int, s: int, lower_s: bool, ec: Curve) -> JacPoint: # Private function provided for testing purposes only. # precomputations r_1 = mod_inv(r, ec.n) r1s = r_1 * s % ec.n r1e = -r_1 * c % ec.n # r = K[0] % ec.n # if ec.n < K[0] < ec.p (likely when cofactor ec.cofactor > 1) # then both x_K=r and x_K=r+ec.n must be tested j = key_id & 0b110 # allow for key_id in [0, 7] x_K = (r + j * ec.n) % ec.p # 1.1 # even root first for Bitcoin Core compatibility i = key_id & 0b01 y_even = ec.y_even(x_K) y_K = ec.p - y_even if i else y_even KJ = x_K, y_K, 1 # 1.2, 1.3, and 1.4 # 1.5 has been performed in the recover_pub_keys calling function QJ = _double_mult(r1s, KJ, r1e, ec.GJ, ec) # 1.6.1 _assert_as_valid_(c, QJ, r, s, lower_s, ec) # 1.6.2 return QJ
def test_exceptions() -> None: # good curve Curve(13, 0, 2, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="p is not prime: "): Curve(15, 0, 2, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="negative a: "): Curve(13, -1, 2, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="p <= a: "): Curve(13, 13, 2, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="negative b: "): Curve(13, 0, -2, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="p <= b: "): Curve(13, 0, 13, (1, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="zero discriminant"): Curve(11, 7, 7, (1, 9), 19, 1, False) err_msg = "Generator must a be a sequence\\[int, int\\]" with pytest.raises(BTClibValueError, match=err_msg): Curve(13, 0, 2, (1, 9, 1), 19, 1, False) # type: ignore with pytest.raises(BTClibValueError, match="Generator is not on the curve"): Curve(13, 0, 2, (2, 9), 19, 1, False) with pytest.raises(BTClibValueError, match="n is not prime: "): Curve(13, 0, 2, (1, 9), 20, 1, False) with pytest.raises(BTClibValueError, match="n not in "): Curve(13, 0, 2, (1, 9), 71, 1, False) with pytest.raises(BTClibValueError, match="INF point cannot be a generator"): Curve(13, 0, 2, INF, 19, 1, False) with pytest.raises(BTClibValueError, match="n is not the group order: "): Curve(13, 0, 2, (1, 9), 17, 1, False) with pytest.raises(BTClibValueError, match="invalid cofactor: "): Curve(13, 0, 2, (1, 9), 19, 2, False) # n=p -> weak curve # missing with pytest.raises(UserWarning, match="weak curve"): Curve(11, 2, 7, (6, 9), 7, 2, True)
import pytest from btclib.alias import INF, INFJ from btclib.ecc.curve import CURVES, Curve, double_mult, mult, multi_mult, secp256k1 from btclib.ecc.curve_group import jac_from_aff from btclib.ecc.number_theory import mod_sqrt from btclib.ecc.pedersen import second_generator from btclib.exceptions import BTClibTypeError, BTClibValueError # FIXME Curve repr should use "dedbeef 00000000", not "0xdedbeef00000000" # FIXME test curves when n>p # test curves: very low cardinality low_card_curves: Dict[str, Curve] = {} # 13 % 4 = 1; 13 % 8 = 5 low_card_curves["ec13_11"] = Curve(13, 7, 6, (1, 1), 11, 1, False) low_card_curves["ec13_19"] = Curve(13, 0, 2, (1, 9), 19, 1, False) # 17 % 4 = 1; 17 % 8 = 1 low_card_curves["ec17_13"] = Curve(17, 6, 8, (0, 12), 13, 2, False) low_card_curves["ec17_23"] = Curve(17, 3, 5, (1, 14), 23, 1, False) # 19 % 4 = 3; 19 % 8 = 3 low_card_curves["ec19_13"] = Curve(19, 0, 2, (4, 16), 13, 2, False) low_card_curves["ec19_23"] = Curve(19, 2, 9, (0, 16), 23, 1, False) # 23 % 4 = 3; 23 % 8 = 7 low_card_curves["ec23_19"] = Curve(23, 9, 7, (5, 4), 19, 1, False) low_card_curves["ec23_31"] = Curve(23, 5, 1, (0, 1), 31, 1, False) all_curves: Dict[str, Curve] = {} all_curves.update(low_card_curves) all_curves.update(CURVES)
from btclib.ecc.curve import Curve # low cardinality curves p<100 ec11_7 = Curve(11, 2, 7, (6, 9), 7, 2, False) ec11_17 = Curve(11, 2, 4, (0, 9), 17, 1, False) ec13_11 = Curve(13, 7, 6, (1, 1), 11, 1, False) ec13_19 = Curve(13, 0, 2, (1, 9), 19, 1, False) ec17_13 = Curve(17, 6, 8, (0, 12), 13, 2, False) ec17_23 = Curve(17, 3, 5, (1, 14), 23, 1, False) ec19_13 = Curve(19, 0, 2, (4, 16), 13, 2, False) ec19_23 = Curve(19, 2, 9, (0, 16), 23, 1, False) ec23_19 = Curve(23, 9, 7, (5, 4), 19, 1, False) ec23_31 = Curve(23, 5, 1, (0, 1), 31, 1, False) ec29_37 = Curve(29, 4, 9, (0, 26), 37, 1, False) ec31_23 = Curve(31, 4, 7, (0, 10), 23, 1, False) ec31_43 = Curve(31, 0, 3, (1, 2), 43, 1, False) ec37_31 = Curve(37, 2, 8, (1, 23), 31, 1, False) ec37_43 = Curve(37, 2, 9, (0, 34), 43, 1, False) ec41_37 = Curve(41, 2, 6, (1, 38), 37, 1, False) ec41_53 = Curve(41, 4, 4, (0, 2), 53, 1, False) ec43_37 = Curve(43, 1, 5, (2, 31), 37, 1, False) ec43_47 = Curve(43, 1, 3, (2, 23), 47, 1, False) ec47_41 = Curve(47, 3, 9, (0, 3), 41, 1, False) ec47_61 = Curve(47, 3, 5, (1, 3), 61, 1, False) ec53_47 = Curve(53, 9, 4, (0, 51), 47, 1, False) ec53_61 = Curve(53, 1, 8, (1, 13), 61, 1, False) ec59_53 = Curve(59, 9, 3, (0, 48), 53, 1, False) ec59_73 = Curve(59, 3, 3, (0, 48), 73, 1, False) ec61_59 = Curve(61, 2, 5, (0, 35), 59, 1, False) ec61_73 = Curve(61, 1, 9, (0, 58), 73, 1, False) ec67_61 = Curve(67, 3, 8, (2, 25), 61, 1, False)