def test_sign_schnorr(self): pk = PrivateKey(randint(1, N)) msg = int_to_big_endian(randint(1, N), 32) sig = pk.sign_schnorr(msg, aux=b"\x00" * 32) self.assertTrue(pk.point.verify_schnorr(msg, sig)) # tweak tweak = randint(1, N) tweak_point = pk.tweaked(tweak).point k = randint(1, N) r = k * G if r.parity: k = N - k r = k * G message = r.bip340() + tweak_point.bip340() + msg challenge = big_endian_to_int(hash_challenge(message)) % N if pk.point.parity == tweak_point.parity: secret = pk.secret else: secret = -pk.secret s = (k + challenge * secret) % N if tweak_point.parity: s = (s - challenge * tweak) % N else: s = (s + challenge * tweak) % N sig = SchnorrSignature.parse(r.bip340() + int_to_big_endian(s, 32)) self.assertTrue(tweak_point.verify_schnorr(msg, sig))
def sec(self, compressed=True): # returns the binary version of the sec format, NOT hex # if compressed, starts with b'\x02' if self.y.num is even, b'\x03' if self.y is odd # then self.x.num # remember, you have to convert self.x.num/self.y.num to binary using int_to_big_endian x = int_to_big_endian(self.x.num, 32) if compressed: if self.parity: return b"\x03" + x else: return b"\x02" + x else: # if non-compressed, starts with b'\x04' followod by self.x and then self.y y = int_to_big_endian(self.y.num, 32) return b"\x04" + x + y
def decode_bech32(s): """Returns network, segwit version and the hash from the bech32 address""" regtest_prefix = PREFIX["regtest"] if s.startswith(regtest_prefix): hrp, raw_data = regtest_prefix, s[5:] else: hrp, raw_data = s.split("1") network = NET_FOR_PREFIX.get(hrp) if not network: raise ValueError(f"unknown human readable part: {hrp}") data = [BECH32_ALPHABET.index(c) for c in raw_data] version = data[0] verify_fnc = bech32_verify_checksum if version == 0 else bech32m_verify_checksum if not verify_fnc(hrp, data): raise ValueError(f"bad address: {s}") number = 0 for digit in data[1:-6]: number = (number << 5) + digit num_bytes = (len(data) - 7) * 5 // 8 bits_to_ignore = (len(data) - 7) * 5 % 8 number >>= bits_to_ignore hash = int_to_big_endian(number, num_bytes) if num_bytes < 2 or num_bytes > 40: raise ValueError(f"bytes out of range: {num_bytes}") return [network, version, hash]
def secure_mnemonic(num_bits=256, extra_entropy=0): """ Generates a mnemonic phrase using num_bits of entropy extra_entropy is optional and should not be saved as it is NOT SUFFICIENT to recover your mnemonic. extra_entropy exists only to prevent 100% reliance on your random number generator. """ if num_bits not in (128, 160, 192, 224, 256): raise ValueError(f"Invalid num_bits: {num_bits}") if type(extra_entropy) is not int: raise TypeError(f"extra_entropy must be an int: {extra_entropy}") if extra_entropy < 0: raise ValueError(f"extra_entropy cannot be negative: {extra_entropy}") # if we have more bits than needed, mask so we get what we need if len(bin(extra_entropy)) > num_bits + 2: extra_entropy &= (1 << num_bits) - 1 # For added paranoia, xor current epoch to extra_entropy # Would use time.time_ns() but that requires python3.7 extra_entropy ^= int(time() * 1_000_000) # xor some random bits with the extra_entropy that was passed in preseed = randbits(num_bits) ^ extra_entropy # convert the number to big-endian s = int_to_big_endian(preseed, num_bits // 8) # convert to mnemonic mnemonic = bytes_to_mnemonic(s, num_bits) # sanity check if mnemonic_to_bytes(mnemonic) != s: raise RuntimeError("Generated mnemonic does not correspond to random bits") return mnemonic
def sign(self, z): # per libsecp256k1 documentation, this helps against side-channel attacks if not lib.secp256k1_context_randomize( GLOBAL_CTX, secrets.token_bytes(32), ): raise RuntimeError("libsecp256k1 context randomization error") secret = int_to_big_endian(self.secret, 32) msg = int_to_big_endian(z, 32) csig = ffi.new("secp256k1_ecdsa_signature *") if not lib.secp256k1_ecdsa_sign(GLOBAL_CTX, csig, msg, secret, ffi.NULL, ffi.NULL): raise RuntimeError("libsecp256k1 ecdsa signing problem") sig = Signature(c=csig) if not self.point.verify(z, sig): raise RuntimeError("generated signature doesn't verify") return sig
def deterministic_k(self, z): k = b"\x00" * 32 v = b"\x01" * 32 if z > N: z -= N z_bytes = int_to_big_endian(z, 32) secret_bytes = int_to_big_endian(self.secret, 32) s256 = hashlib.sha256 k = hmac.new(k, v + b"\x00" + secret_bytes + z_bytes, s256).digest() v = hmac.new(k, v, s256).digest() k = hmac.new(k, v + b"\x01" + secret_bytes + z_bytes, s256).digest() v = hmac.new(k, v, s256).digest() while True: v = hmac.new(k, v, s256).digest() candidate = big_endian_to_int(v) if candidate >= 1 and candidate < N: return candidate k = hmac.new(k, v + b"\x00", s256).digest() v = hmac.new(k, v, s256).digest()
def der(self): # convert the r part to bytes rbin = int_to_big_endian(self.r, 32) # if rbin has a high bit, add a 00 if rbin[0] >= 128: rbin = b"\x00" + rbin while rbin[0] == 0: if rbin[1] >= 128: break else: rbin = rbin[1:] result = bytes([2, len(rbin)]) + rbin sbin = int_to_big_endian(self.s, 32) # if sbin has a high bit, add a 00 if sbin[0] >= 128: sbin = b"\x00" + sbin while sbin[0] == 0: if sbin[1] >= 128: break else: sbin = sbin[1:] result += bytes([2, len(sbin)]) + sbin return bytes([0x30, len(result)]) + result
def wif(self, compressed=True): # convert the secret from integer to a 32-bytes in big endian using int_to_big_endian(x, 32) secret_bytes = int_to_big_endian(self.secret, 32) # prepend b'\xef' on testnet/signet, b'\x80' on mainnet if self.network == "mainnet": prefix = b"\x80" else: prefix = b"\xef" # append b'\x01' if compressed if compressed: suffix = b"\x01" else: suffix = b"" # encode_base58_checksum the whole thing return encode_base58_checksum(prefix + secret_bytes + suffix)
def __rmul__(self, coefficient): coef = coefficient % N new_key = ffi.new("secp256k1_pubkey *") s = self.sec(compressed=False) if not lib.secp256k1_ec_pubkey_parse(GLOBAL_CTX, new_key, s, len(s)): raise RuntimeError("libsecp256k1 parse error") if not lib.secp256k1_ec_pubkey_tweak_mul(GLOBAL_CTX, new_key, int_to_big_endian(coef, 32)): raise RuntimeError("libsecp256k1 multiplication error") serialized = ffi.new("unsigned char [65]") output_len = ffi.new("size_t *", 65) if not lib.secp256k1_ec_pubkey_serialize( GLOBAL_CTX, serialized, output_len, new_key, lib.SECP256K1_EC_UNCOMPRESSED): raise RuntimeError("libsecp256k1 serialization error") return self.__class__(usec=bytes(serialized))
def get_signature(self, s_sum, r, sig_hash, tweak=0): tweak_point = self.get_tweak_point(tweak) if tweak: msg = r.bip340() + tweak_point.bip340() + sig_hash challenge = big_endian_to_int(hash_challenge(msg)) % N if tweak_point.parity: s = (-s_sum - challenge * tweak) % N else: s = (s_sum + challenge * tweak) % N else: s = s_sum % N s_raw = int_to_big_endian(s, 32) sig = r.bip340() + s_raw schnorrsig = SchnorrSignature.parse(sig) if not tweak_point.verify_schnorr(sig_hash, schnorrsig): raise ValueError("Invalid signature") return schnorrsig
def __add__(self, scalar): """Multiplies scalar by generator, adds result to current point""" coef = scalar % N new_key = ffi.new("secp256k1_pubkey *") s = self.sec(compressed=False) if not lib.secp256k1_ec_pubkey_parse(GLOBAL_CTX, new_key, s, len(s)): raise RuntimeError("libsecp256k1 parse error") if not lib.secp256k1_ec_pubkey_tweak_add(GLOBAL_CTX, new_key, int_to_big_endian(coef, 32)): raise RuntimeError("libsecp256k1 add error") serialized = ffi.new("unsigned char [65]") output_len = ffi.new("size_t *", 65) if not lib.secp256k1_ec_pubkey_serialize( GLOBAL_CTX, serialized, output_len, new_key, lib.SECP256K1_EC_UNCOMPRESSED): raise RuntimeError("libsecp256k1 serialize error") return self.__class__(usec=bytes(serialized))
def _crypt(cls, payload, id, exponent, passphrase, indices): if len(payload) % 2: raise ValueError("payload should be an even number of bytes") else: half = len(payload) // 2 left = payload[:half] right = payload[half:] salt = b"shamir" + int_to_big_endian(id, 2) for i in indices: f = pbkdf2_hmac( "sha256", i + passphrase, salt + right, 2500 << exponent, dklen=half, ) left, right = right, bytes(x ^ y for x, y in zip(left, f)) return right + left
def __init__( self, share_bit_length, id, exponent, group_index, group_threshold, group_count, member_index, member_threshold, value, ): self.share_bit_length = share_bit_length self.id = id self.exponent = exponent self.group_index = group_index if group_index < 0 or group_index > 15: raise ValueError( f"Group index should be between 0 and 15 inclusive {group_index}" ) self.group_threshold = group_threshold if group_threshold < 1 or group_threshold > group_count: raise ValueError( f"Group threshold should be between 1 and {group_count} inclusive {group_threshold}" ) self.group_count = group_count if group_count < 1 or group_count > 16: raise ValueError( f"Group count should be between 1 and 16 inclusive {group_count}" ) self.member_index = member_index if member_index < 0 or member_index > 15: raise ValueError( f"Member index should be between 0 and 15 inclusive {member_index}" ) self.member_threshold = member_threshold if member_threshold < 1 or member_threshold > 16: raise ValueError( f"Member threshold should be between 1 and 16 inclusive {member_threshold}" ) self.value = value self.bytes = int_to_big_endian(value, share_bit_length // 8)
def sign_schnorr(self, msg, aux): if len(msg) != 32: raise ValueError("msg needs to be 32 bytes") if len(aux) != 32: raise ValueError("aux needs to be 32 bytes") # per libsecp256k1 documentation, this helps against side-channel attacks if not lib.secp256k1_context_randomize( GLOBAL_CTX, secrets.token_bytes(32), ): raise RuntimeError("libsecp256k1 context randomization error") keypair = ffi.new("secp256k1_keypair *") if not lib.secp256k1_keypair_create( GLOBAL_CTX, keypair, int_to_big_endian(self.secret, 32)): raise RuntimeError("libsecp256k1 keypair creation problem") raw_sig = ffi.new("unsigned char [64]") if not lib.secp256k1_schnorrsig_sign(GLOBAL_CTX, raw_sig, msg, keypair, aux): raise RuntimeError("libsecp256k1 schnorr signing problem") return SchnorrSignature(bytes(ffi.buffer(raw_sig, 64)))
def sign_schnorr(self, msg, aux): if self.point.parity: d = N - self.secret else: d = self.secret if len(msg) != 32: raise ValueError("msg needs to be 32 bytes") if len(aux) != 32: raise ValueError("aux needs to be 32 bytes") t = xor_bytes(int_to_big_endian(d, 32), hash_aux(aux)) k = big_endian_to_int(hash_nonce(t + self.point.bip340() + msg)) % N r = k * G if r.parity: k = N - k r = k * G message = r.bip340() + self.point.bip340() + msg e = big_endian_to_int(hash_challenge(message)) % N s = (k + e * d) % N sig = SchnorrSignature(r, s) if not self.point.verify_schnorr(msg, sig): raise RuntimeError("Bad Signature") return sig
def mnemonic_to_bytes(mnemonic): """returns a byte representation of the mnemonic""" all_bits = 0 words = mnemonic.split() # check that there are 12, 15, 18, 21 or 24 words # if not, raise a ValueError if len(words) not in (12, 15, 18, 21, 24): raise InvalidBIP39Length( f"{len(words)} words (you need 12, 15, 18, 21, or 24 words)" ) num_words = len(words) for word in words: all_bits <<= 11 all_bits += BIP39[word] num_checksum_bits = num_words // 3 checksum = all_bits & ((1 << num_checksum_bits) - 1) all_bits >>= num_checksum_bits num_bytes = (num_words * 11 - num_checksum_bits) // 8 s = int_to_big_endian(all_bits, num_bytes) computed_checksum = sha256(s)[0] >> (8 - num_checksum_bits) if checksum != computed_checksum: raise InvalidChecksumWordsError("Checksum is wrong") return s
def __init__(self, shares): self.shares = shares if len(shares) > 1: # check that the identifiers are the same ids = {s.id for s in shares} if len(ids) != 1: raise TypeError("Shares are from different secrets") # check that the exponents are the same exponents = {s.exponent for s in shares} if len(exponents) != 1: raise TypeError( f"Shares should have the same exponent {exponents} {shares}" ) # check that the k-of-n is the same k = {s.group_threshold for s in shares} if len(k) != 1: raise ValueError(f"K of K-of-N should be the same {k}") n = {s.group_count for s in shares} if len(n) != 1: raise ValueError(f"N of K-of-N should be the same {n}") if k.pop() > n.pop(): raise ValueError("K > N in K-of-N") # check that the share lengths are the same lengths = {s.share_bit_length for s in shares} if len(lengths) != 1: raise ValueError( f"all shares should have the same length {lengths}") # check that the x coordinates are unique xs = {(s.group_index, s.member_index) for s in shares} if len(xs) != len(shares): raise ValueError(f"Share indices should be unique {xs}") self.id = shares[0].id self.salt = b"shamir" + int_to_big_endian(self.id, 2) self.exponent = shares[0].exponent self.group_threshold = shares[0].group_threshold self.group_count = shares[0].group_count self.share_bit_length = shares[0].share_bit_length
def bip340(self): # returns the binary version of BIP340 pubkey if self.x is None: return int_to_big_endian(0, 32) return int_to_big_endian(self.x.num, 32)
def verify(self, z, sig): msg = int_to_big_endian(z, 32) sig_data = sig.cdata() return lib.secp256k1_ecdsa_verify(GLOBAL_CTX, sig_data, msg, self.c)
def serialize(self): return self.r.bip340() + int_to_big_endian(self.s, 32)