support for compressed keys
authorthomasv <thomasv@gitorious>
Mon, 31 Dec 2012 09:41:02 +0000 (10:41 +0100)
committerthomasv <thomasv@gitorious>
Sat, 5 Jan 2013 10:50:49 +0000 (11:50 +0100)
TODOLIST
lib/bitcoin.py
lib/wallet.py

index 08b1d6f..988fbf1 100644 (file)
--- a/TODOLIST
+++ b/TODOLIST
@@ -5,7 +5,6 @@ security:
 
 
 wallet, transactions : 
- - support compressed keys
  - dust sweeping
  - transactions with multiple outputs
  - BIP 32 
index c8e8ba0..eb20500 100644 (file)
@@ -43,8 +43,57 @@ Hash = lambda x: hashlib.sha256(hashlib.sha256(x).digest()).digest()
 hash_encode = lambda x: x[::-1].encode('hex')
 hash_decode = lambda x: x.decode('hex')[::-1]
 
-############ functions from pywallet ##################### 
 
+# pywallet openssl private key implementation
+
+def i2d_ECPrivateKey(pkey, compressed=False):
+    if compressed:
+        key = '3081d30201010420' + \
+              '%064x' % pkey.secret + \
+              'a081a53081a2020101302c06072a8648ce3d0101022100' + \
+              '%064x' % _p + \
+              '3006040100040107042102' + \
+              '%064x' % _Gx + \
+              '022100' + \
+              '%064x' % _r + \
+              '020101a124032200'
+    else:
+        key = '308201130201010420' + \
+              '%064x' % pkey.secret + \
+              'a081a53081a2020101302c06072a8648ce3d0101022100' + \
+              '%064x' % _p + \
+              '3006040100040107044104' + \
+              '%064x' % _Gx + \
+              '%064x' % _Gy + \
+              '022100' + \
+              '%064x' % _r + \
+              '020101a144034200'
+        
+    return key.decode('hex') + i2o_ECPublicKey(pkey, compressed)
+    
+def i2o_ECPublicKey(pkey, compressed=False):
+    # public keys are 65 bytes long (520 bits)
+    # 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate
+    # 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed
+    # compressed keys: <sign> <x> where <sign> is 0x02 if y is even and 0x03 if y is odd
+    if compressed:
+        if pkey.pubkey.point.y() & 1:
+            key = '03' + '%064x' % pkey.pubkey.point.x()
+        else:
+            key = '02' + '%064x' % pkey.pubkey.point.x()
+    else:
+        key = '04' + \
+              '%064x' % pkey.pubkey.point.x() + \
+              '%064x' % pkey.pubkey.point.y()
+            
+    return key.decode('hex')
+            
+# end pywallet openssl private key implementation
+
+                                                
+            
+############ functions from pywallet ##################### 
+            
 addrtype = 0
 
 def hash_160(public_key):
@@ -151,17 +200,39 @@ def DecodeBase58Check(psz):
 def PrivKeyToSecret(privkey):
     return privkey[9:9+32]
 
-def SecretToASecret(secret):
-    vchIn = chr(addrtype+128) + secret
+def SecretToASecret(secret, compressed=False):
+    vchIn = chr((addrtype+128)&255) + secret
+    if compressed: vchIn += '\01'
     return EncodeBase58Check(vchIn)
 
 def ASecretToSecret(key):
     vch = DecodeBase58Check(key)
-    if vch and vch[0] == chr(addrtype+128):
+    if vch and vch[0] == chr((addrtype+128)&255):
         return vch[1:]
     else:
         return False
 
+def regenerate_key(sec):
+    b = ASecretToSecret(sec)
+    if not b:
+        return False
+    b = b[0:32]
+    secret = int('0x' + b.encode('hex'), 16)
+    return EC_KEY(secret)
+
+def GetPubKey(pkey, compressed=False):
+    return i2o_ECPublicKey(pkey, compressed)
+
+def GetPrivKey(pkey, compressed=False):
+    return i2d_ECPrivateKey(pkey, compressed)
+
+def GetSecret(pkey):
+    return ('%064x' % pkey.secret).decode('hex')
+
+def is_compressed(sec):
+    b = ASecretToSecret(sec)
+    return len(b) == 33
+
 ########### end pywallet functions #######################
 
 # secp256k1, http://www.oid-info.com/get/1.3.132.0.10
@@ -176,6 +247,13 @@ generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r )
 oid_secp256k1 = (1,3,132,0,10)
 SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 ) 
 
+class EC_KEY(object):
+    def __init__( self, secret ):
+        self.pubkey = ecdsa.ecdsa.Public_key( generator_secp256k1, generator_secp256k1 * secret )
+        self.privkey = ecdsa.ecdsa.Private_key( self.pubkey, secret )
+        self.secret = secret
+        
+
 
 def filter(s): 
     out = re.sub('( [^\n]*|)\n','',s)
