From c00bf826aa8a2b2b09bc7ed1bc1bd2832f436487 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 20 Feb 2017 21:01:50 -0500 Subject: [PATCH] Move data-signing and data-verifying logic from storage.py to keys.py --- blockstack_client/keys.py | 214 +++++++++++++++++++++++++++++++++++--- 1 file changed, 201 insertions(+), 13 deletions(-) diff --git a/blockstack_client/keys.py b/blockstack_client/keys.py index ad8538da3..45f9eddfb 100644 --- a/blockstack_client/keys.py +++ b/blockstack_client/keys.py @@ -42,7 +42,12 @@ import fastecdsa import fastecdsa.curve import fastecdsa.keys import fastecdsa.ecdsa - +from fastecdsa import _ecdsa +from fastecdsa.util import RFC6979 +import hmac +from struct import pack +import hashlib +import base64 import pybitcoin import bitcoin import binascii @@ -64,11 +69,9 @@ class HDWallet(object): """ Initialize a hierarchical deterministic wallet with hex_privkey and get child addresses and private keys - - TODO: chain state """ - def __init__(self, hex_privkey=None, config_path=CONFIG_PATH): + def __init__(self, hex_privkey=None, chaincode='\x00' * 32, config_path=CONFIG_PATH): """ If @hex_privkey is given, use that to derive keychain otherwise, use a new random seed @@ -78,30 +81,33 @@ class HDWallet(object): global KEYCHAIN_CACHE assert hex_privkey + assert len(chaincode) == 32 + self.hex_privkey = hex_privkey self.priv_keychain = None self.master_address = None self.child_addresses = None - if KEYCHAIN_CACHE.has_key(str(self.hex_privkey)): + keychain_key = str(self.hex_privkey) + ":" + str(chaincode.encode('hex')) + if KEYCHAIN_CACHE.has_key(keychain_key): if BLOCKSTACK_TEST: - log.debug("{} keychain is cached".format(self.hex_privkey)) + log.debug("{} keychain is cached".format(keychain_key)) - self.priv_keychain = KEYCHAIN_CACHE[str(self.hex_privkey)] + self.priv_keychain = KEYCHAIN_CACHE[keychain_key] else: if BLOCKSTACK_TEST: - log.debug("{} keychain is NOT cached".format(self.hex_privkey)) + log.debug("{} keychain is NOT cached".format(keychain_key)) - self.priv_keychain = self.get_priv_keychain(self.hex_privkey) - KEYCHAIN_CACHE[str(self.hex_privkey)] = self.priv_keychain + self.priv_keychain = self.get_priv_keychain(self.hex_privkey, chaincode) + KEYCHAIN_CACHE[keychain_key] = self.priv_keychain self.master_address = self.get_master_address() - def get_priv_keychain(self, hex_privkey): + def get_priv_keychain(self, hex_privkey, chaincode): if hex_privkey: - return PrivateKeychain.from_private_key(hex_privkey) + return PrivateKeychain.from_private_key(hex_privkey, chain_path=chaincode) log.debug('No privatekey given, starting new wallet') return PrivateKeychain() @@ -217,6 +223,70 @@ class HDWallet(object): return None +class RFC6979_blockstack(RFC6979): + """ + Generate RFC6979 nonces from a file + """ + def __init__(self, x, q, hashfunc): + RFC6979.__init__(self, '', x, q, hashfunc) + + + def gen_nonce_from_digest( self, h1 ): + """ + Make the nonce from the digest. + @h1: bin-encoded digest + @hash_size: size of the digest + """ + hash_size = self.hashfunc().digest_size + key_and_msg = self._int2octets(self.x) + self._bits2octets(h1) + + v = b''.join([b'\x01' for _ in range(hash_size)]) + k = b''.join([b'\x00' for _ in range(hash_size)]) + + k = hmac.new(k, v + b'\x00' + key_and_msg, self.hashfunc).digest() + v = hmac.new(k, v, self.hashfunc).digest() + k = hmac.new(k, v + b'\x01' + key_and_msg, self.hashfunc).digest() + v = hmac.new(k, v, self.hashfunc).digest() + + while True: + t = b'' + + while len(t) * 8 < self.qlen: + v = hmac.new(k, v, self.hashfunc).digest() + t = t + v + + nonce = self._bits2int(t) + if nonce >= 1 and nonce < self.q: + return nonce + + k = hmac.new(k, v + b'\x00', self.hashfunc).digest() + v = hmac.new(k, v, self.hashfunc).digest() + + + def gen_nonce_from_file(self, fd, fd_len=None): + ''' http://tools.ietf.org/html/rfc6979#section-3.2 ''' + # based on gen_nonce() + + h1 = self.hashfunc() + + count = 0 + while True: + buf = f.read(65536) + if len(buf) == 0: + break + + if fd_len is not None: + if count + len(buf) > fd_len: + buf = buf[:fd_len - count] + + h.update(buf) + count += len(buf) + + h1 = h1.digest() + + return self.gen_nonce_from_digest(h1) + + def is_multisig(privkey_info): """ Does the given private key info represent @@ -287,7 +357,6 @@ def singlesig_privkey_to_string(privkey_info): Convert private key to string """ return ECPrivateKey(privkey_info).to_hex() - #return virtualchain.BitcoinPrivateKey(privkey_info).to_hex() def multisig_privkey_to_string(privkey_info): @@ -723,3 +792,122 @@ def get_uncompressed_private_and_public_keys( privkey_str ): pubk_hex = virtualchain.BitcoinPrivateKey(pk_hex).public_key().to_hex() return pk_hex, pubk_hex + +def decode_privkey_hex(privkey_hex): + """ + Decode a private key for ecdsa signature + """ + # force uncompressed + priv = str(privkey_hex) + if len(priv) > 64: + assert priv[-2:] == '01' + priv = priv[:64] + + pk_i = int(priv, 16) + return pk_i + + +def decode_pubkey_hex(pubkey_hex): + """ + Decode a public key for ecdsa verification + """ + pubk = str(pubkey_hex) + if keylib.key_formatting.get_pubkey_format(pubk) == 'hex_compressed': + pubk = keylib.key_formatting.decompress(pubk) + + assert len(pubk) == 130 + + pubk_raw = pubk[2:] + pubk_i = (int(pubk_raw[:64], 16), int(pubk_raw[64:], 16)) + return pubk_i + + +def encode_signature(sig_r, sig_s): + """ + Encode an ECDSA signature, with low-s + """ + # enforce low-s + if sig_s * 2 >= fastecdsa.curve.secp256k1.q: + log.debug("High-S to low-S") + sig_s = fastecdsa.curve.secp256k1.q - sig_s + + sig_bin = '{:064x}{:064x}'.format(sig_r, sig_s).decode('hex') + assert len(sig_bin) == 64 + + sig_b64 = base64.b64encode(sig_bin) + return sig_b64 + + +def decode_signature(sigb64): + """ + Decode a signature into r, s + """ + sig_bin = base64.b64decode(sigb64) + assert len(sig_bin) == 64 + + sig_hex = sig_bin.encode('hex') + sig_r = int(sig_hex[:64], 16) + sig_s = int(sig_hex[64:], 16) + return sig_r, sig_s + + +def sign_raw_data(raw_data, privatekey_hex): + """ + Sign a string of data. + Returns signature as a base64 string + """ + pk_i = decode_privkey_hex(privatekey_hex) + sig_r, sig_s = fastecdsa.ecdsa.sign(raw_data, pk_i, curve=fastecdsa.curve.secp256k1) + sig_b64 = encode_signature(sig_r, sig_s) + return sig_b64 + + +def verify_raw_data(raw_data, pubkey_hex, sigb64): + """ + Verify the signature over a string, given the public key + and base64-encode signature. + Return True on success. + Return False on error. + """ + sig_r, sig_s = decode_signature(sigb64) + pubk_i = decode_pubkey_hex(pubkey_hex) + res = fastecdsa.ecdsa.verify((sig_r, sig_s), raw_data, pubk_i, curve=fastecdsa.curve.secp256k1) + return res + + +def sign_digest( digest_hex, privkey_hex, curve=fastecdsa.curve.secp256k1, hashfunc=hashlib.sha256 ): + """ + Sign a digest with ECDSA + Return base64 signature + """ + pk_i = decode_privkey_hex(str(privkey_hex)) + + # generate a deterministic nonce per RFC6979 + rfc6979 = RFC6979_blockstack(pk_i, curve.q, hashfunc) + k = rfc6979.gen_nonce_from_digest(digest_hex.decode('hex')) + + r, s = _ecdsa.sign(digest_hex, str(pk_i), str(k), curve.name) + return encode_signature(int(r), int(s)) + + +def verify_digest( digest_hex, pubkey_hex, sigb64, curve=fastecdsa.curve.secp256k1, hashfunc=hashlib.sha256 ): + """ + Verify a digest and signature with ECDSA + Return True if it matches + """ + + Q = decode_pubkey_hex(str(pubkey_hex)) + r, s = decode_signature(sigb64) + + # validate Q, r, s + if not curve.is_point_on_curve(Q): + raise fastecdsa.ecdsa.EcdsaError('Invalid public key, point is not on curve {}'.format(curve.name)) + elif r > curve.q or r < 1: + raise fastecdsa.ecdsa.EcdsaError('Invalid Signature: r is not a positive integer smaller than the curve order') + elif s > curve.q or s < 1: + raise fastecdsa.ecdsa.EcdsaError('Invalid Signature: s is not a positive integer smaller than the curve order') + + qx, qy = Q + return _ecdsa.verify(str(r), str(s), digest_hex, str(qx), str(qy), curve.name) + +