v99 1 #!/usr/bin/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
v99 18 import os, sys, shutil, argparse, re, tempfile, gnupg
v99 19
v99 20 ##############################################################################
v99 21 vver = 100 # 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
v99 62 verbose = False
v99 63
v99 64 def fatal(msg):
v99 65 sys.stderr.write(msg + "\n")
v99 66 exit(1)
v99 67
v99 68 def spew(msg):
v99 69 if verbose:
v99 70 print msg
v99 71
v99 72 # List of files in a directory, in lexical order.
v99 73 def dir_files(dir):
v99 74 return sorted([os.path.join(dir, fn) for fn in next(os.walk(dir))[2]])
v99 75
v99 76 # GPG is retarded and insists on 'keychain.'
v99 77 # This will be a temp dir, because we don't do any crypto.
v99 78 gpgtmp = tempfile.mkdtemp()
v99 79 gpg = gnupg.GPG(gnupghome=gpgtmp)
v99 80 gpg.encoding = 'utf-8'
v99 81
v99 82 # Known WoT public keys.
v99 83 pubkeys = {}
v99 84
v99 85 # The subset of vpatches that are considered valid.
v99 86 patches = []
v99 87
v99 88 # Banners (i.e. vpatches mapped to their guarantors)
v99 89 banners = {}
v99 90
v99 91 # Roots (i.e. vpatches parented by thin air)
v99 92 roots = []
v99 93
v99 94 # Table mapping file hash to originating vpatch
v99 95 desc = {}
v99 96 desc['false'] = 'false'
v99 97
v99 98
v99 99 # Grep for diff magics, and memoize
v99 100 def vpdata(path, exp, cache):
v99 101 l = cache.get(path)
v99 102 if not l:
v99 103 l = []
v99 104 patch = open(path, 'r').read()
v99 105 for m in re.findall(exp, patch, re.MULTILINE):
v99 106 l += [{'p':m[0], 'h':m[1]}]
v99 107 cache[path] = l
v99 108 return l
v99 109
v99 110 # Get parents of a vpatch
v99 111 pcache = {}
v99 112 def parents(vpatch):
v99 113 parents = vpdata(vpatch, r'^--- (\S+) (\S+)$', pcache)
v99 114 if not parents:
v99 115 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
v99 116 return parents
v99 117
v99 118 # Get children of a vpatch
v99 119 ccache = {}
v99 120 def children(vpatch):
v99 121 children = vpdata(vpatch, r'^\+\+\+ (\S+) (\S+)$', ccache)
v99 122 if not children:
v99 123 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
v99 124 # Record descendents:
v99 125 for child in children:
v99 126 h = child['h']
v99 127 if h != 'false':
v99 128 desc[h] = vpatch
v99 129 return children
v99 130
v99 131 # It is entirely possible to have more than one root!
v99 132 # ... exactly how, is left as an exercise for readers.
v99 133 def find_roots(patchset):
v99 134 rset = []
v99 135 # Walk, find roots
v99 136 for p in patchset:
v99 137 if all(p['h'] == 'false' for p in parents(p)):
v99 138 rset += [p]
v99 139 spew("Found a Root: '{0}'".format(p))
v99 140 return rset
v99 141
v99 142 # Get antecedents.
v99 143 def get_ante(vpatch):
v99 144 ante = {}
v99 145 for p in parents(vpatch):
v99 146 pp = desc.get(p['h']) # Patch where this appears
v99 147 if not ante.get(pp):
v99 148 ante[pp] = []
v99 149 ante[pp] += [p['p']]
v99 150 return ante
v99 151
v99 152 # Get descendants.
v99 153 def get_desc(vpatch):
v99 154 des = {}
v99 155 for p in patches:
v99 156 ante = get_ante(p)
v99 157 if vpatch in ante.keys():
v99 158 des[p] = ante[vpatch]
v99 159 return des
v99 160
v99 161 ##############################################################################
v99 162
v99 163 # Print name of patch and its guarantors, or 'WILD' if none known.
v99 164 def disp_vp(vpatch):
v99 165 seals = ', '.join(map(str, banners[vpatch]))
v99 166 if seals == '':
v99 167 seals = 'WILD'
v99 168 return "{0} ({1})".format(vpatch, seals)
v99 169
v99 170 ##############################################################################
v99 171
v99 172 # Command: WoT
v99 173 def c_wot(args):
v99 174 for k in pubkeys.values():
v99 175 print "{0}:{1} ({2})".format(k['handle'], k['fp'], k['id'])
v99 176
v99 177 # Command: Flow
v99 178 def c_flow(args):
v99 179 for p in patches:
v99 180 print disp_vp(p)
v99 181
v99 182 # Command: Roots.
v99 183 def c_roots(args):
v99 184 for r in roots:
v99 185 print "Root: " + disp_vp(r)
v99 186
v99 187 # Command: Antecedents.
v99 188 def c_ante(args):
v99 189 ante = get_ante(args.query)
v99 190 for p in ante.keys():
v99 191 if p != 'false':
v99 192 print "{0} [{1}]".format(disp_vp(p), '; '.join(map(str, ante[p])))
v99 193
v99 194 # Command: Descendants
v99 195 def c_desc(args):
v99 196 des = get_desc(args.query)
v99 197 for d in des.keys():
v99 198 print "Descendant: {0} [{1}]".format(disp_vp(d), '; '.join(map(str, des[d])))
v99 199
v99 200 # Command: Press.
v99 201 def c_press(args):
v99 202 print "Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
v99 203 headpos = patches.index(args.head)
v99 204 seq = patches[:headpos + 1]
v99 205 os.mkdir(args.dest)
v99 206 for p in seq:
v99 207 print "Using: {0}".format(disp_vp(p))
v99 208 os.system("patch -E --dir {0} -p1 < {1}".format(args.dest, p))
v99 209 print "Completed Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
v99 210
v99 211 # Command: Origin.
v99 212 def c_origin(args):
v99 213 o = desc.get(args.query)
v99 214 if o:
v99 215 print disp_vp(o)
v99 216 else:
v99 217 print "No origin known."
v99 218
v99 219 ##############################################################################
v99 220
v99 221 ##############################################################################
v99 222 # Command line parameter processor.
v99 223 parser = argparse.ArgumentParser(description=intro, epilog=prolog)
v99 224
v99 225 # Print paths, etc
v99 226 parser.add_argument('-v', dest='verbose', default=False,
v99 227 action="store_true", help='Verbose.')
v99 228
v99 229 # Permit the use of patches no one has yet sealed. Use this ONLY for own dev work!
v99 230 parser.add_argument('-wild', dest='wild', default=False,
v99 231 action="store_true", help='Permit wild (UNSEALED!) vpatches.')
v99 232
v99 233 # Glom keyid (short fingerprint) onto every WoT handle.
v99 234 parser.add_argument('-fingers', dest='fingers', default=False,
v99 235 action="store_true", help='Prefix keyid to all WoT handles.')
v99 236
v99 237 # Default path of WoT public keys is /home/yourusername/.wot
v99 238 # This dir must exist. Alternatively, you may specify another.
v99 239 parser.add_argument('--wot', dest='wot', default=os.path.join(os.path.expanduser('~'), '.wot'),
v99 240 action="store", help='Use WoT in given directory. (Default: ~/.wot)')
v99 241
v99 242 # Default path of the seals (PGP signatures) is /home/yourusername/.seals
v99 243 # This dir must exist. Alternatively, you may specify another.
v99 244 parser.add_argument('--seals', dest='seals', default=os.path.join(os.path.expanduser('~'), '.seals'),
v99 245 action="store", help='Use Seals in given directory. (Default: ~/.seals)')
v99 246
v99 247 # REQUIRED: Path of directory with vpatches.
v99 248 parser.add_argument('vpatches', help='Vpatch directory to operate on. [REQUIRED]')
v99 249
v99 250 # REQUIRED: Command.
v99 251 subparsers = parser.add_subparsers(help='Command [REQUIRED]')
v99 252
v99 253 parser_w = subparsers.add_parser('w', help='Display WoT.')
v99 254 parser_w.set_defaults(f=c_wot)
v99 255
v99 256 parser_r = subparsers.add_parser('r', help='Display Roots.')
v99 257 parser_r.set_defaults(f=c_roots)
v99 258
v99 259 parser_a = subparsers.add_parser('a', help='Display Antecedents [PATCH]')
v99 260 parser_a.set_defaults(f=c_ante)
v99 261 parser_a.add_argument('query', action="store", help='Patch.')
v99 262
v99 263 parser_d = subparsers.add_parser('d', help='Display Descendants [PATCH]')
v99 264 parser_d.set_defaults(f=c_desc)
v99 265 parser_d.add_argument('query', action="store", help='Patch.')
v99 266
v99 267 parser_l = subparsers.add_parser('f', help='Compute Flow.')
v99 268 parser_l.set_defaults(f=c_flow)
v99 269
v99 270 parser_p = subparsers.add_parser('p', help='Press [HEADPATCH AND DESTINATION]')
v99 271 parser_p.set_defaults(f=c_press)
v99 272 parser_p.add_argument('head', action="store", help='Head patch.')
v99 273 parser_p.add_argument('dest', action="store", help='Destionation directory.')
v99 274
v99 275 parser_o = subparsers.add_parser('o', help='Find Origin [SHA512]')
v99 276 parser_o.set_defaults(f=c_origin)
v99 277 parser_o.add_argument('query', action="store", help='SHA512 to search for.')
v99 278
v99 279
v99 280 ##############################################################################
v99 281
v99 282 # V cannot operate without vpatches, WoT, and Seals datasets.
v99 283 def reqdir(path):
v99 284 if (not (os.path.isdir(path))):
v99 285 fatal("Directory '{0}' does not exist!".format(path))
v99 286 return path
v99 287
v99 288
v99 289 def main():
v99 290 global verbose, pubkeys, patches, roots, banners
v99 291
v99 292 args = parser.parse_args()
v99 293 verbose = args.verbose
v99 294
v99 295 # Patch and Sigs dirs
v99 296 pdir = reqdir(args.vpatches)
v99 297 sdir = reqdir(args.seals)
v99 298 wdir = reqdir(args.wot)
v99 299
v99 300 spew("Using patches from:" + pdir)
v99 301 spew("Using signatures from:" + sdir)
v99 302 spew("Using wot from:" + wdir)
v99 303
v99 304 pfiles = dir_files(pdir)
v99 305 sfiles = dir_files(sdir)
v99 306 wfiles = dir_files(wdir)
v99 307
v99 308 # Build WoT from pubkeys
v99 309 handle = {}
v99 310 for w in wfiles:
v99 311 pubkey = open(w, 'r').read()
v99 312 impkey = gpg.import_keys(pubkey)
v99 313 for fp in impkey.fingerprints:
v99 314 handle[fp] = os.path.splitext(os.path.basename(w))[0]
v99 315
v99 316 for k in gpg.list_keys():
v99 317 name = handle[k['fingerprint']]
v99 318 if args.fingers:
v99 319 name += '-' + k['keyid']
v99 320 pubkeys[k['keyid']] = {'fp':k['fingerprint'],
v99 321 'id':', '.join(map(str, k['uids'])),
v99 322 'handle':name}
v99 323
v99 324 # Validate seals
v99 325 for p in pfiles:
v99 326 pt = os.path.basename(p)
v99 327 banners[p] = []
v99 328 for s in sfiles:
v99 329 sig = os.path.basename(s)
v99 330 # All seals must take the form patchtitle.vpatch.yourname.sig
v99 331 if sig.find(pt) == 0: # substring of sig filename up through '.vpatch'
v99 332 v = gpg.verify_file(open(s, 'r'), data_filename=p)
v99 333 if v.valid:
v99 334 banners[p] += [pubkeys[v.key_id]['handle']]
v99 335 else:
v99 336 fatal("---------------------------------------------------------------------\n" +
v99 337 "WARNING: {0} is an INVALID seal for {1} !\n".format(sig, pt) +
v99 338 "Check that this user is in your WoT, and that this key has not expired.\n" +
v99 339 "Otherwise remove the invalid seal from your SEALS directory.\n" +
v99 340 "---------------------------------------------------------------------")
v99 341
v99 342 # Select the subset of vpatches currently in use.
v99 343 for p in pfiles:
v99 344 if banners.get(p) or args.wild:
v99 345 patches += [p]
v99 346 children(p) # Memoize.
v99 347 parents(p) # Memoize.
v99 348
v99 349 roots = find_roots(patches)
v99 350 if not roots:
v99 351 fatal('No roots found!')
v99 352
v99 353 # Topological ordering of flow graph
v99 354 l = []
v99 355 for p in patches:
v99 356 l += [(p, get_desc(p).keys())]
v99 357 s = map(lambda x:x[0], toposort(l))
v99 358 patches = s[::-1]
v99 359
v99 360 # Run command
v99 361 args.f(args)
v99 362
v99 363 # Remove temporary keychain
v99 364 shutil.rmtree(gpgtmp)
v99 365
v99 366 ##############################################################################
v99 367
v99 368 if __name__ == '__main__' :
v99 369 main()
v99 370
v99 371 ##############################################################################