@@ -195,7 +273,6 @@ def raw_tx( inputs, outputs, for_sig = None ):
             sig = sig + chr(1)                               # hashtype
             script  = int_to_hex( len(sig))                  +  '     push %d bytes\n'%len(sig)
             script += sig.encode('hex')                      +  '     sig\n'
-            pubkey = chr(4) + pubkey
             script += int_to_hex( len(pubkey))               +  '     push %d bytes\n'%len(pubkey)
             script += pubkey.encode('hex')                   +  '     pubkey\n'
         elif for_sig==i:
index 648b30d..9cde7c8 100644 (file)
@@ -113,22 +113,33 @@ class Wallet:
         while not self.is_up_to_date(): time.sleep(0.1)
 
     def import_key(self, keypair, password):
-        address, key = keypair.split(':')
+
+        address, sec = keypair.split(':')
         if not self.is_valid(address):
             raise BaseException('Invalid Bitcoin address')
         if address in self.all_addresses():
             raise BaseException('Address already in wallet')
-        b = ASecretToSecret( key )
-        if not b: 
-            raise BaseException('Unsupported key format')
-        secexp = int( b.encode('hex'), 16)
-        private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
+        
+        # rebuild public key from private key, compressed or uncompressed
+        pkey = regenerate_key(sec)
+        if not pkey:
+            return False
+        
+        # figure out if private key is compressed
+        compressed = is_compressed(sec)
+        
+        # rebuild private and public key from regenerated secret
+        private_key = GetPrivKey(pkey, compressed)
+        public_key = GetPubKey(pkey, compressed)
+        addr = public_key_to_bc_address(public_key)
+        
         # sanity check
-        public_key = private_key.get_verifying_key()
-        if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ):
+        if not address == addr :
             raise BaseException('Address does not match private key')
-        self.imported_keys[address] = self.pw_encode( key, password )
-
+        
+        # store the originally requested keypair into the imported keys table
+        self.imported_keys[address] = self.pw_encode(sec, password )
+        
 
     def new_seed(self, password):
         seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
@@ -172,19 +183,23 @@ class Wallet:
         return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
 
     def get_private_key_base58(self, address, password):
-        pk = self.get_private_key(address, password)
-        if pk is None: return None
-        return SecretToASecret( pk )
+        secexp, compressed = self.get_private_key(address, password)
+        if secexp is None: return None
+        pk = number_to_string( secexp, generator_secp256k1.order() )
+        return SecretToASecret( pk, compressed )
 
     def get_private_key(self, address, password):
         """  Privatekey(type,n) = Master_private_key + H(n|S|type)  """
         order = generator_secp256k1.order()
         
         if address in self.imported_keys.keys():
-            b = self.pw_decode( self.imported_keys[address], password )
-            if not b: return None
-            b = ASecretToSecret( b )
-            secexp = int( b.encode('hex'), 16)
+            sec = self.pw_decode( self.imported_keys[address], password )
+            if not sec: return None, None
+
+            pkey = regenerate_key(sec)
+            compressed = is_compressed(sec)
+            secexp = pkey.secret
+        
         else:
             if address in self.addresses:
                 n = self.addresses.index(address)
@@ -201,20 +216,21 @@ class Wallet:
             if not seed: return None
             secexp = self.stretch_key(seed)
             secexp = ( secexp + self.get_sequence(n,for_change) ) % order
+            compressed = False
 
-        pk = number_to_string(secexp,order)
-        return pk
+        return secexp, compressed
 
     def msg_magic(self, message):
         return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message
 
     def sign_message(self, address, message, password):
-        private_key = ecdsa.SigningKey.from_string( self.get_private_key(address, password), curve = SECP256k1 )
+        secexp, compressed = self.get_private_key(address, password)
+        private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
         public_key = private_key.get_verifying_key()
         signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string )
         assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string)
         for i in range(4):
-            sig = base64.b64encode( chr(27+i) + signature )
+            sig = base64.b64encode( chr(27 + i + (4 if compressed else 0)) + signature )
             try:
                 self.verify_message( address, sig, message)
                 return sig
@@ -598,9 +614,13 @@ class Wallet:
         s_inputs = []
         for i in range(len(inputs)):
             addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
-            private_key = ecdsa.SigningKey.from_string( self.get_private_key(addr, password), curve = SECP256k1 )
+            secexp, compressed = self.get_private_key(addr, password)
+            private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
             public_key = private_key.get_verifying_key()
-            pubkey = public_key.to_string()
+
+            pkey = EC_KEY(secexp)
+            pubkey = GetPubKey(pkey, compressed)
+
             tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
             sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
             assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)