From 91b2b637b48a2e5674193a5ac8a6351e468d3689 Mon Sep 17 00:00:00 2001 From: slush0 Date: Tue, 14 Jan 2014 14:29:18 +0100 Subject: [PATCH] BIP32 public CKD --- tests/test_addresses.py | 27 +++++++++++- tests/test_signtx.py | 26 ++++++++++++ trezorlib/ckd_public.py | 74 +++++++++++++++++++++++++------- trezorlib/client.py | 14 ------ trezorlib/msqrt.py | 94 +++++++++++++++++++++++++++++++++++++++++ trezorlib/tools.py | 29 ++++++++++++- 6 files changed, 233 insertions(+), 31 deletions(-) create mode 100644 trezorlib/msqrt.py diff --git a/tests/test_addresses.py b/tests/test_addresses.py index 32839f5ab8..de60ffcebf 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -1,5 +1,7 @@ import unittest import common +import trezorlib.ckd_public as bip32 +from trezorlib import tools class TestAddresses(common.TrezorTest): def test_btc(self): @@ -36,6 +38,29 @@ class TestAddresses(common.TrezorTest): language='english') self.assertEqual(self.client.get_address('Testnet', [111, 42]), 'moN6aN6NP1KWgnPSqzrrRPvx2x1UtZJssa') - + + def test_public_ckd(self): + self.client.load_device_by_mnemonic(mnemonic=self.mnemonic1, + pin='', + passphrase_protection=False, + label='test', + language='english') + + node = self.client.get_public_node([]) + node_sub1 = self.client.get_public_node([1]) + node_sub2 = bip32.public_ckd(node, [1]) + + print node_sub1 + print node_sub2 + + self.assertEqual(node_sub1.chain_code, node_sub2.chain_code) + self.assertEqual(node_sub1.public_key, node_sub2.public_key) + + address1 = self.client.get_address('Bitcoin', [1]) + address2 = bip32.get_address(node_sub2, 0) + + self.assertEqual(address2, '1CK7SJdcb8z9HuvVft3D91HLpLC6KSsGb') + self.assertEqual(address1, address2) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_signtx.py b/tests/test_signtx.py index 832e0a7470..07a28e7072 100644 --- a/tests/test_signtx.py +++ b/tests/test_signtx.py @@ -7,6 +7,7 @@ import trezorlib.types_pb2 as proto_types class TestSignTx(common.TrezorTest): + ''' def test_simplesigntx(self): # tx: d5f65ee80147b4bcc70b75e4bbf2d7382021b871bd8867ef8fa525ef50864882 # input 0: 0.0039 BTC @@ -25,6 +26,31 @@ class TestSignTx(common.TrezorTest): tx = self.client.simple_sign_tx('Bitcoin', [inp1, ], [out1, ]) print binascii.hexlify(tx.serialized_tx) self.assertEqual(binascii.hexlify(tx.serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b4830450221009a0b7be0d4ed3146ee262b42202841834698bb3ee39c24e7437df208b8b7077102202b79ab1e7736219387dffe8d615bbdba87e11477104b867ef47afed1a5ede7810121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0160cc0500000000001976a914de9b2a8da088824e8fe51debea566617d851537888ac00000000') + ''' + + + def test_simplesigntx_testnet(self): + # tx: d5f65ee80147b4bcc70b75e4bbf2d7382021b871bd8867ef8fa525ef50864882 + # input 0: 0.0039 BTC + self.client.load_device_by_xprv('xprv9s21ZrQH143K3zttRjiQmYwyugvd13pnd2VzefWrfSouRfnj5oSkJgBQXxtn18E9mqrDop7fQ8Xnb9JCLPE4vghzhpU4dT33ZJ7frjzTEW8', + '', False, 'testnet') + + inp1 = proto_types.TxInputType(address_n=[6], # mo8uUSFJULCMA4neRS9aS9jiXZ1N72FSLK + # amount=390000, + prev_hash=binascii.unhexlify('d83b27f16ce5069e0c8e4a02813f252500e257744d5b00c9b6128be7189117b1'), + prev_index=0, + ) + + out1 = proto_types.TxOutputType(address='mjKKH3Dk95VMbdNnDQYHZXoQ9QwuCZocwb', + amount=80085000, + script_type=proto_types.PAYTOADDRESS, + ) + + rawtx = {'d83b27f16ce5069e0c8e4a02813f252500e257744d5b00c9b6128be7189117b1': '01000000013b21cc65080c57793d0e47045a24d8e92262dc47efdc425fd5cad9a25e928f6c000000006b483045022100bde591f2c997bafa8388916663b148f4093914851a33a9903da69ad97afa6f470220138c6ff11321339974bac9c0992d7b9d72aef0c2d098f26267ec9f05d532c859012103edcc8dc5cac7dca6ed191d812621fb300863fea0dd5d14180b482b917a35acc4ffffffff020800c604000000001976a91453958011070469e2ef5e1115f34f509717d6884288acf8c99502000000001976a9141e2ba9407a6920246d0f345beecb89ed47c99a7788ac00000000'} + tx = self.client.simple_sign_tx('Testnet', [inp1, ], [out1, ]) + print binascii.hexlify(tx.serialized_tx) + # self.assertEqual(binascii.hexlify(tx.serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b4830450221009a0b7be0d4ed3146ee262b42202841834698bb3ee39c24e7437df208b8b7077102202b79ab1e7736219387dffe8d615bbdba87e11477104b867ef47afed1a5ede7810121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0160cc0500000000001976a914de9b2a8da088824e8fe51debea566617d851537888ac00000000') + ''' def test_signtx(self): diff --git a/trezorlib/ckd_public.py b/trezorlib/ckd_public.py index ec5886085f..9ee747efae 100644 --- a/trezorlib/ckd_public.py +++ b/trezorlib/ckd_public.py @@ -1,25 +1,63 @@ import struct import hmac import hashlib -from ecdsa.util import string_to_number + +import ecdsa +from ecdsa.util import string_to_number, number_to_string from ecdsa.curves import SECP256k1 +from ecdsa.ellipticcurve import Point, INFINITY -import messages_pb2 as proto - -# FIXME, this isn't finished yet +import msqrt +import tools +import types_pb2 as proto_types PRIME_DERIVATION_FLAG = 0x80000000 +def point_to_pubkey(point): + order = SECP256k1.order + x_str = number_to_string(point.x(), order) + y_str = number_to_string(point.y(), order) + vk = x_str + y_str + return chr((ord(vk[63]) & 1) + 2) + vk[0:32] # To compressed key + +def sec_to_public_pair(pubkey): + """Convert a public key in sec binary format to a public pair.""" + x = string_to_number(pubkey[1:33]) + sec0 = pubkey[:1] + if sec0 not in (b'\2', b'\3'): + raise Exception("Compressed pubkey expected") + + def public_pair_for_x(generator, x, is_even): + curve = generator.curve() + p = curve.p() + alpha = (pow(x, 3, p) + curve.a() * x + curve.b()) % p + beta = msqrt.modular_sqrt(alpha, p) + if is_even == bool(beta & 1): + return (x, p - beta) + return (x, beta) + + return public_pair_for_x(ecdsa.ecdsa.generator_secp256k1, x, is_even=(sec0 == b'\2')) + def is_prime(n): return (bool)(n & PRIME_DERIVATION_FLAG) -def hash_160(public_key): - md = hashlib.new('ripemd160') - md.update(hashlib.sha256(public_key).digest()) - return md.digest() - def fingerprint(pubkey): - return string_to_number(hash_160(pubkey)[:4]) + return string_to_number(tools.hash_160(pubkey)[:4]) + +def get_address(public_node, address_type): + return tools.public_key_to_bc_address(public_node.public_key, address_type) + +def public_ckd(public_node, n): + if not isinstance(n, list): + raise Exception('Parameter must be a list') + + node = proto_types.HDNodeType() + node.CopyFrom(public_node) + + for i in n: + node.CopyFrom(get_subnode(node, i)) + + return node def get_subnode(node, i): # Public Child key derivation (CKD) algorithm of BIP32 @@ -34,16 +72,22 @@ def get_subnode(node, i): I64 = hmac.HMAC(key=node.chain_code, msg=data, digestmod=hashlib.sha512).digest() I_left_as_exponent = string_to_number(I64[:32]) - node_out = proto.HDNodeType() + node_out = proto_types.HDNodeType() node_out.version = node.version node_out.depth = node.depth + 1 node_out.child_num = i node_out.chain_code = I64[32:] node_out.fingerprint = fingerprint(node.public_key) - # FIXME - # node_out.public_key = cls._get_pubkey(node_out.private_key) - # x, y = self.public_pair - # the_point = I_left_as_exponent * ecdsa.generator_secp256k1 + ecdsa.Point(ecdsa.generator_secp256k1.curve(), x, y, SECP256k1.generator.order()) + # BIP32 magic converts old public key to new public point + x, y = sec_to_public_pair(node.public_key) + point = I_left_as_exponent * SECP256k1.generator + \ + Point(SECP256k1.curve, x, y, SECP256k1.order) + + if point == INFINITY: + raise Exception("Point cannot be INFINITY") + + # Convert public point to compressed public key + node_out.public_key = point_to_pubkey(point) return node_out diff --git a/trezorlib/client.py b/trezorlib/client.py index c37c1a18c2..b3c0ccdad1 100644 --- a/trezorlib/client.py +++ b/trezorlib/client.py @@ -396,20 +396,6 @@ class TrezorClient(object): self.init_device() return isinstance(resp, proto.Success) - def bip32_ckd(self, public_node, n): - raise Exception("Unfinished code") - - if not isinstance(n, list): - raise Exception('Parameter must be a list') - - node = types.HDNodeType() - node.CopyFrom(public_node) - - for i in n: - node.CopyFrom(ckd_public.get_subnode(node, i)) - - return node - def firmware_update(self, fp): if self.features.bootloader_mode == False: raise Exception("Device must be in bootloader mode") diff --git a/trezorlib/msqrt.py b/trezorlib/msqrt.py new file mode 100644 index 0000000000..f582ec2605 --- /dev/null +++ b/trezorlib/msqrt.py @@ -0,0 +1,94 @@ +# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/ + +def modular_sqrt(a, p): + """ Find a quadratic residue (mod p) of 'a'. p + must be an odd prime. + + Solve the congruence of the form: + x^2 = a (mod p) + And returns x. Note that p - x is also a root. + + 0 is returned is no square root exists for + these a and p. + + The Tonelli-Shanks algorithm is used (except + for some simple cases in which the solution + is known from an identity). This algorithm + runs in polynomial time (unless the + generalized Riemann hypothesis is false). + """ + # Simple cases + # + if legendre_symbol(a, p) != 1: + return 0 + elif a == 0: + return 0 + elif p == 2: + return p + elif p % 4 == 3: + return pow(a, (p + 1) / 4, p) + + # Partition p-1 to s * 2^e for an odd s (i.e. + # reduce all the powers of 2 from p-1) + # + s = p - 1 + e = 0 + while s % 2 == 0: + s /= 2 + e += 1 + + # Find some 'n' with a legendre symbol n|p = -1. + # Shouldn't take long. + # + n = 2 + while legendre_symbol(n, p) != -1: + n += 1 + + # Here be dragons! + # Read the paper "Square roots from 1; 24, 51, + # 10 to Dan Shanks" by Ezra Brown for more + # information + # + + # x is a guess of the square root that gets better + # with each iteration. + # b is the "fudge factor" - by how much we're off + # with the guess. The invariant x^2 = ab (mod p) + # is maintained throughout the loop. + # g is used for successive powers of n to update + # both a and b + # r is the exponent - decreases with each update + # + x = pow(a, (s + 1) / 2, p) + b = pow(a, s, p) + g = pow(n, s, p) + r = e + + while True: + t = b + m = 0 + for m in xrange(r): + if t == 1: + break + t = pow(t, 2, p) + + if m == 0: + return x + + gs = pow(g, 2 ** (r - m - 1), p) + g = (gs * gs) % p + x = (x * gs) % p + b = (b * g) % p + r = m + +def legendre_symbol(a, p): + """ Compute the Legendre symbol a|p using + Euler's criterion. p is a prime, a is + relatively prime to p (if p divides + a, then a|p = 0) + + Returns 1 if a has a square root modulo + p, -1 otherwise. + """ + ls = pow(a, (p - 1) / 2, p) + return -1 if ls == p - 1 else ls diff --git a/trezorlib/tools.py b/trezorlib/tools.py index 3e0837196a..cb264a4a52 100644 --- a/trezorlib/tools.py +++ b/trezorlib/tools.py @@ -1,7 +1,34 @@ +import hashlib + +Hash = lambda x: hashlib.sha256(hashlib.sha256(x).digest()).digest() + +def hash_160(public_key): + md = hashlib.new('ripemd160') + md.update(hashlib.sha256(public_key).digest()) + return md.digest() + + +def hash_160_to_bc_address(h160, address_type): + vh160 = chr(address_type) + h160 + h = Hash(vh160) + addr = vh160 + h[0:4] + return b58encode(addr) + +def compress_pubkey(public_key): + if public_key[0] == '\x04': + return chr((ord(public_key[64]) & 1) + 2) + public_key[1:33] + raise Exception("Pubkey is already compressed") + +def public_key_to_bc_address(public_key, address_type, compress=True): + if public_key[0] == '\x04' and compress: + public_key = compress_pubkey(public_key) + + h160 = hash_160(public_key) + return hash_160_to_bc_address(h160, address_type) + __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' __b58base = len(__b58chars) - def b58encode(v): """ encode v, which is a string of bytes, to base58."""