├── README.md └── borring.py /README.md: -------------------------------------------------------------------------------- 1 | # borring 2 | 3 | **UPDATE: Removed bitcoin directory; this serves to prevent misguided usage of this code as anything other than educational/pseudocode! Do NOT attempt to use this in real projects!** 4 | 5 | *Update: algorithm now confirmed to be compatible with that in Elements Alpha*. 6 | 7 | Basic implementation of Borromean ring signatures in Python, for learning. I wrote this to aid my own understanding; it may also help you. It is not intended to be functional or fit for any other purpose. 8 | 9 | `python borring.py -h` for usage syntax. 10 | 11 | See [the Borromean ring signatures paper](https://github.com/Blockstream/borromean_paper/raw/master/borromean_draft_0.01_34241bb.pdf) for the theory. 12 | 13 | The idea is to have a signature over (key1 or key2 or key3 ...) AND (key4 or key5 or key6 ...) AND ... , for an arbitrary number of keys in each 'or' loop and an arbitrary number of loops. 14 | 15 | The keys used are Bitcoin (secp256k1 by default) keys. The public keys for verification of a signature are to be stored in a text file in hex format, and the corresponding private key for **one** of the public keys in each "OR loop" must be stored in hex format in a second file (any in the loop; but only one); sample key files are created by using the -g option, so there's no need to mess around finding keys from somewhere else. 16 | 17 | The message to be signed must be specified as the single argument to the script, as a single string. 18 | 19 | For Bitcoin operations (public/private and ECC operations) a snapshot of [pybitcointools](https://github.com/vbuterin/pybitcointools) was used; but see first 'update' note above. 20 | 21 | 22 | ## Example (4 OR loops, each with 7 keys) 23 | 24 | ```` 25 | ~/DevRepos/borring$ python borring.py -g -N 4 -M 7 26 | generating 27 | ~/DevRepos/borring$ python borring.py -w asigfile 'blah blah blah' 28 | writing sig to file: asigfile 29 | signature length: 928 30 | ~/DevRepos/borring$ python borring.py -e asigfile 'blah blah blah' 31 | Now trying to verify 32 | verification success 33 | 7383ec2d299131d473c1d969af10a88da9e8d4812d877d1d20f99ed2521f77b8 34 | 7383ec2d299131d473c1d969af10a88da9e8d4812d877d1d20f99ed2521f77b8 35 | ```` 36 | -------------------------------------------------------------------------------- /borring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os, binascii 3 | import bitcoin as btc 4 | from optparse import OptionParser 5 | from hashlib import sha256 6 | import random 7 | 8 | 9 | def borr_hash(m, pubkey, i, j, is_pubkey=True): 10 | '''A Sha256 hash of data in the standard 'borromean' 11 | format. Note that 'pubkey' is the value kG as according to 12 | kG = sG - eP. 13 | i,j : the indices to a specific vertex in the graph. 14 | m - the message to be hashed. 15 | Returns - the hash. 16 | ''' 17 | #little hack assuming sizes < 256 18 | p = btc.encode_pubkey(pubkey,'bin_compressed') if is_pubkey else pubkey 19 | x = p + m +'\x00'*3 + chr(i) + '\x00'*3 + chr(j) 20 | return sha256(x).digest() 21 | 22 | def generate_keyfiles(n, m, vf, sf): 23 | '''Generate a set of public and private keys 24 | for testing. 25 | n - the number of OR loops 26 | m - the number of keys per loop (note: constant in this crude version) 27 | vf - the file path to which to write the verification keys 28 | sf - the file path to which to write the signing (private) keys 29 | ''' 30 | signing_indices = [random.choice(range(m)) for _ in range(n)] 31 | priv=[] 32 | with open(sf,'wb') as f: 33 | for i in range(n): 34 | priv.append(os.urandom(32)) 35 | f.write(binascii.hexlify(priv[i])+'\n') 36 | with open(vf,'wb') as f: 37 | for i in range(n): 38 | pubkeys = [] 39 | for j in range(m): 40 | if j==signing_indices[i]: 41 | p = btc.privtopub(priv[i]) 42 | else: 43 | p = btc.privtopub(os.urandom(32)) 44 | p = btc.decode_pubkey(p) 45 | p = btc.encode_pubkey(p,'bin_compressed') 46 | pubkeys.append(binascii.hexlify(p)) 47 | f.write(','.join(pubkeys)+'\n') 48 | 49 | def import_keys(vf, sf=None): 50 | '''Import the verification keys; 51 | one list of pubkeys per loop in hex format, comma separated 52 | the first pubkey in each list is to be understood as the input 53 | for the connecting node. 54 | vf - the path to the file containing verification pubkeys 55 | sf - the path to the file containing signing keys, if applicable. 56 | Returns: 57 | vks - set of verification keys 58 | signing_indices - the indices corresponding to the private keys 59 | sks - the list of private keys 60 | ''' 61 | vks = {} 62 | with open(vf,'rb') as f: 63 | or_loops = f.readlines() 64 | for i,loop in enumerate(or_loops): 65 | raw_pks = loop.strip().split(',') 66 | vks[i] = [btc.decode_pubkey(pk) for pk in raw_pks] 67 | if not sf: 68 | return (vks, None, None) 69 | #import the signing private keys; 70 | #at least one per loop 71 | with open(sf,'rb') as f: 72 | raw_sks = f.readlines() 73 | sks = [binascii.unhexlify(priv.strip()) for priv in raw_sks] 74 | #check that the signing keys have the right configuration 75 | if not len(sks)==len(vks): 76 | raise Exception("Need "+str(len(vks))+" signing keys for a valid signature, got "+str(len(sks))) 77 | sk_pks = [btc.decode_pubkey(btc.privtopub(sk)) for sk in sks] 78 | signing_indices = [] 79 | for loop in vks: 80 | if not len(set(vks[loop]).intersection(set(sk_pks)))==1: 81 | raise Exception("Verification key loop does not contain a signing key.") 82 | signing_indices.append([x for x,item in enumerate(vks[loop]) if item in sk_pks][0]) 83 | #signing keys have right configuration relative to verification keys. 84 | return (vks, signing_indices, sks) 85 | 86 | def get_kG(e, P, s=None): 87 | '''Use EC operation: kG = sG +eP. 88 | If s (signature) is not provided, it is generated 89 | randomly and returned. 90 | e - hash value, 32 bytes binary 91 | P - verification pubkey 92 | s - 32 bytes binary''' 93 | if not s: 94 | s = os.urandom(32) 95 | sG = btc.fast_multiply(btc.G,btc.decode(s,256)) 96 | eP = btc.fast_multiply(P,btc.decode(e,256)) 97 | return (btc.fast_add(sG, eP), s) 98 | 99 | def get_sig_message(m, vks): 100 | '''The full message is a sha256 hash 101 | of the message string concatenated with the 102 | compressed binary format of all of the verification keys.''' 103 | full_message = m 104 | for loop in vks: 105 | for p in vks[loop]: 106 | full_message += btc.encode_pubkey(p,'bin_compressed') 107 | return sha256(full_message).digest() 108 | 109 | def encode_sig(e, s): 110 | sig = e 111 | for a in sorted(s): 112 | for b in s[a]: 113 | sig += b 114 | return sig 115 | 116 | def decode_sig(sig, keyset, fmt='bin'): 117 | ''' 118 | Signature format: e0 (the hash value for the zero index for all loops) 119 | then s(i,j) , i range over the number of loops, 120 | j range over the number of verification keys in the ith loop. 121 | All 1+i*j values are 32 byte binary variables.''' 122 | if fmt != 'bin': 123 | sig = binascii.unhexlify(sig) 124 | e0 = sig[:32] 125 | s = {} 126 | c = 32 127 | for i in range(len(keyset)): 128 | s[i]=[None]*len(keyset[i]) 129 | for j in range(len(keyset[i])): 130 | s[i][j] = sig[c:c+32] 131 | c+=32 132 | return (e0, s) 133 | 134 | if __name__ == '__main__': 135 | parser = OptionParser(usage='usage: python borring.py [options] message', 136 | description='A simple demonstration of Borromean ring signatures using Bitcoin keys.'+ 137 | ' The verification keys file format is: pubkey1,pubkey2,... \\n pubkey1, pubkey2,..' + 138 | ' where the first pubkey on each line is intended to point to the same node on the directed graph'+ 139 | ' (see Fig 2 in the Borromean ring signatures paper).') 140 | parser.add_option('-v', '--verify-keys-file', action='store', dest='verify_keys_file', 141 | default='verifykeys.txt', help='path to file containing Bitcoin public keys in hex, one per line') 142 | parser.add_option('-s', '--sign-keys-file', action='store', dest='sign_keys_file', 143 | default='signkeys.txt', help='path to file containing signing private keys, in hex') 144 | parser.add_option('-g','--generate-keys', action='store_true',dest='generate_keys', 145 | default=False,help='generate a set of dummy public verification and'+ 146 | ' private signing keys for testing') 147 | parser.add_option('-N', '--num-rings', action='store', type='int',dest='nrings', 148 | default=5, help='specify number of key rings, to be used with -g') 149 | parser.add_option('-M', '--keys-per-ring', action='store', type='int', dest='keys_per_ring', 150 | default=6, help='specify number of verification keys per ring (same for each), to be used with -g') 151 | parser.add_option('-w','--write-sig', action='store',dest='write_sig_file', 152 | help='write signature to file') 153 | parser.add_option('-e','--verify-sig', action='store', dest='read_sig_file', 154 | help='verify a signature in the specified file, using the verify keys specified in -v') 155 | parser.add_option('-a','--message-file',action='store', dest='message_file', 156 | help='give path to file where message to be signed is stored; alternative to passing message on command line.') 157 | parser.add_option('-o','--print-modified-message', action='store_true', dest='mod_message', 158 | default=False,help='Print the augmented message, which hashes the original message with the verification'+ 159 | ' keys as specified by -v.') 160 | (options, args) = parser.parse_args() 161 | 162 | if options.generate_keys: 163 | print 'generating' 164 | generate_keyfiles(options.nrings, options.keys_per_ring, options.verify_keys_file, options.sign_keys_file) 165 | exit(0) 166 | try: 167 | if options.message_file: 168 | with open(options.message_file,"rb") as f: 169 | message = binascii.unhexlify(f.read()) 170 | else: 171 | message = args[0] 172 | except: 173 | parser.error('Provide a single string argument as a message, or specify message file with -a') 174 | 175 | # 176 | #vks: the set of all verification public keys 177 | #signing_indices: the index of the vertex for which we have the private key, for each OR loop 178 | #sks: the set of signing keys (set to None for verification case) 179 | if options.read_sig_file: options.sign_keys_file = None 180 | vks, signing_indices, sks = import_keys(options.verify_keys_file, options.sign_keys_file) 181 | 182 | #construct message to be signed 183 | if not options.message_file: 184 | M = get_sig_message(message, vks) 185 | else: 186 | M = message 187 | print 'working with message' 188 | print binascii.hexlify(M) 189 | 190 | #if option selected, simply print out message which has pubkey commitments 191 | if options.mod_message: 192 | print get_sig_message(message, vks) 193 | exit(0) 194 | 195 | if not options.read_sig_file: 196 | #sign operation 197 | 198 | #the set of all (Borromean) hash values; 199 | #the vertices of the graph 200 | e = {} 201 | 202 | #the set of all signature values (s(i,j)) 203 | s = {} 204 | 205 | #for each OR loop, the index at which we start 206 | #the signature calculations, the one after the 207 | #the signing index (the one we have the privkey for) 208 | start_index = {} 209 | 210 | #the random value set as the k-value for the signing index 211 | k = [os.urandom(32) for i in range(len(vks))] 212 | to_be_hashed = '' 213 | for i, loop in enumerate(vks): 214 | e[i]=[None]*len(vks[loop]) 215 | s[i] = [None]*len(vks[loop]) 216 | kG = btc.fast_multiply(btc.G, btc.decode(k[i],256)) 217 | start_index[i] = (signing_indices[i]+1)%len(vks[loop]) 218 | 219 | if start_index[i]==0: 220 | to_be_hashed += btc.encode_pubkey(kG,'bin_compressed') 221 | #in this case, there are no more vertices to process in the first stage 222 | continue 223 | 224 | e[i][start_index[i]] = borr_hash(M, kG, i, start_index[i]) 225 | for x in range(start_index[i]+1,len(vks[loop])): 226 | y,s[i][x-1] = get_kG(e[i][x-1], vks[i][x-1]) 227 | e[i][x] = borr_hash(M,y,i,x) 228 | 229 | #kGend is the EC point corresponding to the k-value 230 | #for the vertex before zero, which will be included in the hash for e0 231 | kGend, s[i][len(vks[i])-1] = get_kG(e[i][len(vks[i])-1],vks[i][len(vks[i])-1]) 232 | to_be_hashed += btc.encode_pubkey(kGend,'bin_compressed') 233 | 234 | #note that e[i][0] is calculated as a borromean hash of e0, it is not equal to e0 235 | to_be_hashed += M 236 | e0 = sha256(to_be_hashed).digest() 237 | for i in range(len(vks)): 238 | e[i][0] = borr_hash(M,e0,i,0,is_pubkey=False) 239 | #continue processing for all vertices after the 0-th, up 240 | #to the signing index. 241 | for i,loop in enumerate(vks): 242 | for x in range(1,signing_indices[i]+1): 243 | y, s[i][x-1] = get_kG(e[i][x-1],vks[i][x-1]) 244 | e[i][x] = borr_hash(M,y,i,x) 245 | #finally, set the signature at the signing index using the privkey 246 | s[i][signing_indices[i]] = \ 247 | btc.encode((btc.decode(k[i],256) - \ 248 | (btc.decode(sks[i],256))*(btc.decode(e[i][signing_indices[i]],256)))%btc.N, 256) 249 | final_sig = encode_sig(e0,s) 250 | if options.write_sig_file: 251 | print 'writing sig to file: '+options.write_sig_file 252 | print 'signature length: '+str(len(final_sig)) 253 | with open(options.write_sig_file,'wb') as f: 254 | f.write(final_sig) 255 | else: 256 | #verify operation 257 | #as noted in the paper, this operation is much simpler 258 | #than signing as we don't treat the signing index as distinct. 259 | #Because we don't know it! 260 | with open(options.read_sig_file,'rb') as f: 261 | sig = f.read() 262 | fmt = 'hex' if options.message_file else 'bin' 263 | received_sig = decode_sig(sig, vks, fmt=fmt) 264 | e = {} 265 | e0, s = received_sig 266 | r0s = '' 267 | for i, loop in enumerate(vks): 268 | e[i] = [None]*len(vks[loop]) 269 | e[i][0] = borr_hash(M,e0,i,0,is_pubkey=False) 270 | for j in range(len(vks[loop])): 271 | #as in signing, r is the EC point corresponding 272 | #to the k-value for the last vertex before 0. 273 | r,dummy = get_kG(e[i][j],vks[i][j],s=s[i][j]) 274 | if j!=len(vks[loop])-1: 275 | e[i][j+1] = borr_hash(M,r,i,j+1) 276 | else: 277 | r0s += btc.encode_pubkey(r,'bin_compressed') 278 | if not e0==sha256(r0s + M).digest(): 279 | print 'verification failed' 280 | else: 281 | print 'verification success' 282 | 283 | print binascii.hexlify(e0) 284 | print binascii.hexlify(sha256(r0s + M).digest()) 285 | 286 | 287 | 288 | 289 | --------------------------------------------------------------------------------