raw
v98                     1 #!/usr/bin/env python
v99 2
v99 3 ##############################################################################
v99 4 # Quick Intro:
v99 5 # 1) Create '.wot' in your home directory. Fill it with public keys from 'wot'.
v99 6 # 2) Create '.seals' in your home directory. Place all signatures there from 'sigs'.
v99 7 # 3) Create a 'patches' directory somewhere where 'v' can find it. Or use this one.
v99 8 # 4) ./v.py patches command
v99 9 # e.g.,
v99 10 # ./v.py patches w
v99 11 # ^^ displays WoT
v99 12 # ./v.py patches p patches/asciilifeform_add_verifyall_option.vpatch asciis_bleedingedge
v99 13 # ^^ this 'presses' (creates the actual tree)
v99 14 # ^^ approximately like a 'checkout' in your vanilla flavoured shithub.
v99 15
v99 16 ##############################################################################
v99 17
v98 18 import os, sys, shutil, argparse, re, tempfile, gnupg, subprocess
v99 19
v99 20 ##############################################################################
v98 21 vver = 98 # This program's Kelvin version.
v99 22
v99 23 ## HOW YOU CAN HELP: ##
v99 24
v99 25 # * TESTS plox, ty!
v99 26 #
v99 27 # Report findings in #bitcoin-assets on Freenode.
v99 28
v99 29 ##############################################################################
v99 30
v99 31 prolog = '''\
v99 32 (C) 2015 NoSuchlAbs.
v99 33 You do not have, nor can you ever acquire the right to use, copy or distribute
v99 34 this software ; Should you use this software for any purpose, or copy and
v99 35 distribute it to anyone or in any manner, you are breaking the laws of whatever
v99 36 soi-disant jurisdiction, and you promise to continue doing so for the indefinite
v99 37 future. In any case, please always : read and understand any software ;
v99 38 verify any PGP signatures that you use - for any purpose.
v99 39 '''
v99 40
v99 41 intro = "V (ver. {0}K)\n".format(vver)
v99 42
v99 43 ##############################################################################
v99 44 def toposort(unsorted):
v99 45 sorted = []
v99 46 unsorted = dict(unsorted)
v99 47 while unsorted:
v99 48 acyclic = False
v99 49 for node, edges in unsorted.items():
v99 50 for edge in edges:
v99 51 if edge in unsorted:
v99 52 break
v99 53 else:
v99 54 acyclic = True
v99 55 del unsorted[node]
v99 56 sorted.append((node, edges))
v99 57 if not acyclic:
v99 58 fatal("Cyclic graph!")
v99 59 return sorted
v99 60 ##############################################################################
v99 61
v98 62 vpatch_path = "vpatch"
v99 63 verbose = False
v99 64
v99 65 def fatal(msg):
v99 66 sys.stderr.write(msg + "\n")
v99 67 exit(1)
v99 68
v99 69 def spew(msg):
v99 70 if verbose:
v99 71 print msg
v99 72
v99 73 # List of files in a directory, in lexical order.
v99 74 def dir_files(dir):
v99 75 return sorted([os.path.join(dir, fn) for fn in next(os.walk(dir))[2]])
v99 76
v99 77 # GPG is retarded and insists on 'keychain.'
v99 78 # This will be a temp dir, because we don't do any crypto.
v99 79 gpgtmp = tempfile.mkdtemp()
v99 80 gpg = gnupg.GPG(gnupghome=gpgtmp)
v99 81 gpg.encoding = 'utf-8'
v99 82
v99 83 # Known WoT public keys.
v99 84 pubkeys = {}
v99 85
v99 86 # The subset of vpatches that are considered valid.
v99 87 patches = []
v99 88
v99 89 # Banners (i.e. vpatches mapped to their guarantors)
v99 90 banners = {}
v99 91
v99 92 # Roots (i.e. vpatches parented by thin air)
v99 93 roots = []
v99 94
v99 95 # Table mapping file hash to originating vpatch
v99 96 desc = {}
v99 97 desc['false'] = 'false'
v99 98
v99 99
v99 100 # Grep for diff magics, and memoize
v99 101 def vpdata(path, exp, cache):
v99 102 l = cache.get(path)
v99 103 if not l:
v99 104 l = []
v99 105 patch = open(path, 'r').read()
v99 106 for m in re.findall(exp, patch, re.MULTILINE):
v99 107 l += [{'p':m[0], 'h':m[1]}]
v99 108 cache[path] = l
v99 109 return l
v99 110
v99 111 # Get parents of a vpatch
v99 112 pcache = {}
v99 113 def parents(vpatch):
v99 114 parents = vpdata(vpatch, r'^--- (\S+) (\S+)$', pcache)
v99 115 if not parents:
v99 116 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
v99 117 return parents
v99 118
v99 119 # Get children of a vpatch
v99 120 ccache = {}
v99 121 def children(vpatch):
v99 122 children = vpdata(vpatch, r'^\+\+\+ (\S+) (\S+)$', ccache)
v99 123 if not children:
v99 124 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
v99 125 # Record descendents:
v99 126 for child in children:
v99 127 h = child['h']
v99 128 if h != 'false':
v99 129 desc[h] = vpatch
v99 130 return children
v99 131
v99 132 # It is entirely possible to have more than one root!
v99 133 # ... exactly how, is left as an exercise for readers.
v99 134 def find_roots(patchset):
v99 135 rset = []
v99 136 # Walk, find roots
v99 137 for p in patchset:
v99 138 if all(p['h'] == 'false' for p in parents(p)):
v99 139 rset += [p]
v99 140 spew("Found a Root: '{0}'".format(p))
v99 141 return rset
v99 142
v99 143 # Get antecedents.
v99 144 def get_ante(vpatch):
v99 145 ante = {}
v99 146 for p in parents(vpatch):
v99 147 pp = desc.get(p['h']) # Patch where this appears
v99 148 if not ante.get(pp):
v99 149 ante[pp] = []
v99 150 ante[pp] += [p['p']]
v99 151 return ante
v99 152
v99 153 # Get descendants.
v99 154 def get_desc(vpatch):
v99 155 des = {}
v99 156 for p in patches:
v99 157 ante = get_ante(p)
v99 158 if vpatch in ante.keys():
v99 159 des[p] = ante[vpatch]
v99 160 return des
v99 161
v99 162 ##############################################################################
v99 163
v99 164 # Print name of patch and its guarantors, or 'WILD' if none known.
v99 165 def disp_vp(vpatch):
v99 166 seals = ', '.join(map(str, banners[vpatch]))
v99 167 if seals == '':
v99 168 seals = 'WILD'
v99 169 return "{0} ({1})".format(vpatch, seals)
v99 170
v99 171 ##############################################################################
v99 172
v99 173 # Command: WoT
v99 174 def c_wot(args):
v99 175 for k in pubkeys.values():
v99 176 print "{0}:{1} ({2})".format(k['handle'], k['fp'], k['id'])
v99 177
v99 178 # Command: Flow
v99 179 def c_flow(args):
v99 180 for p in patches:
v99 181 print disp_vp(p)
v99 182
v99 183 # Command: Roots.
v99 184 def c_roots(args):
v99 185 for r in roots:
v99 186 print "Root: " + disp_vp(r)
v99 187
v99 188 # Command: Antecedents.
v99 189 def c_ante(args):
v99 190 ante = get_ante(args.query)
v99 191 for p in ante.keys():
v99 192 if p != 'false':
v99 193 print "{0} [{1}]".format(disp_vp(p), '; '.join(map(str, ante[p])))
v99 194
v99 195 # Command: Descendants
v99 196 def c_desc(args):
v99 197 des = get_desc(args.query)
v99 198 for d in des.keys():
v99 199 print "Descendant: {0} [{1}]".format(disp_vp(d), '; '.join(map(str, des[d])))
v99 200
v99 201 # Command: Press.
v99 202 def c_press(args):
v99 203 print "Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
v99 204 headpos = patches.index(args.head)
v99 205 seq = patches[:headpos + 1]
v98 206 if os.path.exists(args.dest):
v98 207 print "Warning: target {0} already exists".format(args.dest)
v98 208 else:
v98 209 os.mkdir(args.dest)
v99 210 for p in seq:
v99 211 print "Using: {0}".format(disp_vp(p))
v98 212 out = subprocess.Popen([vpatch_path], cwd=args.dest, stdin=subprocess.PIPE,
v98 213 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
v98 214 with open(p, "r") as inp:
v98 215 body = inp.read()
v98 216 stdout, stderr = out.communicate(body)
v98 217 print stdout,
v98 218 if out.returncode != 0:
v98 219 fatal(("-----------------------------------\n" +
v98 220 "There was an error while pressing:\n" +
v98 221 "{0}\n" +
v98 222 "Result in {1} might be in an invalid state!\n" +
v98 223 "-----------------------------------").format(stderr.strip(), args.dest))
v99 224 print "Completed Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
v99 225
v99 226 # Command: Origin.
v99 227 def c_origin(args):
v99 228 o = desc.get(args.query)
v99 229 if o:
v99 230 print disp_vp(o)
v99 231 else:
v99 232 print "No origin known."
v99 233
v99 234 ##############################################################################
v99 235
v99 236 ##############################################################################
v99 237 # Command line parameter processor.
v99 238 parser = argparse.ArgumentParser(description=intro, epilog=prolog)
v99 239
v99 240 # Print paths, etc
v99 241 parser.add_argument('-v', dest='verbose', default=False,
v99 242 action="store_true", help='Verbose.')
v99 243
v99 244 # Permit the use of patches no one has yet sealed. Use this ONLY for own dev work!
v99 245 parser.add_argument('-wild', dest='wild', default=False,
v99 246 action="store_true", help='Permit wild (UNSEALED!) vpatches.')
v99 247
v99 248 # Glom keyid (short fingerprint) onto every WoT handle.
v99 249 parser.add_argument('-fingers', dest='fingers', default=False,
v99 250 action="store_true", help='Prefix keyid to all WoT handles.')
v99 251
v99 252 # Default path of WoT public keys is /home/yourusername/.wot
v99 253 # This dir must exist. Alternatively, you may specify another.
v99 254 parser.add_argument('--wot', dest='wot', default=os.path.join(os.path.expanduser('~'), '.wot'),
v99 255 action="store", help='Use WoT in given directory. (Default: ~/.wot)')
v99 256
v99 257 # Default path of the seals (PGP signatures) is /home/yourusername/.seals
v99 258 # This dir must exist. Alternatively, you may specify another.
v99 259 parser.add_argument('--seals', dest='seals', default=os.path.join(os.path.expanduser('~'), '.seals'),
v99 260 action="store", help='Use Seals in given directory. (Default: ~/.seals)')
v99 261
v99 262 # REQUIRED: Path of directory with vpatches.
v99 263 parser.add_argument('vpatches', help='Vpatch directory to operate on. [REQUIRED]')
v99 264
v99 265 # REQUIRED: Command.
v99 266 subparsers = parser.add_subparsers(help='Command [REQUIRED]')
v99 267
v99 268 parser_w = subparsers.add_parser('w', help='Display WoT.')
v99 269 parser_w.set_defaults(f=c_wot)
v99 270
v99 271 parser_r = subparsers.add_parser('r', help='Display Roots.')
v99 272 parser_r.set_defaults(f=c_roots)
v99 273
v99 274 parser_a = subparsers.add_parser('a', help='Display Antecedents [PATCH]')
v99 275 parser_a.set_defaults(f=c_ante)
v99 276 parser_a.add_argument('query', action="store", help='Patch.')
v99 277
v99 278 parser_d = subparsers.add_parser('d', help='Display Descendants [PATCH]')
v99 279 parser_d.set_defaults(f=c_desc)
v99 280 parser_d.add_argument('query', action="store", help='Patch.')
v99 281
v99 282 parser_l = subparsers.add_parser('f', help='Compute Flow.')
v99 283 parser_l.set_defaults(f=c_flow)
v99 284
v99 285 parser_p = subparsers.add_parser('p', help='Press [HEADPATCH AND DESTINATION]')
v99 286 parser_p.set_defaults(f=c_press)
v99 287 parser_p.add_argument('head', action="store", help='Head patch.')
v99 288 parser_p.add_argument('dest', action="store", help='Destionation directory.')
v99 289
v99 290 parser_o = subparsers.add_parser('o', help='Find Origin [SHA512]')
v99 291 parser_o.set_defaults(f=c_origin)
v99 292 parser_o.add_argument('query', action="store", help='SHA512 to search for.')
v99 293
v99 294
v99 295 ##############################################################################
v99 296
v99 297 # V cannot operate without vpatches, WoT, and Seals datasets.
v99 298 def reqdir(path):
v99 299 if (not (os.path.isdir(path))):
v99 300 fatal("Directory '{0}' does not exist!".format(path))
v99 301 return path
v99 302
v99 303
v99 304 def main():
v99 305 global verbose, pubkeys, patches, roots, banners
v99 306
v99 307 args = parser.parse_args()
v99 308 verbose = args.verbose
v99 309
v99 310 # Patch and Sigs dirs
v99 311 pdir = reqdir(args.vpatches)
v99 312 sdir = reqdir(args.seals)
v99 313 wdir = reqdir(args.wot)
v99 314
v99 315 spew("Using patches from:" + pdir)
v99 316 spew("Using signatures from:" + sdir)
v99 317 spew("Using wot from:" + wdir)
v99 318
v99 319 pfiles = dir_files(pdir)
v99 320 sfiles = dir_files(sdir)
v99 321 wfiles = dir_files(wdir)
v99 322
v99 323 # Build WoT from pubkeys
v99 324 handle = {}
v99 325 for w in wfiles:
v99 326 pubkey = open(w, 'r').read()
v99 327 impkey = gpg.import_keys(pubkey)
v99 328 for fp in impkey.fingerprints:
v99 329 handle[fp] = os.path.splitext(os.path.basename(w))[0]
v99 330
v99 331 for k in gpg.list_keys():
v99 332 name = handle[k['fingerprint']]
v99 333 if args.fingers:
v99 334 name += '-' + k['keyid']
v98 335 uids = ', '.join(map(str, k['uids']))
v99 336 pubkeys[k['keyid']] = {'fp':k['fingerprint'],
v98 337 'id':uids,
v99 338 'handle':name}
v98 339 for subkey in k.get('subkeys', []):
v98 340 keyid, fingerprint = subkey[0], subkey[2]
v98 341 pubkeys[keyid] = {'fp':fingerprint,
v98 342 'id':uids,
v98 343 'handle':name}
v99 344
v99 345 # Validate seals
v99 346 for p in pfiles:
v99 347 pt = os.path.basename(p)
v99 348 banners[p] = []
v99 349 for s in sfiles:
v99 350 sig = os.path.basename(s)
v99 351 # All seals must take the form patchtitle.vpatch.yourname.sig
v99 352 if sig.find(pt) == 0: # substring of sig filename up through '.vpatch'
v99 353 v = gpg.verify_file(open(s, 'r'), data_filename=p)
v99 354 if v.valid:
v99 355 banners[p] += [pubkeys[v.key_id]['handle']]
v99 356 else:
v99 357 fatal("---------------------------------------------------------------------\n" +
v99 358 "WARNING: {0} is an INVALID seal for {1} !\n".format(sig, pt) +
v99 359 "Check that this user is in your WoT, and that this key has not expired.\n" +
v99 360 "Otherwise remove the invalid seal from your SEALS directory.\n" +
v99 361 "---------------------------------------------------------------------")
v99 362
v99 363 # Select the subset of vpatches currently in use.
v99 364 for p in pfiles:
v99 365 if banners.get(p) or args.wild:
v99 366 patches += [p]
v99 367 children(p) # Memoize.
v99 368 parents(p) # Memoize.
v99 369
v99 370 roots = find_roots(patches)
v99 371 if not roots:
v99 372 fatal('No roots found!')
v99 373
v99 374 # Topological ordering of flow graph
v99 375 l = []
v99 376 for p in patches:
v99 377 l += [(p, get_desc(p).keys())]
v99 378 s = map(lambda x:x[0], toposort(l))
v99 379 patches = s[::-1]
v99 380
v99 381 # Run command
v99 382 args.f(args)
v99 383
v99 384 # Remove temporary keychain
v99 385 shutil.rmtree(gpgtmp)
v99 386
v99 387 ##############################################################################
v99 388
v99 389 if __name__ == '__main__' :
v99 390 main()
v99 391
v99 392 ##############################################################################