wotsap-0.7/0000755000175000017500000000000010656267766011060 5ustar jczjczwotsap-0.7/ChangeLog0000644000175000017500000000462610656267766012642 0ustar jczjcz 2007-08-07 Jörgen Cederlöf Release 0.7 Most of these changes are somewhat old and have been running on the online service for quite some time. * pks2wot: - Some typo fixes. - Check expiration of signatures. - Removed Zimmerman's key 0x63CB691DFAEBD5FC from startkeys, since it has signed noone. - Use pksclient --version. * wotsap: - Some URL changes in documentation texts. - Open files in binary mode, to make it work on Microsoft systems. - Use unified_diff instead of ndiff for debug info. ndiff is very very slow (years?) for the debug data. - Optional email obfuscation - Show information about reaching/reachable/strong set with modstring. - Added --size option. - Added --nounknowns option. - Made --group do search if key string doesn't contain a comma. - Some small improvements of e.g. error handling. - Optionally use TrueType font. 2004-12-12 Jörgen Cederlöf Release 0.6 Loads of changes, too many to list in detail, most of them actually implemented some time ago. Everything is optimized for Python 2.3 and should work on Python 2.4 but still has compatibility for Python 2.2. * pks2wot: - All keys and signatures verified with GnuPG - Always use primary user ID - Indicate if primary user ID is signed - Save signature type - New .wot file format including signature types - Safe caching and other speedups * wotsap: - Handle new .wot file format - Most Wanted patch by Marco Bodrato - Group matrix - Modstrings - Handles signature types - Better command line options handling - Can show all differences between two .wot files - No external font files are needed wotsap-0.7/pks2wot0000755000175000017500000011000310656267766012412 0ustar jczjcz#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # pks2wot, create .wot file from pks server. # Part of Web of trust statistics and pathfinder, Wotsap # http://www.lysator.liu.se/~jc/wotsap/ # Copyright (C) 2003,2004 Jörgen Cederlöf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. from __future__ import division # Try to make error messages from old Python versions show something # meaningful. from __future__ import generators def __tmp(): yield \ '*********** Too old Python. You need at least version 2.2.0. ***********' del __tmp version = "pks2wot.py version 0.6" import sys import os import string import struct import time import random import tempfile import types __debugstr = "" def debug(string): global __debugstr __debugstr += string + "\n" print string sys.stdout.flush() benchmarktimes, tmpbenchmarktimes = {}, {} def benchmark(startstopreport, str="default", append=""): global benchmarktimes, tmpbenchmarktimes if startstopreport == "start": if str in tmpbenchmarktimes: debug('BENCHMARK ERROR: Clock "%s" was started twice.' % str) tmpbenchmarktimes[str] = time.time() elif startstopreport == "stop": logstr = str + append if logstr not in benchmarktimes: benchmarktimes[logstr] = [0, 0.] benchmarktimes[logstr][0] += 1 if str not in tmpbenchmarktimes: debug('BENCHMARK ERROR: Clock "%s" was stopped without being started.' % str) return benchmarktimes[logstr][1] += time.time() - tmpbenchmarktimes[str] del tmpbenchmarktimes[str] elif startstopreport == "tick": if str not in benchmarktimes: benchmarktimes[str] = [0, 0.] benchmarktimes[str][0] += 1 elif startstopreport == "report": if tmpbenchmarktimes: debug("BENCHMARK ERROR: All clocks were not stopped. Times may be wrong.") debug("tmpbenchmarktimes = %s" % str(tmpbenchmarktimes)) ret = ["Times:"] h = benchmarktimes.keys() h.sort() for x in h: ret.append(" %7d %10.2f %s" % (benchmarktimes[x][0], benchmarktimes[x][1], x)) return string.join(ret, "\n") else: debug("BENCHMARK ERROR: Unknown command %s." % startstopreport) return None def isvalidkeyid(keyid): if type(keyid) is not types.StringType: return 0 if keyid[0:2] != "0x": return 0 if len(keyid) != 18: return 0 for c in keyid[2:]: if c not in "0123456789ABCDEF": return 0 return 1 __keycache = [{}, [], 0] # This is the slow part. Fetch key, and use gpg to parse it. def getsinglekeyfrompks(args, keyid, tmpdir, verbose): """Get a key name and signatures through pksclient""" benchmark("start", "getsinglekeyfrompks()") verify = args['verify'] pksdb = args['pksdb'] pksclient = args['pksclient'] gnupg = args['gnupg'] nopkslogging = args['nopkslogging'] eightdigits = args['eightdigits'] cachesize = args['cachesize'] sigcache = args['sigcache'] newsigcache = args['newsigcache'] def splitcolons(string): """Split output from gpg --with-colons. Reverse the quoting from gpg/util/miscutil.c/print_string()""" # Benchmarking this might seem pointless, but emphasizes one # important point: Small string manipulating functions, even # though they do lots of splitting and replacing, doesn't take # any time worth mentioning and would therefore be pointless # to optimize. I like being reminded of this every time I see # the benchmark. benchmark("start", "splitcolons") # Reverse this: #for( ; n; n--, p++ ) # if( *p < 0x20 || (*p >= 0x7f && *p < 0xa0) || *p == delim || # (delim && *p=='\\')) { # putc('\\', fp); # if ( *p == '\n' ) putc('n', fp); # else if( *p == '\r' ) putc('r', fp); # else if( *p == '\f' ) putc('f', fp); # else if( *p == '\v' ) putc('v', fp); # else if( *p == '\b' ) putc('b', fp); # else if( !*p ) putc('0', fp); # else fprintf(fp, "x%02x", *p ); # } else putc(*p, fp); # Also see file DETAILS from GnuPG distribution fields = [] for x in string.split(":"): tmp = x .replace("\\n", "\n") \ .replace("\\r", "\r") \ .replace("\\f", "\f") \ .replace("\\v", "\v") \ .replace("\\b", "\b") \ .replace("\\0", "\x00")\ .split("\\") s = tmp[0] for y in tmp[1:]: assert y[0] == "x" assert len(y) >= 3 s += struct.pack('B', int(y[1:3],16)) + y[3:] fields.append(s) benchmark("stop", "splitcolons") return fields def call_pksclient_real(db, keyid, keyfile, append=0): v, a, pksflags = "2>/dev/null", ">", "" if verbose: v = "" if append: a = ">>" if nopkslogging: pksflags = "t" if eightdigits: keyid = "0x" + keyid[-8:] benchmark("start", "pksclient") benchmark("start", "pksclient: Key ") # Problem: if pksclient is stopped, it will often leave stale # lock files behind. We want to be able to stop the execution # with C-c, but that will stop pksclient. Therefore, we make # sure that pksclient has no controlling terminal. if not os.fork(): # We are a child, but we still have a controlling # terminal. Call setsid() to become group/session leader # and get rid of the controlling tty. os.setsid() cmd = "%s %s get %s b%s %s%s %s" % \ (pksclient, db, keyid, pksflags, a, keyfile, v) if verbose: print cmd ret = os.system(cmd) if ret>=256: ret //= 256 os._exit(ret) # _exit() won't call cleanup handlers. try: ret = os.wait()[1] except KeyboardInterrupt: print "Keyboard interrupt. Waiting for pksclient to finish." os.wait() print "OK, pksclient has finished. Exiting." sys.exit(1) if ret>=256: ret //= 256 benchmark("stop", "pksclient: Key ", "%sfound." % (ret and "not " or "")) benchmark("stop", "pksclient") return not ret def call_pksclient(db, keyid, keyfile, append=0): """A caching wrapper around the real call_pksclient().""" benchmark("start", "pksclientcache. Append: %s" % str(append)) cache = __keycache[0] if keyid in cache: if not cache[keyid]: benchmark("tick", "pksclientcache: Using cached non-existing key") ret = 0 else: benchmark("start", "pksclientcache: Using cached key") if append: mode = 'a' else: mode = 'w' open(keyfile, mode).write(cache[keyid]) ret = 1 benchmark("stop", "pksclientcache: Using cached key") else: benchmark("start", "pksclientcache: Calling pksclient") if append: # Open and seek to end of file f = open(keyfile, 'r') f.seek(0, 2) if not call_pksclient_real(db, keyid, keyfile, append): # We cache information about key being unavailable # forever - no need to remove it from the cache later, # it is only a few bytes. benchmark("tick", "pksclientcache: Caching non-existant key") cache[keyid] = None ret = 0 else: benchmark("start", "pksclientcache: Caching key") if append: # Read only new key cache[keyid] = f.read() else: cache[keyid] = open(keyfile, 'r').read() __keycache[1].append(keyid) __keycache[2] += len(cache[keyid]) while __keycache[2] > cachesize: # XXX What is the best strategy here? Let's just use # FIFO until we know what is best. # OK, this would probably be faster with a linked # list, but the list only contains keyids so it is # quite fast to copy. remove = __keycache[1].pop(0) __keycache[2] -= len(cache[remove]) del cache[remove] benchmark("tick", "pksclientcache: Expiring key from cache") ret = 1 benchmark("stop", "pksclientcache: Caching key") benchmark("stop", "pksclientcache: Calling pksclient") benchmark("stop", "pksclientcache. Append: %s" % str(append)) return ret def get_name_using_gpg(keyfile, tmpdir): """Use GPG to extract name and KeyID""" if verbose: v = "" else: v = "2>/dev/null" benchmark("start", "gpg: get name") cmd = "%s --with-colons " \ "--list-keys " \ "--homedir %s " \ "--keyring %s " \ "--no-default-keyring " \ "--no-auto-check-trustdb " \ "--no-expensive-trust-checks %s" \ % (gnupg, tmpdir, keyfile, v) reply = os.popen(cmd) name=keyid=None for x in reply.xreadlines(): y = splitcolons(x) if y[0] == "pub": name = y[9].strip() # Make sure we always use GPGs idea of keyid. keyid = "0x" + y[4] if y[1] != "": benchmark("stop", "gpg: get name") return None, None break benchmark("stop", "gpg: get name") if not isvalidkeyid(keyid): print >>sys.stderr, "Something seems to be wrong in the output when running:" print >>sys.stderr, cmd os._exit(7) # No cleanup will make debugging easier. return keyid, name def get_signatures_using_gpg_real(keyid, keyfile, tmpdir, verify): if verbose: v = "" else: v = "2>/dev/null" if verify: action = "--check-sigs" else: action = "--list-sigs" benchmark("start", "gpg: get sigs. Verify: %s" % str(verify)) reply = os.popen("%s --with-colons " "%s " "--homedir %s " "--keyring %s " "--no-default-keyring " "--no-auto-check-trustdb " "--no-expensive-trust-checks " "--fixed-list-mode %s %s" % (gnupg, action, tmpdir, keyfile, keyid, v)) # OK, parse the GPG output. uidsigs = {} # Number of times this signature appears (decreased if rev) uidsiglvl = {} # Maximum cert check level for this signature sigs = {} # The real signatures we return # I can't find this explicitly in the GnuPG documentation, but # reading between the lines, it is quite clear that the first # user ID is the primary one. primary_uid = 2 for x in reply.readlines() + ["sub"]: y = splitcolons(x) if y[0] == "uid" or y[0] == "sub": # Start/end of user ID if keyid in uidsigs and uidsigs[keyid]>0: # If self-signed del uidsigs[keyid] # Remove self-signature for z in uidsigs: if uidsigs[z] > 0: # For non-revoked sigs if z not in sigs or sigs[z] < uidsiglvl[z]: sigs[z] = uidsiglvl[z] # Update sigs with new or higher levels uidsigs, uidsiglvl = {}, {} if primary_uid: primary_uid -= 1 elif y[0] == "sig": # Signature if verify and y[1] != "!": # Invalid signature #if y[1] != "?": # print "0x" + keyid + ": " + x[:-1] continue # The gpg --with-colons --fixed-list-mode format # doesn't contain information about whether a # signature has expired, but the expire time is there # so we can check it ourselves. if y[6] and int(y[6]) < time.time(): continue key = "0x" + y[4] lvl = int(y[10][1], 16) if lvl > 3: # Ignore direct-key signatures. continue if primary_uid: lvl += 4 # Add 4 if primary UID is signed. if key not in uidsigs: uidsigs[key] = 1 else: uidsigs[key] += 1 if key not in uidsiglvl or uidsiglvl[key] < lvl: uidsiglvl[key] = lvl # Update uidsiglvl with new or higher levels elif y[0] == "rev": # Revoked signature key = "0x" + y[4] if key not in uidsigs: uidsigs[key] = -1 else: uidsigs[key] -= 1 if y[0] == "sub": # Stop processing when we get to subkeys. break benchmark("stop", "gpg: get sigs. Verify: %s" % str(verify)) return sigs def get_signatures_using_gpg(keyid, keyfile, tmpdir, verify): """A caching wrapper around the real get_signatures_using_gpg().""" # There are three cases where a signature can become invalid: # 1. The signed key becomes invalid (revoked, expired) # 2. The signature itself becomes invalid (revoked, expired) # 3. The signing key becomes invalid (revoked, expired) # This means that we never have to validate the signatures # using both keys _at the same time_ to detect that a # signature has become invalid. If we always run every key on # it's own through gnupg, we detect cases 1 and 2 when running # the signed key and 3 when running the signing key. We need # only re-verify signatures using gpg when the signed key has # changed. If signed key is unchanged, we can use the last # signature list as candidates and count on invalid keys being # removed later. # Similarily, there are three cases where a signature becomes valid: # 1. The signed key becomes valid (e.g. added self-signature) # 2. The signature gets added # 3. The signing key becomes valid (new self-signature or just uploaded) # 1 and 2 both modifies the signed key. Unfortunately, 3 does # not. It should be possible to verify only those signatures # where the signing key has changed, but that is complicated. # Instead we verify all signatures on a key iff the signed key # or any of the signing keys are modified. Some signatures # will be re-verified without being strictly needed, but # changes are probably rare enough to not impact speed # noticably anyway. # Conclusion: If not verifying signatures, don't use cache. If # verifying signatures, calculate SHA1-sum of the whole # keyring. If unchanged, use cached list of signatures. # This approach should produce identical results as using no # cache if this is true: # If (gpg --check-sigs have previously verified a signature) and # (no key, just the time, has changed since verification) then: # ( # if (gpg --list-keys sees nothing wrong with the signed key) and # (gpg --list-sigs sees nothing wrong with the signed key # or the signature) and # (gpg --list-keys sees nothing wrong with the signing key) # then and only then: # (gpg --check-sigs will verify the signature) # ) # If this should be false, I'd like to know about it. if not verify: return get_signatures_using_gpg_real(keyid,keyfile,tmpdir,verify) if sigcache or newsigcache is not None: benchmark("start", "gpgcache: Calculating SHA1-sum for keyring") import sha ringhash = sha.sha(open(keyfile).read()).digest() benchmark("stop", "gpgcache: Calculating SHA1-sum for keyring") else: ringhash = None if ringhash in sigcache: benchmark("tick", "gpgcache: Using cached signatures") sigs = sigcache[ringhash] else: benchmark("start", "gpgcache: Using GnuPG to validate signatures") sigs = get_signatures_using_gpg_real(keyid,keyfile,tmpdir,verify) benchmark("stop", "gpgcache: Using GnuPG to validate signatures") if newsigcache is not None: newsigcache[ringhash] = sigs benchmark("tick", "gpgcache: Caching signatures") return sigs # OK, no more defining functions. if not call_pksclient(pksdb, keyid, "%s/key" % tmpdir, 0): benchmark("stop", "getsinglekeyfrompks()") return None newkeyid, name = get_name_using_gpg("%s/key" % tmpdir, tmpdir) if not name: benchmark("tick", "getsinglekeyfrompks: Ignoring invalid or revoked key.") benchmark("stop", "getsinglekeyfrompks()") return None sigs=get_signatures_using_gpg(newkeyid, "%s/key"%tmpdir, tmpdir, 0) if verify: benchmark("start", "verify signatures") # A keyring is created by concatenating the binary keys. for sig in sigs: call_pksclient(pksdb, sig, "%s/key" % tmpdir, 1) sigs = get_signatures_using_gpg(newkeyid, "%s/key"%tmpdir, tmpdir, 1) benchmark("stop", "verify signatures") benchmark("stop", "getsinglekeyfrompks()") # XXX Return None if unsigned? (See xxx later) return newkeyid, name, sigs # The same as the function above, but use a dictionary instead of # external pksclient, and don't return names. def getkeyfromdict(dict, key, tmpdir=None, verbose=0): """Get key signatures from a dictionary""" if dict.has_key(key): return key, None, dict[key] else: return None def getwot(startkeys, getkeyfunc, getkeyarg, verbose, slow=0, printevery=100): """Get the whole strong set. Can take some time to complete""" # GPG needs a home directory. We want it to be empty, so nothing is # interfering. # Python 2.2 don't have mkdtemp, so we are not race free there. if 'mkdtemp' in dir(tempfile): tmpdir = tempfile.mkdtemp(".tmp", "pks2wot-") # Race free else: tmpdir = tempfile.mktemp("-pks2wot.tmp") # Not race free. os.mkdir(tmpdir) try: # {key: [keys,signed,by,key], ...} sigdict = {} # {key: "Owner of this key", ...} namedict = {} # List of all keys we know we have to check. keystocheck = [] for key in startkeys: keystocheck.append(key) # List of seen keys. seenkeys = {} nexttoprint = totalcount = lastok = 0 while keystocheck: # XXX This is depth first. This is probably fastest, but # I'm not completely sure. Just s/pop()/pop(0)/ to turn # this into width first. key = keystocheck.pop() if slow: if (not totalcount % 8) and totalcount: sys.stdout.write(str(len(namedict) - lastok)) sys.stdout.flush() lastok = len(namedict) if len(namedict) >= nexttoprint: print "" debug("Keys fetched/OK/seen/in queue: %5d/%5d/%5d/%5d. Now fetching: %s"\ % (totalcount, len(namedict), len(seenkeys), len(keystocheck), key)) nexttoprint += printevery totalcount += 1 ret = getkeyfunc(getkeyarg, key, tmpdir, verbose) # Don't bother with broken and unsigned keys if not ret: continue # The reason for this is twofold: # 1. We use 16 character KeyIDs to minimize collisions. # However, in the pks source, file kd_search.c, function # parse_keyidstr(), we find this: # /* If it is too big, shrink to the short 8-digit keyid. This will # give a graceful transition to a future version that can handle # 16-digit or full fingerprint keyids. -ds */ # So, sometimes we ask for key 0x0123456789abcdef but instead # get 0x938273419abcdef. There isn't anything to do to get the # real key we want except fixing pks, but at least we know we # got the wrong key, so we simply exclude it. # Examples of this are 0xC73D3F078236BBFA -> # 0x63F339EA8236BBFA, 0x8AB603A4C30BC6E5 -> 0xD2E1C877C30BC6E5 # and 0x84E4870D6FB0F259 -> 0x84E4870A6FB0F259. # 2. There are keys that that pks thinks has one KeyID, # but GPG thinks has another. If the pks KeyID can be # chosen, without this check you could manipulate the .wot # file pretty much as you like. if key != ret[0]: print debug ("We asked pksclient for %s, but gpg thinks we got %s." % (key, ret[0])) continue # Use keyID from gpg. key = ret[0] namedict[key], sigdict[key] = ret[1:] for x in sigdict[key]: if x not in seenkeys: keystocheck.append(x) seenkeys.update(sigdict[key]) finally: # Remove the temporary GPG home directory. for f in tmpdir+"/trustdb.gpg", tmpdir+"/key": try: os.remove(f) except OSError: pass try: os.rmdir(tmpdir) except OSError, (errno, strerror): print >>sys.stderr, \ 'Unable to remove temporary GPG directory "%s": %s' % (tmpdir, strerror) return (sigdict, namedict) def reversesigs(sigdict): """Reverse signatures. (s/signs/signed by/)""" revsigdict = {} for key in sigdict: for sig in sigdict[key]: if sig not in revsigdict: revsigdict[sig] = {} revsigdict[sig][key] = sigdict[key][sig] return revsigdict def writetofile(savefilename, sigs, names, keys, begintime, endtime, servername, debugstr, verify): """Save .wot file. File format is documented at: http://www.lysator.liu.se/~jc/wotsap/wotfileformat.txt""" file = os.popen('bzip2 >"%s"' % savefilename, 'w') # Write .ar header file.write("!\n") def writearfileheader(filename, size): file.write("%-16s%-12s%-6s%-6s%-8s%-10s`\n" % \ (filename + '/', int(time.time()), 0, 0, 100644, size)) def writearfile(filename, string): writearfileheader(filename, len(string)) file.write(string) if len(string) & 1: file.write('\n') # Write file "README" tmp = \ " README\n"\ "This is a Web of Trust archive." "\n"\ "The file format is documented at:" "\n"\ " http://www.lysator.liu.se/~jc/wotsap/wotfileformat.txt" "\n"\ "" "\n"\ "It was extracted at the public key server %s." "\n"\ "Extraction started %s and ended %s." "\n"\ "" "\n"\ "The signatures have %sbeen verified using GnuPG." "\n"\ "" "\n"\ "This file was generated with %s." "\n"\ % (servername, time.asctime(time.gmtime(begintime)) + " UTC", time.asctime(time.gmtime( endtime)) + " UTC", not verify and "NOT " or "", version) writearfile("README", tmp) # Write file "WOTVERSION" writearfile("WOTVERSION", "0.2\n") # Write file "names" # Replace unprintable characters with "?" tmp = [] for x in names: tmp.append( x.translate(string.maketrans( "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", "????????????????????????????????"))) writearfile("names", string.join(tmp, '\n') + '\n') # Write file "keys" writearfileheader("keys", len(keys) * 4) for key in keys: # This should work fine with both 2.2 and 2.3 int() behaviour. file.write(struct.pack('!I', long(key[-8:], 16))) # No padding needed. # Write file "signatures" size=0 for siglist in sigs: size += (len(siglist)+1) * 4 writearfileheader("signatures", size) for siglist in sigs: file.write(struct.pack('!i', len(siglist))) for sig in siglist: file.write(struct.pack('!i', (sig[1]<<28) + sig[0])) # No padding needed. # Write file "debug" writearfile("debug", debugstr) file.close() def toordered(sigdict, namedict): """Transform web to list format. To avoid people figuring out what key to falsely sign just to get on top of the list, we always randomize the order. For some strange reason, this also seems to give better compression.""" keylist = sigdict.keys() random.shuffle(keylist) keydict = {} for x in xrange(len(keylist)): keydict[keylist[x]] = x siglist = range(len(sigdict)) for x in keylist: siglist[keydict[x]] = [(keydict[y], sigdict[x][y]) for y in sigdict[x]] # Sort each signature list, for best compression siglist[keydict[x]].sort() namelist = [ namedict[key] for key in keylist ] return keylist, namelist, siglist def clearlockfiles(pksdb): cmd = "rm -f %s/{__*,log*}" % pksdb print "Clearing stale db lock files: %s" % cmd os.system(cmd) def main(argv): import getopt benchmark("start", "TOTAL") def usage(): print version print "Usage: %s [OPTIONS] local_server_name pks_database_dir output.wot" \ % sys.argv[0] print print " local_server_name should be something to identify this server," print " e.g. \"Foo server (http://keyserver.foo.org:11371/)\"" print print " -n disable signature verification. Faster, but less safe." print " -c clear stale lock files in db. Use with caution!" print " -v turn on (very) verbose output." print " -w FILE dump intermediate state to file." print " -r FILE read intermediate state from file, skipping the" print " slow pksclient/gpg parts. Useful for debugging." print print " --pksclient=/path/to/pksclient" print " --gnupg=/path/to/gpg" print " Explicitly set path to pksclient or gpg. Default is to" print " let a shell find them in $PATH." print " --nopkslogging" print " Pass the 't' flag to pksclient, making it run without" print " logging and transactions. This is faster, but less safe." print " Use only if noone else is accessing the database." print " --eightdigits" print " Feed pksclient with eight instead of sixteen digit KeyIDs." print " Needed for older versions of pks. Newer versions ignore" print " higher bits anyway." print " --startkeys=0x0123456789ABCDEF,0XFEDCBA9876543210" print " A comma-separated list of 16-digit KeyIDs in the largest" print " strongly connected set to start from. Defaults to four" print " keys known to be in the set." print " --feedback=nnn" print " Delay between feedbacks when fetching signatures." print " Defaults to 100." print " --cachesize=N" print " Cache N MiB of keys in memory during extraction. Most of" print " the time goes to key extraction from pks, and each key is" print " fetched several times, so a large cache will improve spead" print " greatly." print " Defaults to 50." print " --sigcache=FILE" print " Use FILE as a signature and name cache file. Unchanged" print " (SHA1-sum of) keys are not processed by gnupg again." print " FILE does not need to exist. It is overwritten or created" print " with new data on exit." sys.exit(1) try: opts, args = getopt.gnu_getopt(argv[1:], "ncvw:r:", ['pksclient=', 'gnupg=', 'nopkslogging', 'eightdigits', 'startkeys=', 'feedback=', 'cachesize=', 'sigcache=']) except getopt.GetoptError: usage() if len(args) != 3: usage() # servername: The name of the keyserver. Will appear in # the .wot's README file. # pksdb: The database directory # savefile: Where to save the .wot file. (servername, pksdb, savefile) = args # Some keys in the strong set to start from. startkeys = ["0x013C5083E8C80C34", "0xA8D947F9ED9547ED", "0xA8919F49D23450F9"] verify = 1 verbose = None debugdump = None pksclient = 'pksclient' gnupg = 'gpg' nopkslogging = None eightdigits = 0 printevery = 100 cachesize = 50*2**20 sigcachefile = None for o, a in opts: if o == '-n': verify = 0 elif o == '-v': verbose = 1 elif o == '-c': clearlockfiles(pksdb) elif o in ('-w', '-r'): if debugdump: usage() debugdump = o[1] debugfile = a elif o == '--pksclient': pksclient = a elif o == '--gnupg': gnupg = a elif o == '--nopkslogging': nopkslogging = 1 elif o == '--eightdigits': eightdigits = 1 elif o == '--startkeys': startkeys = a.split(',') for key in startkeys: if not isvalidkeyid(key): print "Invalid KeyID:", key print usage() elif o == '--feedback': printevery = int(a) elif o == '--cachesize': cachesize = int(float(a)*2**20) elif o == '--sigcache': sigcachefile = a begintime = time.time() debug("We are running on %s." % sys.platform) debug("My version: %s." % version) debug("Python version:") for line in sys.version.splitlines(): debug(" " + line) debug("GnuPG version:") for line in os.popen("%s --version"%gnupg).xreadlines(): debug(" " + line[:-1]) debug("pksclient version:") for line in os.popen("%s --version 2>&1" % pksclient).xreadlines(): if line.startswith("%s /db/path " % pksclient): debug(" [[Unknown version. Please implement pksclient --version.]]") break debug(" " + line[:-1]) debug("") debug("Starting to fetch signatures from the key server.") debug("Now is %s local time." % time.ctime(begintime)) if sigcachefile and debugdump != 'r': sigcacheversion = 1 import cPickle try: sigcache = cPickle.load(open(sigcachefile)) debug("Using sigcache file created %s with %sverified keys." %\ (time.ctime(sigcache['createdtime']), not sigcache['verified'] and "un" or "")) if sigcache['verified'] != verify: debug("Cached file and new run differ on verifying " +\ "signatures. This would produce too strange " +\ "results. Bailing out.") sys.exit(2) if 'version' not in sigcache or sigcache['version'] != sigcacheversion: # Should we just erase the old file instead? debug("Cached file has unknown version. Bailing out.") sys.exit(2) except IOError: debug("Could not open sigcache file. Will create new later.") sigcache = {'keys': {}} newsigcache = { 'WHATISTHIS': 'This is a cache file for OpenPGP key ' +\ 'signatures and names used by pks2wot, ' +\ 'part of Wotsap, ' +\ 'http://www.lysator.liu.se/~jc/wotsap/', 'version': sigcacheversion, 'createdtime': time.time(), 'verified': verify, 'keys': {} } else: sigcache = {'keys': {}} newsigcache = {'keys': None} # This is the slow operation. getkeyarg = { 'verify' : verify, 'pksdb' : pksdb, 'pksclient' : pksclient, 'gnupg' : gnupg, 'nopkslogging' : nopkslogging, 'eightdigits' : eightdigits, 'cachesize' : cachesize, 'sigcache' : sigcache['keys'], 'newsigcache' : newsigcache['keys']} if debugdump != "r": (sigdict,namedict) = getwot(startkeys, getsinglekeyfrompks, getkeyarg, verbose, slow=1, printevery=printevery) else: import cPickle debug("Reading pickled state from %s." % debugfile) (sigdict,namedict) = cPickle.load(open(debugfile)) if debugdump == "w": import cPickle debug("Writing pickled state to %s." % debugfile) cPickle.dump((sigdict,namedict), open(debugfile, 'w'), cPickle.HIGHEST_PROTOCOL) if sigcachefile and debugdump != 'r': debug("Writing sigcache.") cPickle.dump(newsigcache, open(sigcachefile, 'w'), cPickle.HIGHEST_PROTOCOL) endtime = time.time() print "" debug("Done fetching signatures.") debug("Now is %s local time." % time.ctime(endtime)) debug("Number of keys in backwards reachable set: %d" % len(sigdict)) sys.stdout.flush() # Don't start with invalid keys in the other direction # XXX: Shouldn't be necessary? newstartkeys = [] for x in startkeys: if x in sigdict: newstartkeys.append(x) # Process all keys again, in the other direction, to filter out # the strong set. sigdict = reversesigs(sigdict) sigdict = getwot(newstartkeys, getkeyfromdict, sigdict, verbose, slow=0, printevery=printevery)[0] debug("Number of keys in strong set: (backwards) %d" % len(sigdict)) sigdict = reversesigs(sigdict) debug("And forward. Should be the same: %d" % len(sigdict)) # Now, we want everything numbered. This means we have to use # lists instead of dictionaries. keylist, namelist, siglist = toordered(sigdict, namedict) benchmark("stop", "TOTAL") debug(benchmark("report")) print "Writing to file %s:" % savefile, sys.stdout.flush() writetofile(savefile, siglist, namelist, keylist, begintime, endtime, servername, __debugstr, verify) print "Done." if __name__ == "__main__": main(sys.argv) wotsap-0.7/wotfileformat.txt0000644000175000017500000001062210656267766014504 0ustar jczjcz The Web of Trust .wot file format, version 0.2 1. Overview To stimulate statistics and analyzes of the OpenPGP Web of Trust, a file format is proposed to carry information about key IDs, names and signatures, in files small enough to be easily downloaded by anyone. Because the OpenPGP Web of Trust is supposed to grow a lot, the file format is designed with compressed size as first priority. Key IDs and names are specified only once. The signatures are specified using sorted lists of indices into the key list. Similar data is close together. The result is a format which can store the current (November 2004) strongly connected set in less than 0.9 MiB. The first system to use this file format is Wotsap, http://www.lysator.liu.se/~jc/wotsap/. The latest version of this file is available at http://www.lysator.liu.se/~jc/wotsap/wotfileformat.txt with detached signature at http://www.lysator.liu.se/~jc/wotsap/wotfileformat.txt.sig 2. File format A wot file consists of the data chunks with names and content listed below. The chunks are stored, in the same order as below, in a bzip2-compressed ar-archive. All texts are coded using UTF-8, and all integers are in network byte order. "README": A short text describing what kind of file this is, and where and when it was generated. "WOTVERSION": The version of this specification, followed by a newline. Currently "0.2\n". "names" One string specifying the name of the primary user ID of each key. Each name is followed by a newline. The inclusion of a key here implies that it is a valid key which is part of the web of trust. The order of the keys is random and has no meaning, except that the same order is used in all lists in a specific wot file. The orders in two different wot files are generally not the same. "keys" Four bytes specifying the key ID of each key. The keys are in the same order as in the "names" chunk. "signatures" For each key, in the same order as the above lists, a 4-byte integer specifying the number of signatures on that key, followed by a list of that number of 4-byte signature descriptions. The 4 most significant bits indicates the signature type and the 28 least significant bits indicate the signing key as an index, starting from zero, for the above lists. The 4-bit signature type is interpreted as: If the signing key has signed the primary user ID and one or more other IDs, all signatures except the one on the primary user ID are ignored. If and only if the signing key has signed the primary user ID, bit 2 is set and the two least significant bits are set to the cert check level (0-3) of that signature. If the primary user ID is not signed, one or more other user IDs are. Bit 2 is not set, but the two least significant bits are set to the highest cert check level of those signatures. Bit 3 is reserved and might be given a meaning in future 0.2.x versions. Set to zero when writing and ignore when reading. "debug" (optional) Optional debug text, which should be of no interest to anyone not developing/debugging/tuning the .wot generation program. 4. Future extensions Future versions will, if possible, only add new chunks to the archive. Such versions will have version numbers 0.2.x. Current implementations must be able to read these files, by ignoring the extra chunks. If incompatible changes are introduced, the version number will change to 0.3. 5. Version history Version 0.2 - 2004-11-07 * Some clarifications * Easier parsing of signatures chunk signatures chunk in 0.1 contained: "For each key, a list specifying the keys that has signed this key. The list elements are 4-byte indices into the above lists. The lists are in the same order as in the above chunks. The lists are separated by 0xFFFFFFFF." * Specify if primary UID is signed * Specify cert check level * New (optional) debug file * Renamed internal files as "chunks" to avoid confusion Available at http://www.lysator.liu.se/~jc/wotsap/wotfileformat-0.2.txt with detached signature at http://www.lysator.liu.se/~jc/wotsap/wotfileformat-0.2.txt.sig Version 0.1 - 2003-03-26 Initial version, available at http://www.lysator.liu.se/~jc/wotsap/wotfileformat-0.1.txt with detached signature at http://www.lysator.liu.se/~jc/wotsap/wotfileformat-0.1.txt.sig 6. Author and date Jörgen Cederlöf , 2004-11-07 wotsap-0.7/wotsap0000755000175000017500000022502510656267766012331 0ustar jczjcz#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # Web of trust statistics and pathfinder, Wotsap # http://www.lysator.liu.se/~jc/wotsap/ # Copyright (C) 2003,2004 Jörgen Cederlöf # modified 2004 by Marco Bodrato ("wanted sigs") # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # This file also contains a font extracted from the Debian package # xfonts-75dpi: # Copyright 1984-1989, 1994 Adobe Systems Incorporated. # Copyright 1988, 1994 Digital Equipment Corporation. # # Adobe is a trademark of Adobe Systems Incorporated which may be # registered in certain jurisdictions. # Permission to use these trademarks is hereby granted only in # association with the images described in this file. # # Permission to use, copy, modify, distribute and sell this software # and its documentation for any purpose and without fee is hereby # granted, provided that the above copyright notices appear in all # copies and that both those copyright notices and this permission # notice appear in supporting documentation, and that the names of # Adobe Systems and Digital Equipment Corporation not be used in # advertising or publicity pertaining to distribution of the software # without specific, written prior permission. Adobe Systems and # Digital Equipment Corporation make no representations about the # suitability of this software for any purpose. It is provided "as # is" without express or implied warranty. # We need the yield keyword in 2.2. from __future__ import generators # New (3.0) division syntax is more intiutive. from __future__ import division # Try to make error messages from old Python versions show something # meaningful. def __tmp(): yield \ '*********** Too old Python. You need at least version 2.2.0. ***********' del __tmp VERSION="0.7" defaultconfig = { "size": (980, 700), "arrowlen": 18, "arrowang": 23, "colors": [0x9d, 0xaf, 0x75, # Background (col_bg) 0xc0, 0xc0, 0xff, # Box interiors (col_boxi) 0x00, 0x00, 0x00, # Font (col_font) 0x00, 0x00, 0x00, # Lines (col_line) 0x00, 0x00, 0x00, # Arrows cert level 0 (col_arrow_cert0) 0xd0, 0x40, 0x20, # Arrows cert level 1 (col_arrow_cert1) 0x00, 0x90, 0x90, # Arrows cert level 2 (col_arrow_cert2) 0x10, 0xe0, 0x20, # Arrows cert level 3 (col_arrow_cert3) 0xf0, 0xff, 0xf0, # Added Arrows (col_arrow_added) 0x00, 0x00, 0x00, # Arrow borders (col_arrowb) #0x00, 0x10, 0x70, # Arrow borders (col_arrowb) 0xff, 0x00, 0x00, # Box borders (col_boxb) 0x00, 0x00, 0x00, # Logo background (~col_logbs) 0xff, 0xff, 0xff] # Logo foreground (~col_logfs) } (col_bg, col_boxi, col_font, col_line, col_arrow_cert0, col_arrow_cert1, col_arrow_cert2, col_arrow_cert3, col_arrow_added, col_arrowb, col_boxb) = range(11) col_logbs, col_logfs = "\x0b", "\x0c" import sys import os import string import struct import math import re import random # How cert levels are presented clascii = "abcd0123?!U" # We need symbolic names for some of them. CL_unknown = clascii.index("?") CL_added = clascii.index("!") CL_unilink = clascii.index("U") class wotError(Exception): """Base class for exceptions in this module.""" def __str__(self): return self.__unicode__().encode("UTF-8", "replace") def __unicode__(self): return u"wotError: unknown error" class wotKeyNotFoundError(wotError): """Exception raised when a key is not found. Attributes: key -- The key that could not be found """ def __init__(self, key): self.key = key def __unicode__(self): return u'Key not found: "%s"' % self.key class wotPathNotFoundError(wotError): """Exception raised when no path is found. Attributes: bottom -- The bottom key top -- The top key mod -- Modstring """ def __init__(self, bottom, top, mod=None): self.bottom = bottom self.top = top self.mod = mod def __unicode__(self): r = u'Path not found: "%s" to "%s" ' % (self.bottom, self.top) if self.mod is not None: r += u"Using modstring:\n%s" % unicode(self.mod) else: r += u"without modstring." return r class wotLoadFileError(wotError): """Exception raised when loading a .wot file failed. Attributes: msg -- Message describing the error """ def __init__(self, msg): self.msg = msg def __unicode__(self): return self.msg class wotModstringError(wotError): """Exception raised when a modstring contains errors Attributes: msg -- Message describing the error """ def __init__(self, msg): self.msg = msg def __unicode__(self): return self.msg class wotTimeoutError(wotError): """Exception raised when something takes too long time Class attributes: errorstring -- Default error string Object attributes: times = { 'percentdone' -- How many percent were completed?, 'timeelapsed' -- How many seconds did it take?, 'calculatedtime' -- Calculated total time, 'calculatedtimeleft' -- Calculated time to complete, 'appendstring' -- String to append to message } """ errorstring = u"Just %(percentdone)d%% processed in %(timeelapsed).2f " \ "seconds. With this speed, completing would \n" \ "take %(calculatedtimeleft).1f seconds more, " \ "totally %(calculatedtime).1f seconds.\n" \ "%(appendstring)s" def __init__(self, times=None): self.times = times def __unicode__(self): return self.errorstring % self.times class wotTooManyError(wotError): """Exception raised when there are too many matches. Attributes: nmatches = number of matches """ def __init__(self, nmatches=None): self.nmatches = nmatches def __unicode__(self): return 'Sorry, too many matches: %d. Try a more restricted search.' % \ self.nmatches init_pil_get_logo_logo = None def init_pil_get_logo(): global init_pil_get_logo_logo if init_pil_get_logo_logo is not None: return init_pil_get_logo_logo try: global Image, ImageDraw, ImageFont import Image, ImageDraw, ImageFont except ImportError: print >>sys.stderr, \ "wotsap: Unable to import Python Imaging Library modules\n" \ " (Image, ImageDraw, ImageFont)\n" \ "They can be downloaded from:\n" \ " http://www.pythonware.com/products/pil/" raise packedlogo=[\ 528,1,6,1,3,1,13,1,2,1,27,1,17,1,15,1,1,1,21,1,7,1,8,1,15,1,20,1,6,1,6,1, 3,1,13,1,2,1,27,1,17,1,15,1,23,1,16,1,15,1,20,1,6,1,1,2,2,3,1,3,1,1,1,2,3, 1,2,1,2,1,1,1,2,1,2,1,1,1,2,1,2,1,1,1,2,1,2,1,4,1,1,1,2,1,2,2,2,3,2,3,2,3, 2,1,1,1,4,1,1,1,1,1,2,1,5,2,3,2,3,1,8,1,2,2,3,1,1,1,2,1,2,1,2,3,2,3,2,2,2, 3,2,1,1,2,3,1,7,2,2,1,2,1,3,1,2,2,2,1,5,1,2,1,1,1,2,1,2,1,1,1,2,1,2,1,1,1, 2,1,2,1,4,1,1,1,2,1,1,1,2,1,4,1,2,1,2,1,3,1,1,2,5,1,1,1,1,1,2,1,4,1,2,1,1, 1,2,1,2,1,2,2,2,1,1,1,1,1,2,1,2,1,1,1,2,1,2,1,1,1,3,1,2,1,2,1,2,1,4,1,1,2, 2,1,2,1,7,1,3,1,2,1,3,1,2,1,3,1,5,1,2,1,2,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1, 1,5,1,1,1,1,1,3,2,3,3,2,1,2,1,3,1,1,1,6,1,1,1,1,1,2,1,5,2,2,4,2,1,1,1,2,2, 2,1,1,1,5,1,2,1,1,1,1,1,2,1,3,1,2,1,3,2,3,3,1,1,3,1,2,1,7,1,3,1,2,1,3,1,2, 1,3,1,5,1,2,1,2,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,5,1,1,1,1,1,5,1,1,1,2,1, 2,1,2,1,3,1,1,1,6,1,1,1,1,1,2,1,7,1,1,1,5,1,8,1,1,1,5,1,2,1,1,1,1,1,2,1,3, 1,2,1,5,1,1,1,2,1,1,1,3,1,2,1,7,1,3,1,2,1,3,1,2,2,2,1,4,1,2,1,4,1,1,1,5,1, 1,1,5,1,1,1,6,1,2,2,2,1,2,1,1,1,2,1,2,1,2,1,3,1,1,1,6,1,1,1,1,1,2,1,4,1,2, 1,1,1,2,1,1,1,9,1,1,1,2,1,1,1,4,1,1,1,3,1,3,1,2,1,2,1,2,1,1,1,2,1,1,2,2,1, 1,1,8,1,3,1,2,2,2,2,1,1,1,2,3,1,1,1,2,1,4,1,1,1,5,1,1,1,5,1,1,1,4,1,1,1,2, 1,4,2,3,2,1,1,1,2,2,3,2,1,4,1,1,1,1,1,2,3,2,1,2,2,3,2,2,1,9,1,2,2,2,1,4,1, 1,1,4,3,3,2,2,2,3,2,1,2,1,2,2,1,22,1,44,1,60,1,1,1,37,1,28,1,43,1,62,1,38, 1,361] logostr = [] curcol, othercol = col_logbs, col_logfs for x in packedlogo: logostr.append(curcol*x) curcol, othercol = othercol, curcol init_pil_get_logo_logo = \ Image.fromstring('P', (175,15), string.join(logostr, "")) return init_pil_get_logo_logo # Created using gunzip, pilfont.py, pngcrush and base64-encode on # /usr/X11R6/lib/X11/fonts/75dpi/courR14-ISO8859-1.pcf.gz from Debian # package xfonts-75dpi. _fontstr = (''' UElMZm9udAo7Ozs7OzsxMzsKREFUQQoACQAAAAD/9wAHAAAAAAAAAAcACQAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAkAAAAA//8AAQAAAAcAAAAIAAEACQAAAAP/9gAEAAAACAAAAAkA CgAJAAAAAv/3AAb/+wAJAAAADQAEAAkAAAAC//cABwAAAA0AAAASAAkACQAAAAL/9QAHAAIA EgAAABcADQAJAAAAAf/2AAgAAAAXAAAAHgAKAAkAAAAB//gABwAAAB4AAAAkAAgACQAAAAP/ 9wAE//sAJAAAACUABAAJAAAAA//2AAYAAgAlAAAAKAAMAAkAAAAC//YABQACACgAAAArAAwA CQAAAAH/9wAG//0AKwAAADAABgAJAAAAAf/4AAj//wAwAAAANwAHAAkAAAAC//4ABQACADcA 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 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAP//AAEAAAJUAAAC VQABAAkAAAAE//kABQACAlUAAAJWAAkACQAAAAL/9wAHAAACVgAAAlsACQAJAAAAAf/3AAgA AAJbAAACYgAJAAkAAAAB//kAB///AmIAAAJoAAYACQAAAAH/9wAIAAACaAAAAm8ACQAJAAAA BP/3AAUAAgJvAAACcAALAAkAAAAB//cABwABAnAAAAJ2AAoACQAAAAL/9wAH//gCdgAAAnsA AQAJAAAAAP/3AAgAAAJ7AAACgwAJAAkAAAAC//cABv/9AoMAAAKHAAYACQAAAAD/+QAIAAAC hwAAAo8ABwAJAAAAAf/6AAj//QKPAAAClgADAAkAAAAB//sAB//8ApYAAAKcAAEACQAAAAD/ 9wAIAAACnAAAAqQACQAJAAAAAv/4AAb/+QKkAAACqAABAAkAAAAC//YABv/6AqgAAAKsAAQA CQAAAAH/+AAI//8CrAAAArMABwAJAAAAAv/2AAb//AKzAAACtwAGAAkAAAAC//YABv/8ArcA AAK7AAYACQAAAAL/9gAG//gCuwAAAr8AAgAJAAAAAP/5AAgAAwK/AAACxwAKAAkAAAAB//cA CAABAscAAALOAAoACQAAAAP/+wAF//0CzgAAAtAAAgAJAAAAAgAAAAUAAwLQAAAC0wADAAkA AAAD//YABv/8AtMAAALWAAYACQAAAAL/9wAG//0C1gAAAtoABgAJAAAAAP/5AAgAAALaAAAC 4gAHAAkAAP////YACQABAuIAAALsAAsACQAA////9gAJAAEC7AAAAvYACwAJAAD////2AAkA AQL2AAADAAALAAkAAAAC//kABwACAwAAAAMFAAkACQAAAAD/9AAJAAADBQAAAw4ADAAJAAAA AP/0AAkAAAMOAAADFwAMAAkAAAAA//QACQAAAxcAAAMgAAwACQAAAAD/9AAJAAAAAAANAAkA GQAJAAAAAP/1AAkAAAAJAA0AEgAYAAkAAAAA//MACQAAABIADQAbABoACQAA////9wAIAAAA GwANACQAFgAJAAAAAf/3AAgAAwAkAA0AKwAZAAkAAAAB//QACAAAACsADQAyABkACQAAAAH/ 9AAIAAAAMgANADkAGQAJAAAAAf/0AAgAAAA5AA0AQAAZAAkAAAAB//UACAAAAEAADQBHABgA CQAAAAL/9AAHAAAARwANAEwAGQAJAAAAAv/0AAcAAABMAA0AUQAZAAkAAAAC//QABwAAAFEA DQBWABkACQAAAAL/9QAHAAAAVgANAFsAGAAJAAAAAP/3AAgAAABbAA0AYwAWAAkAAAAA//QA CAAAAGMADQBrABkACQAAAAD/9AAIAAAAawANAHMAGQAJAAAAAP/0AAgAAABzAA0AewAZAAkA AAAA//QACAAAAHsADQCDABkACQAAAAD/9AAIAAAAgwANAIsAGQAJAAAAAP/1AAgAAACLAA0A kwAYAAkAAAAB//gACP//AJMADQCaABQACQAA////9wAIAAAAmgANAKMAFgAJAAAAAP/0AAgA AACjAA0AqwAZAAkAAAAA//QACAAAAKsADQCzABkACQAAAAD/9AAIAAAAswANALsAGQAJAAAA AP/1AAgAAAC7AA0AwwAYAAkAAAAB//QACAAAAMMADQDKABkACQAAAAH/9wAIAAAAygANANEA FgAJAAAAAf/2AAgAAADRAA0A2AAXAAkAAAAB//YACAAAANgADQDfABcACQAAAAH/9gAIAAAA 3wANAOYAFwAJAAAAAf/2AAgAAADmAA0A7QAXAAkAAAAB//YACAAAAO0ADQD0ABcACQAAAAH/ 9wAIAAAA9AANAPsAFgAJAAAAAf/1AAgAAAD7AA0BAgAYAAkAAAAA//kACAAAAQIADQEKABQA CQAAAAH/+QAIAAMBCgANAREAFwAJAAAAAf/2AAgAAAERAA0BGAAXAAkAAAAB//YACAAAARgA DQEfABcACQAAAAH/9gAIAAABHwANASYAFwAJAAAAAf/3AAgAAAEmAA0BLQAWAAkAAAAC//YA BwAAAS0ADQEyABcACQAAAAL/9gAHAAABMgANATcAFwAJAAAAAv/2AAcAAAE3AA0BPAAXAAkA AAAC//cABwAAATwADQFBABYACQAAAAD/9QAIAAABQQANAUkAGAAJAAAAAP/2AAgAAAFJAA0B UQAXAAkAAAAA//YACAAAAVEADQFZABcACQAAAAD/9gAIAAABWQANAWEAFwAJAAAAAP/2AAgA AAFhAA0BaQAXAAkAAAAA//YACAAAAWkADQFxABcACQAAAAD/9wAIAAABcQANAXkAFgAJAAAA AP/4AAj//wF5AA0BgQAUAAkAAAAA//kACAAAAYEADQGJABQACQAAAAD/9gAIAAABiQANAZEA FwAJAAAAAP/2AAgAAAGRAA0BmQAXAAkAAAAA//YACAAAAZkADQGhABcACQAAAAD/9wAIAAAB oQANAakAFgAJAAAAAP/2AAgAAwGpAA0BsQAaAAkAAAAA//cACAADAbEADQG5ABkACQAAAAD/ 9wAIAAMBuQANAcEAGQ== ''', ''' iVBORw0KGgoAAAANSUhEUgAAAyAAAAAaAQAAAABnAgWTAAAFxUlEQVRIx52VfUyV1x3HP88L 9z7My+VB5SVyhQc1HWm67a52G1YsD+oUmjWVZnFNmqUw1y1bagdxzVB5OYA2dLVgA8lY19aL S1ZnTErTxrDGygOlgxrXXmtf/9BepA1Yu3ovXOWC97lnf4iuNluyu+9/55zf9/f9vZzzOxw9 1elc3GfK4cbu8rVTLbN/8kqtK1CxMimTlw5NXTtZlpw61F2xUr4zNjk7J4ffkmN93squ7PWO NbBr5+Duwd/9Mf7dvyXc+VRDb8e3Llf8sm+1nH2nLmvsgKw+bOqaWE2BqW8/pdRlrmBYzVKe OFiyvuv+HGX06el4zxY29V7efXDODDdh1CzrmUkNRZSfP3vXsTJTaT2jLBFEXvizb4kvN8tK fvjhYNXSdcfXPLI9o/9cSQaBhpcb7rsj+yUrMDpyzgmNq+3kESlUHFvBwXB4szpZb3ookihR UHBTRsR0UoARQwfGMUS2KCfSIeqHW4YaA9cEqisupPpru0kuSWzGW+8Pd+IOzNWW5WzEDn2k tmRsOqa++aVQU1FsVCjW3/OAxONLgAoCQLYwAjOdUT/eDvF28WMEpoYEAEPvi3nhzu8jw+bT AWWvmZTOyKpaKN5cKYpHS5xIvdr+yslH+jw5gPKsMian7QilG73ZgmAtUQxcCSgtgG8BE1Da HG1iKZ517QAgg5DYhAGXYj/u41dRFDuUgXRkk4NKhbBDakvGtmDICzrKVHNdW/5Dgzwj/DFB OFRumlexwWtnTcKneOIkzkPTR/dHxWgEHWR7ZaeK4mpa87Imcgt7TvDCCSQyN2TY+W/99faf 3GvVBgvU9mvPl1mXAVRI6K74UrNTbQIWoqdBkpJiXsxawD9gDSBD0pYiDkm15954/GJd2LXJ LS/cJ0Nndybkz86Dkz1i/5qJmlNrn+oFUFvElaeCRDHQmQYuLbhyXjChIK+Xg4QVrTgI02Rj J2E+NC9ijEPKrb6zc7x7pXXn3bM7No4gR+3e01es8yyEYxa/h/PYET2zviygHsWS9QAYQ4EE FIESfeg3za+9/vDnf/jpN+eOvNq6XR55/dE4PHC8EUC3YqBCyReLTbEXo8m3S4W0QYYWNzLa 9NLpSH0ZiPIC1QZrrzyxfsyYFYCfW+G74UZVbSgUfqepcSphA0qk6PpJEUcAJXqTIx6FnLh5 SM3U2wED1QaL/4588VWRG7BvNToCKF8jWuXqTRFF8L9Ba3P+vfgaadt/pniHPrkMGPxf8KZh u6LQo80eUzp3tvW0dvyIN7Qt4a3HeMM3PBioDVb+5e8ruwZ8Jcd9972SVeAxstb6ivo8xve1 2sK0ArqDTKV3h2c5vyg1ftih6fuF0blJ04XHXFXgHHYyb6s5ENymDIR8eZ6C5Xn4VnsKluOz 0kw74FE7LUihTVsA3v1mCAKR4lWA9wm9VkAE/12Ll2izgB18e5dIQ0IvO51UTSnZLN0DgbEQ KK8pzyX54vGX3/1g98xnjYWivGzdD9Y9aT82AGit1YWTnNx1T/0naYgohqpfjZtVNS8uvN3V FQCM9/+ZZwFVgY/3f8P3nbMpddWWVnn3uaVRoNgO1whAtj2cTia+q/GMkDLy3hr4PGVEAK29 F4CrrXrS2/bB0YaR5pls1GixAE6fOQNALJ2OqHg1JQoEb24ZowBud2tjbHK4/krFyaY5A4oX m7AhbQlQ/X7w3eYrCAPFAEQd67qjzJwSu/uzx4fbtZtPaeK3hwGVnD3piOiBCx25279XPV0E +fYYANm2A25pzDBXrO03UxPDHTvzsxomJurABRUW9hxOpJWJhnfSX3FPMzDRKoCE9bEAGN9w 8eyL1f343IV43lx/3bvNrutegIU9e3Y0zuhp1atyb9imc1t/auOyiDFeVVW1VWBUVW1d/Fz9 N4ZV0VdJhpXWLNKPP2kHtfyXUFOuHmi1YV4gK5h/WgiAmRuz9sIttAdHR9Pr/YZI2sOxPD3z fwHwwTn7qlsWNAAAAABJRU5ErkJggg== ''') # Terminology: Given key X, up (UP) in the web is the keys that # are signed by X. Down (DN) is the keys that has signed X. To find # the keys you might trust, you search up from your key. To find # out who might trust you, you search down from your key. UP, DN = range(2) def loadfile(wotfilename): """Load file as specified by http://www.lysator.liu.se/~jc/wotsap/wotfileformat.txt""" def error(str): raise wotLoadFileError, str try: f = open(wotfilename, "rb") except IOError, (errno, strerror): error(u'Unable to open file "%s": %s' % (wotfilename, strerror)) header = "!\n" try: s = f.read(len(header)) except IOError, (errno, strerror): error(u'Unable to read from file "%s": %s' % (wotfilename, strerror)) if s != header: f = os.popen('bunzip2 < "%s"' % wotfilename, "rb") s = f.read(len(header)) if s != header: error(u"%s does not look like a .wot file." % wotfilename) sigs=names=keys=0 debug=readme=None while 1: filename = f.read(16) if len(filename) == 0: break mtime, uid, gid, mode, size, trailer = \ int(f.read(12)), int(f.read(6)), int(f.read(6)), \ int(f.read(8)), int(f.read(10)), f.read(2) if trailer != '`\n': error(u"Corrupt WOT file, trailer not found.") filename = filename.split('/')[0] if filename == "README": readme = unicode(f.read(size), 'utf-8', errors='replace') if size & 1: f.read(1) elif filename == "debug": debug = unicode(f.read(size), 'utf-8', errors='replace') if size & 1: f.read(1) elif filename == "names": names = unicode(f.read(size), 'utf-8', errors='replace') \ .split('\n') if size & 1: f.read(1) # If the last name has a newline (as it should have): if names[-1] == "": del names[-1] elif filename == "keys": keys = [] for n in xrange(size//4): keys.append(struct.unpack('!i', f.read(4))[0]) elif filename == "WOTVERSION": version = f.read(size) if size & 1: f.read(1) if version == "0.1\n" or version[:4] == "0.1.": version = "0.1" elif version == "0.2\n" or version[:4] == "0.2.": version = "0.2" else: error(u"Unknown WOT file version %s" % version) elif filename == "signatures": if version == "0.1": current = {} sigs = [current] for n in xrange(size//4): key = struct.unpack('!i', f.read(4))[0] if key != -1: current[key] = CL_unknown # Unknown cert check level else: current = {} sigs.append(current) else: sigs = [] for x in keys: current = {} num = struct.unpack('!i', f.read(4))[0] for y in struct.unpack('!'+'i'*num, f.read(4*num)): current[y & 0x0fffffff] = y>>28 sigs.append(current) else: print >>sys.stderr, "Strange. Found %s file." % filename f.seek(size + (size&1), 1) f.close() if not (len(names) == len(keys) == len(sigs)): error(u"Corrupt WOT file: Number of keys/names/sigs does not match: %d/%d/%d"\ % (len(keys), len(names), len(sigs))) return names, keys, sigs, readme, wotfilename, version, debug def reversesigs(sigs): """Reverse signatures. (s/signs/signed by/) Assumes the reversed set is of equal size <=> the set is a strongly connected set.""" revsigs = [ {} for x in sigs ] for n in xrange(len(sigs)): for key in sigs[n]: revsigs[key][n] = sigs[n][key] return revsigs # OK, this has some special cases, to make it faster. This is probably # the only function that needs to be really fast. # If mod is set, we also need dir == UP or DN. def findnext(keys, web, forbiddenkeys=None, mod=None, dir=None, getconns=1): """Return a dictionary containing keys and a set of keys link to.""" connections = {} modsigs = mod and (mod.exclsigs or mod.excllvls or mod.exclunlk) if mod and mod.exclkeys: modexcl = mod.exclkeys else: modexcl = [] if not forbiddenkeys: forbiddenkeys = [] # Why, you ask, why do we keep both forbiddenkeys and mod.excllvls # instead of joining them to one dictionary? Well, I made some # tests, and this method is ~ 40% faster. Copying a dictionary is # expensive. if getconns: if not modsigs: for x in keys: for y in web[x]: if y not in forbiddenkeys and y not in modexcl: if y not in connections: connections[y] = {} connections[y][x] = web[x][y] else: for x in keys: for y in web[x]: # There might be confusion about modstrings excluding # levels here. Are they up or down? As it turns out, # there is no need to worry. It is up to the caller to # provide a correct web, with correct signature # levels. if y not in forbiddenkeys and y not in modexcl and \ ((not mod.exclunlk) or x in mod.wot.sigs[dir][y]) and \ (x,y) not in mod.exclsigs and \ web[x][y] not in mod.excllvls: if y not in connections: connections[y] = {} connections[y][x] = web[x][y] else: if not modsigs: for x in keys: # Here we add even forbidden keys. connections.update(web[x]) else: for x in keys: for y in web[x]: if y not in forbiddenkeys and y not in mod.exclkeys and \ ((not mod.exclunlk) or x in mod.wot.sigs[dir][y]) and \ (x,y) not in mod.exclsigs and \ web[x][y] not in mod.excllvls: connections[y] = None # And here we remove them. if (not getconns) and (not modsigs): if len(connections) > len(forbiddenkeys) + len(modexcl): for x in forbiddenkeys: if x in connections: del connections[x] for x in modexcl: if x in connections: del connections[x] else: conn,connections = connections, {} for x in conn: if x not in forbiddenkeys and x not in modexcl: connections[x] = None # Included sigs have precedence over excluded sigs/keys. if mod and mod.add_sigs: for x,y in mod.add_sigs: if dir == DN: x,y = y,x if x in keys and y is not None and y not in forbiddenkeys: if getconns: if y not in connections: connections[y] = {} connections[y][x] = CL_added else: connections[y] = None return connections # To compare speed with findnext(). def __findnext_nomod(keys, web, forbiddenkeys=None): "Test function" connections = {} for x in keys: for y in web[x]: if (not forbiddenkeys) or y not in forbiddenkeys: if y not in connections: connections[y] = {} connections[y][x] = web[x][y] return connections # To compare speed with findnext(). def __findnext_nomod_nogetconns(keys, web, forbiddenkeys=None): "Test function" conn0,conn1 = {}, {} for x in keys: conn0.update(web[x]) for x in conn0: if x not in forbiddenkeys: conn1[x] = None return conn1 def findpaths(wot, bottom, top, mod): """Find all paths in wot from bottom to top""" if bottom == top: return [{bottom: None}] seen = {bottom: None} conn = [{bottom: None}] for lev in xrange(500): conn.append(findnext(conn[lev], wot.sigs[UP], seen, mod, UP)) if top in conn[-1]: # We got a match, but we also have lots of unneeded # connections to irrelevant keys. We must trim down the # web to just those paths leading to top. We do this by # reversing the web and going from top to bottom, using # this web. Since the modstring is already coded into the # web, we need not use it again. ret = [{top: None}] conn.reverse() conn.pop() for w in conn: ret.append( findnext(ret[-1], w)) return ret if len(conn[-1]) == 0: raise wotPathNotFoundError(bottom, top, mod) seen.update(conn[-1]) raise wotPathNotFoundError(bottom, top, mod) # This function is actually slightly slower than the real msd() now. def __msd_standalone(wot, key): """Test function.""" seen = {key: None} lastlevel = {key: None} total = 0 for lev in xrange(100): total += lev * len(lastlevel) thislevel = {} for x in lastlevel: thislevel.update(wot.sigs[DN][x]) lastlevel = {} for x in thislevel: if x not in seen: lastlevel[x] = None seen.update(lastlevel) if not lastlevel: break return total / len(seen) def msd(wot, key, mod=None, forbiddenkeys=None): """Quite fast msd checker, but not fast enough to calculate all msds at startup.""" if mod: mod = Mod(wot, mod) lastlevel = {key: None} seen = {key: None} forbiddenkeys = forbiddenkeys or {} seen.update(forbiddenkeys) total = 0 for lev in xrange(100): total += lev*len(lastlevel) thislevel = findnext(lastlevel, wot.sigs[DN], seen, mod, DN, 0) if len(thislevel) == 0: break seen.update(thislevel) lastlevel = thislevel # The MSD returned is calculated from the group that can reach # this key. If there exists keys that cannot reach this key, the # MSD for the key in the larger group is, in some sense, infinite. numofseen = len(seen)-len(forbiddenkeys) return total / numofseen, total, numofseen def str2key(key): """Transforms a key given as string to integer""" if key[0:2] == "0x": key = key[2:] key = long(key, 16) if key >= 2**31: key -= 2**32 return int(key) def key2str(key): """Transforms a key given as integer to string""" if key<0: key = key+2**32 key = string.zfill(hex(key)[2:], 8).upper() if key[-1] == "L": key = key[:-1] return key def fullkey(wot, key, certlvl=None, alt=None): """Transform key to string with both keyID and name""" c = u"" if certlvl is not None: c = u"%s " % clascii[certlvl] if key is not None: name = wot.names[key] if wot.obfuscateemail: name = name.replace('@', wot.obfuscatewith) return u"%s0x%s %s" % (c, key2str(wot.keys[key]), name) else: if alt is not None: return u'[Key %s not found]' % alt else: return u'[Key not found]' def bettersign(wot, key, numberofkeystoremember=10, interestingkeys=(), mod=None, timer=None): """Search for keys which signature should minimize distance.""" included = {key: None} conn = {key: None} total = 0 if not numberofkeystoremember: return [] keydistance = [ None for x in wot.keys ] for lev in xrange(100): for x in conn: keydistance [x] = lev total += lev * len(conn) conn = findnext(conn, wot.sigs[DN], included, mod, DN, 0) if not conn: break included.update(conn) if timer: import time totaltime = timer.get('time', 5) fraction = timer.get('fraction', 0.50) action = timer.get('action', 'raise') begintime = time.time() deadline = begintime + totaltime*fraction minimum = [(total,None) for x in xrange(numberofkeystoremember)] processedkeys = -1 for actualkey in interestingkeys: processedkeys += 1 if timer and time.time() >= deadline: fractionprocessed = processedkeys/len(interestingkeys) elapsedtime = time.time() - begintime if fractionprocessed < fraction: times = {'percentdone' : fractionprocessed*100, 'timeelapsed' : elapsedtime, 'calculatedtime' : elapsedtime/fractionprocessed, 'calculatedtimeleft' : elapsedtime/fractionprocessed - elapsedtime, 'appendstring': "Looking for most wanted signatures very costly; You may want to simply look at \n" "http://keyserver.kjsl.com/~jharris/ka/current/top50table.html instead.\n" "Or restrict the wanted signatures.\n" "Proceding anyway, but this might take a while.\n" } if action == 'raise': raise wotTimeoutError(times) elif action == 'stderr': print >>sys.stderr, wotTimeoutError(times) timer = None if actualkey not in included: continue # Filtered out by modstring actualsum = total seen = {actualkey: None, key: None} conn = {actualkey: None} for lev in xrange(1,100): modified = {} for x in conn: if keydistance[x] > lev: actualsum += lev - keydistance[x] modified[x] = None if not modified: break conn = findnext(modified, wot.sigs[DN], seen, mod, DN, 0) seen.update(conn) #print float(actualsum) / len(wot.keys), key2str(wot.keys[actualkey]) if actualsum < minimum[-1][0]: for x in xrange(len(minimum)): if actualsum < minimum[x][0]: break minimum[x:]=[(actualsum, actualkey)] + minimum[x:-1] for i in xrange(numberofkeystoremember): if minimum[i][1] is None: break return minimum[:i] def keystats(wot, key, mod=None, wanted=0, restrict=None, timer=None): """Statistics for a key""" seen = {key: None} conn = {key: None} total = totalweighted = 0 header = u"Statistics for key %s\n" % fullkey(wot, key) if mod: header += u"\n" header += u"These modifications of the web of trust were used:\n" header += unicode(mod) downtrace = \ u"Tracing downwards from this key. Keys in level 1 have signed this\n"\ "key, keys in level (n) have signed at least one key in level (n-1).\n\n" smalllevels = "Levels with 10 keys or less:\n" for lev in xrange(100): if len(conn) <= 10: smalllevels += u" Level %2d:\n" % lev for x in conn: smalllevels += u" %s\n" % fullkey(wot, x) total += len(conn) totalweighted += lev * len(conn) downtrace += u"Keys in level %2d: %6d\n" % (lev, len(conn)) conn = findnext(conn, wot.sigs[DN], seen, mod, DN) if not conn: break seen.update(conn) if not mod: downtrace += u"Total number of keys in strong set: %6d\n" % len(wot.keys) else: seenup = {key: None} conn = {key: None} for lev in xrange(100): conn = findnext(conn, wot.sigs[UP], seenup, mod, UP) seenup.update(conn) both = len([None for x in seen if x in seenup]) downtrace += u"Total number of keys reaching this key: %6d\n" % len(seen) downtrace += u"Total number of keys reachable by this key: %6d\n" % len(seenup) downtrace += u"Total number of keys both reachable by and reaching this key: %6d\n" % both downtrace += u"Total number of keys in strong set without modstring: %6d\n" % len(wot.keys) msds = u"Mean shortest distance: %2.4f\n" % \ (totalweighted / total) if mod: msds += u"From number of keys: %6d\n" % total msds += u" (MSD is %2.4f from %6d keys without modstring.)" % \ (msd(wot, key)[0], len(wot.keys)) want = u'' if wanted: interestingkeys = nametokey(wot, restrict, getall=1) best = bettersign(wot, key, wanted, interestingkeys, mod, timer) want += u"Most wanted signatures" if restrict: want += " from the %d keys matching '%s'" % \ (len(interestingkeys), restrict) want += u": \n" for x in best: if x[1] is not None: want += u"MSD=%2.4f if signed by %s\n" % \ (x[0]/total,fullkey(wot,x[1])) if best: want += u"MSD=%2.4f right now.\n" % (totalweighted/total) else: want += u"No wanted signatures found.\n" footer = u"Other report for this key are available:\n" \ u" http://keyserver.kjsl.com/~jharris/ka/current/%s/%s\n" \ u" http://thomas.butter.dk/gpg/getmsd.php?key=%s\n" \ u" http://pgp.cs.uu.nl/stats/%s.html\n" %\ (key2str(wot.keys[key]).upper()[:2],\ key2str(wot.keys[key]).upper() ,\ key2str(wot.keys[key]).upper() ,\ key2str(wot.keys[key]).upper()) sup = wot.sigs[UP][key] sdn = wot.sigs[DN][key] sigs_up = [(x, "-,%s" % clascii[sup[x]]) for x in sup if x not in sdn] sigs_cr = [(x, "%s,%s"%(clascii[sdn[x]],clascii[sup[x]])) for x in sup if x in sdn] sigs_dn = [(x, "%s,-" % clascii[sdn[x]]) for x in sdn if x not in sup] n, a = len(sdn), (wot.numofsigs / len(wot.keys)) if mod: ups = u"Modstring is not used below this point.\n\n" else: ups = u"" ups += u"This key is signed by %d key%s, which is " %(n, n!=1 and "s" or "") if n > a: ups += u"more than" elif n==a: ups += u"equal to" else: ups += u"less than" ups += u" the average %2.4f.\n" % a ups += u"This key has signed %d key%s.\n\n" % \ (len(sup), len(sup)!=1 and "s" or "") ups += u"Below, the numbers p,q before the keys are the cert check levels %s-%s,\n" \ % (clascii[4], clascii[7]) ups += u"or %s-%s if the primary UID is not signed. p is for the signature on the key \n" \ % (clascii[0], clascii[3]) ups += u"this report is about, and q is for the signature on the key to the right.\n\n" for x in [(sigs_dn, u"This key is signed by, excluding cross-signatures:"), (sigs_cr, u"This key is cross-signed with:"), (sigs_up, u"Keys signed by this key, excluding cross-signatures:")]: ups += x[1] + u"\n" for y in x[0]: ups += u" %s %s\n" % (y[1], fullkey(wot, y[0])) ups+=u"Total: %d key%s.\n\n" % (len(x[0]), len(x[0])!=1 and u"s" or u"") return header+'\n'+downtrace+'\n'+msds+'\n'+smalllevels+'\n'+want+'\n'+ups+'\n'+footer (pos_top, pos_middle, pos_bottom) = (-0.5, 0, 0.5) def yposition(pos, numlevels, where, boxh, height): return boxh/2+(pos+0.5)*(height-boxh)/numlevels + where*boxh # To create anything other than PNG, like SVG or text, these two are # the functions to change. def drawline(draw, x0, y0, x1, y1, config, certlevel=CL_unknown): """Draw an arrow in a PIL object""" if certlevel in (0,4,CL_unknown): wide = 0 else: wide = 1 if 0 <= certlevel < 4: dashed = 1 color = col_arrow_cert0 + certlevel elif 4 <= certlevel < 8: dashed = 0 color = col_arrow_cert0 + certlevel - 4 elif certlevel == CL_unknown: # Thin red arrow dashed = 0 color = col_arrow_cert0 + 1 elif certlevel == CL_added: dashed = 0 color = col_arrow_added else: raise AssertionError # Calculate line coordinates. x=x1-x0 y=y1-y0 length = math.sqrt(x**2 + y**2) if wide: # Shorten top and lower bottom so edges don't go outside arrow # heads and boxes. (But bottom is already low enough.) harrowlen = config["arrowlen"] / 2 xend = x1 - harrowlen*x/length yend = y1 - harrowlen*y/length disx = -1 disy = 0 if x>0: disy = -1 if x<0: disy = 1 p0x,p0y = (x0 -disx, y0 -disy) p1x,p1y = (xend-disx, yend-disy) p2x,p2y = (xend+disx, yend+disy) p3x,p3y = (x0 +disx, y0 +disy) if not dashed: # Signature on primary UID if not wide: draw.line( [x0, y0, x1, y1], fill=color) else: draw.polygon( [ (p0x,p0y), (p1x,p1y), (p2x,p2y), (p3x,p3y) ], outline=col_arrowb, fill=color) else: # Why doesn't PIL have a function for drawing dashed lines? dl = 5 dx = (x*dl)/length dy = (y*dl)/length if not wide: l = 0 while l < length: draw.line( [x0, y0, x0+dx, y0+dy], fill=color) x0 += 2*dx y0 += 2*dy l += 2*dl else: stop = math.sqrt((p0x-p1x)**2 + (p0y-p1y)**2) l = 0 while l < stop: draw.polygon( [(p0x ,p0y ), (p3x ,p3y ), (p3x+dx,p3y+dy), (p0x+dx,p0y+dy)], outline=col_arrowb, fill=color) p0x += 2*dx p3x += 2*dx p0y += 2*dy p3y += 2*dy l += 2*dl a = config["arrowang"] * 2*math.pi / 360 s = config["arrowlen"] / length # Rotation and scaling matrix: rotm = [ math.cos(a)*s, math.sin(a)*s, -math.sin(a)*s, math.cos(a)*s] rot0 = (-rotm[0]*x-rotm[1]*y , -rotm[2]*x-rotm[3]*y) rot1 = (-rotm[0]*x+rotm[1]*y , +rotm[2]*x-rotm[3]*y) draw.polygon( [ (x1 , y1 ), (x1+rot0[0], y1+rot0[1]), (x1+rot1[0], y1+rot1[1])], outline=col_arrowb, fill=color) def drawnode(draw, x, y, w, h, key, wot): """Draw a node (key) in a PIL object.""" draw.rectangle( (x,y,x+w,y+h), fill=col_boxi, outline=col_boxb) border = 2 x += border y += border w -= border*2 if w<0: w=0 h -= border*2 # What charset should we use? It depends on font, of course. keystr = key2str(wot.keys[key]) name = wot.names[key].encode('iso-8859-15', 'replace') lines = (keystr + '\n' + name.replace('<', '\n<')).split('\n', 2) sizes = [] totalheight = 0 for l in xrange(len(lines)): lines[l] = lines[l].strip() while wot.font.getsize(lines[l])[0] > w: lines[l] = lines[l][:-1] sizes.append(wot.font.getsize(lines[l])) totalheight += sizes[-1][1] yoffset = (h - totalheight) / 2 for l in xrange(len(lines)): xoffset = (w - sizes[l][0])/2 draw.text((x+xoffset, y+yoffset), lines[l], font=wot.font, fill=col_font) yoffset += sizes[l][1] def create_pil(web, keys, config, wot): """Create a PIL object with a graph of a web.""" logo = init_pil_get_logo() conf = defaultconfig.copy() if config: conf.update(config) size = conf["size"] boxh = wot.font.getsize('Al9F_-/\<>@"')[1] * 3.5 im = Image.new('P', size); palette = [ 0 for x in xrange(768) ] palette[0:len(defaultconfig["colors"])] = defaultconfig["colors"] if config and config.has_key("colors"): palette[0:len( config["colors"])] = config["colors"] im.putpalette(palette) draw = ImageDraw.Draw(im) levels = len(web) # Calculate widths. Proportional to # number_of_links_up*number_of_links_down widths = [[size[0]/2]] positions = [[size[0]/4]] for level in xrange(1, levels-1): numnodes = len(web[level]) lu = [0 for x in range(numnodes)] ld = [0 for x in range(numnodes)] for x in xrange(len(web[level])): lu[x] = len(web[level][x]) for x in web[level+1]: for y,certlevel in x: ld[y] += 1 w = [ u*d for u,d in zip(lu, ld)] tot = 0 for x in w: tot += x # Pad with a total width as wide as the average node. pad = tot/len(w) tot += pad # Adjust units to fit image pad *= size[0] / tot for x in xrange(len(w)): w[x] *= size[0] / tot # Calculate positions. Positions are always the _left_ corner. dist = pad / len(w) pos = dist/2 p = [] for x in xrange(len(w)): p.append(int(pos)) pos += w[x] + dist w[x] = int(w[x]) widths.append(w) positions.append(p) widths.append([size[0]/2]) positions.append([size[0]/4]) # Edges for level in xrange(1, levels): for xpos in xrange(len(web[level])): for pointto,certlevel in web[level][xpos]: drawline(draw, positions[level][xpos] + widths[level][xpos]/ (len(widths[level-1])+1) * (pointto+1) , yposition(level, levels, pos_top, boxh, size[1]), positions[level-1][pointto] + widths[level-1][pointto]/ (len(widths[level])+1) * (xpos+1) , yposition(level-1, levels, pos_bottom, boxh, size[1])+1, conf, certlevel=certlevel) # Nodes for level in xrange(levels): for xpos in xrange(len(web[level])): drawnode(draw, positions[level][xpos], yposition(level, levels, pos_top, boxh, size[1]), widths[level][xpos], boxh, keys[level][xpos], wot) im.paste(logo, (size[0]-logo.size[0]-10, size[1]-logo.size[1]-10,)) return im def webtoordered(web): """Transform unordered web to one suitable for graphing.""" webkeys = [web[0].keys()] webarrw = [[[]]] for level in xrange(1, len(web)): if len(web[level]) > 1: # A very simple edge crossing minimization heuristic: Give # number centered around 0 for each node. Close to center # of keys it links to, and close to the middle if many # edges on the other side. # TODO: Prefer graphs that minimizes crossings between # better signatures? nodes = web[level].keys() nextlevellinks = [] for x in web[level+1].values(): nextlevellinks.extend(x.keys()) center = (len(webkeys[level-1])-1) / 2. positions = [] for node in nodes: pos = 0. for edgeto in web[level][node]: pos += webkeys[level-1].index(edgeto)-center pos /= nextlevellinks.count(node) positions.append(pos) sorted = positions[:] sorted.sort() keys = range(len(web[level])) pos = 0 for x in sorted: index = positions.index(x) keys[pos] = nodes[index] positions[index] = None pos += 1 webkeys.append(keys) webarrw.append([ [ (webkeys[level-1].index(x), web[level][y][x]) for x in web[level][y]] for y in webkeys[level]]) else: webkeys.append(web[level].keys()) webarrw.append([ [ (webkeys[level-1].index(x), web[level][y][x]) for x in web[level][y]] for y in webkeys[level]]) return webkeys, webarrw # This could be done a lot better. We could draw a text graph similar # to the image created above. def textweb(wot, web): """Graph a web using text only.""" ret = "" for x in web: for y in x: if x[y]: for key in x[y]: ret += " [%s] %s\n" % (clascii[x[y][key]], fullkey(wot, key)) ret += "%s has signed those above \n" % fullkey(wot, y) else: ret += "%s\n" % fullkey(wot, y) ret += '\n' return ret keyre = re.compile("^(0x)?[0-9a-fA-F]{8}$") # We could do this much faster with a dictionary, but looping through # all keys is fast enough, and saves some memory. def nametokey(wot, name, getall=0): """Search key from name""" if getall and not name: return xrange(len(wot.keys)) name = name.strip() if keyre.search(name): key = str2key(name) try: k = wot.keys.index(key) except ValueError: if getall: return [] else: return None if getall: return [k] else: return k elif name == "random": k = random.randint(0, len(wot.keys)-1) if getall: return [k] else: return k else: if getall: ret = [] words = name.lower().split() if len(words)==0: if getall: return xrange(len(wot.keys)) else: return 0 for i in xrange(len(wot.names)): for word in words: if wot.names[i].lower().find(word) == -1: break else: if getall: ret.append(i) else: return i if getall: return ret def print_key(wot, key, firstindent, indent): yield firstindent + fullkey(wot, key) sigs = wot.sigs[DN][key] sigslist = list(sigs) sigslist.sort() for signedby in sigslist: yield indent + fullkey(wot, signedby, sigs[signedby]) def print_wot(wot): """Print all keys and signatures in wot. Unsorted.""" for key in xrange(len(wot.keys)): for line in print_key(wot, key, u"", u" "): yield line # XXX If onlykeys, show differences in both directions? def diff_wots(wot0, wot1, onlykeys=None): """Print differences between two web-of-trusts.""" import difflib yield u"Metadata:" for line in difflib.ndiff([wot0.filename+"\n", "Fileversion: %s\n" % wot0.fileversion], [wot1.filename+"\n", "Fileversion: %s\n" % wot1.fileversion]): yield line[:-1] # Diff the debug files yield u"Debug:" wot0debug = wot0.debug or "\ [No debug information found]\n" wot1debug = wot1.debug or "\ [No debug information found]\n" for line in difflib.unified_diff(wot0debug.splitlines(1), wot1debug.splitlines(1)): yield line[:-1] # Diff the READMEs yield u"README:" for line in difflib.ndiff(wot0.readme.splitlines(1), wot1.readme.splitlines(1)): yield line[:-1] # Build reverse lookup dictionaries keys0 = {} keys1 = {} for wot, keys in (wot0, keys0), (wot1, keys1): for keynr in xrange(len(wot.keys)): keys[wot.keys[keynr]] = keynr if onlykeys is not None: # Don't diff the whole WOT. keys0_view = {} keys1_view = {} for key in onlykeys: if key in keys0: keys0_view[key] = keys0[key] if key in keys1: keys1_view[key] = keys1[key] else: keys0_view, keys1_view = keys0.copy(), keys1.copy() common_keys = keys0_view.copy() # Not common yet. Some are deleted below. yield u"Removed keys:" for key in keys0_view.copy(): if key not in keys1: for line in print_key(wot0, keys0[key], u" - ", u" "): yield line del common_keys[key] yield u"New keys:" for key in keys1_view.copy(): if key not in keys0: for line in print_key(wot1, keys1[key], u" + ", u" "): yield line yield u"Changed names:" for key in common_keys: if wot0.names[keys0[key]] != wot1.names[keys1[key]]: yield u" ! %s -> %s" % (fullkey(wot0, keys0[key]), wot1.names[keys1[key]]) if onlykeys is not None: yield u"MSDs:" for key in common_keys: msd0, msd1 = msd(wot0, keys0[key]), msd(wot1, keys1[key]) # XXX Modstring msd0str, msd1str = "%2.4f"%msd0[0], "%2.4f"%msd1[0] if msd0str != msd1str: c = "!" else: c = " " # Always print MSD. if msd0str != msd1str: yield u" %s %s -> %s %s" % (c, msd0str, msd1str, fullkey(wot1, keys1[key])) yield u"Changed signatures:" for key in common_keys: sigs0, sigs1 = {}, {} tmp = wot0.sigs[DN][keys0[key]] for x in tmp: sigs0[wot0.keys[x]] = tmp[x] tmp = wot1.sigs[DN][keys1[key]] for x in tmp: sigs1[wot1.keys[x]] = tmp[x] del tmp changed = 0 for sig in sigs0.copy(): if sig not in sigs1: if not changed: yield u" " + fullkey(wot1, keys1[key]);changed=1 yield u" - " + fullkey(wot0, keys0[sig], sigs0[sig]) del sigs0[sig] for sig in sigs1.copy(): if sig not in sigs0: if not changed: yield u" " + fullkey(wot1, keys1[key]);changed=1 yield u" + " + fullkey(wot1, keys1[sig], sigs1[sig]) del sigs1[sig] for sig in sigs0: if sigs0[sig] != sigs1[sig]: if not changed: yield u" " + fullkey(wot1, keys1[key]);changed=1 yield u" ! %s -> %s %s" % (clascii[sigs0[sig]], clascii[sigs1[sig]], \ fullkey(wot0, keys0[sig])) def wotstats(wot): """Statistics about the whole WOT""" ret = "Statistics for this Web of Trust:\n" ret += "Total number of keys: %6d\n" % len(wot.keys) ret += "Total number of signatures: %6d\n" % wot.numofsigs ret += "Average signatures per key: %2.4f\n" % \ (wot.numofsigs / len(wot.keys)) ret += "\n" if wot.readme: ret += "The Web of Trust dump contained this README file:\n" ret += "\n" ret += wot.readme else: ret += "The Web of Trust dump contained no README file.\n" return ret # TODO: Make HTML version with coloured stripes, to you can find the # right connections. def groupmatrix(wot, keys, modstring=None): dlen = len(str(len(keys)-1)) _tmp = u"%"+str(dlen)+u"d" s = [ _tmp % x for x in xrange(len(keys))] fullkeys = [fullkey(wot, key, alt=alt) for key,alt in keys] if modstring: yield u'Warning: modstring not implemented.' # Print top/bottom enumeration def vertenum(chars, nrkeys): for n in xrange(chars): line = u" " + u" " * dlen for m in xrange(nrkeys): line += s[m][n] yield line yield u"The numbers on the same row as a key are signatures on that key made" yield u"by the key belonging to that column." yield u"" notfoundkeys = keys.count(None) totalsigs = 0 nesigs = 0 sigsnekeys = 0 numberofsigs = [0 for x in clascii] for line in vertenum(dlen, len(keys)): yield line yield u" " + u" "*dlen + u"." + u"-"*len(keys) + u"." # Print matrix for ny in xrange(len(keys)): y = keys[ny][0] # Python2.2 doesn't have enumerate. line = u"" for nx in xrange(len(keys)): x = keys[nx][0] # Python2.2 doesn't have enumerate. if x is not None and y is not None: if x == y: line += "\\" elif x in wot.sigs[DN][y]: totalsigs += 1 numberofsigs[wot.sigs[DN][y][x]] += 1 line += clascii[wot.sigs[DN][y][x]] else: totalsigs += 1 nesigs += 1 line += " " else: if nx == ny: line += "\\" else: totalsigs += 1 sigsnekeys += 1 line += " " yield u" %s|%s|%s - %s" % (s[ny], line, s[ny], fullkeys[ny]) yield u" " + u" "*dlen + u"`" + u"-"*len(keys) + u"´" for line in vertenum(dlen, len(keys)): yield line _tmp = u"%"+str(len(str(totalsigs)))+u"d" yield "" yield ("Number of keys not found: "+_tmp) % notfoundkeys yield ("Number of possible signatures: "+_tmp) % totalsigs yield ("of which involves not found keys: "+_tmp) % sigsnekeys # Python2.2 doesn't have sum. if 'sum' in dir(__builtins__): _tmp2 = sum(numberofsigs) else: _tmp2=0 for x in numberofsigs: _tmp2+=x yield ("Number of signatures: "+_tmp) % _tmp2 for x in xrange(len(clascii)): c = clascii[x] # Python2.2 doesn't have enumerate if x == CL_unilink or x == CL_added: continue yield ("Number of signatures with level %s: "+_tmp) % (c, numberofsigs[x]) yield ("Number of non-existant signatures: "+_tmp) % nesigs # Printing names vertically. This is ugly, but might come in # handy? #maxlen = max([len(x) for x in fullkeys]) #for y in xrange(maxlen): # line = u" "*dlen + u" " # for x in xrange(len(keys)): # name = fullkeys[x] # if len(name) > y: # line += name[y] # else: # line += u" " # yield line class Mod: """Modstrings - to specify temporary changes in a Wot.""" keyre = re.compile("^0x[0-9a-fA-F]{8}$") sigre = re.compile("^0x[0-9a-fA-F]{8}-0x[0-9a-fA-F]{8}$") lvlre = re.compile("^level-[%s]$" % clascii.replace("-","\-") .replace("]","\]").replace("^","\^")) def __init__(self, wot, modstr): self.wot = wot self.modstr = modstr self.exclunlk = 0 self.exclsigs, self.exclkeys, \ self.excllvls, self.add_sigs, self.add_keys = {},{},{},{},{} def e(str): raise wotModstringError, u'Error in modstring "%s": %s' % (modstr, str) for statement in modstr.split(","): if statement[3:4] != ":": e(u'"%s".' % statement) op = statement[0:3] if op == "add": op = 1 elif op == "del": op = 0 else: e(u'"%s".' % op) arg = statement[4:] if self.keyre.search(arg): if not op: self.exclkeys[nametokey(wot, arg)] = None else: e(u'Adding of keys not implemented.') elif self.sigre.search(arg): if op: self.add_sigs[nametokey(wot, arg[ 0:10]), nametokey(wot, arg[11:21])]=None else: self.exclsigs[nametokey(wot, arg[ 0:10]), nametokey(wot, arg[11:21])]=None elif self.lvlre.search(arg): if op: e(u'Adding signature level means nothing.') level = clascii.index(arg[6]) if level == CL_unilink: self.exclunlk = 1 else: self.excllvls[level] = None else: e(u'"%s"'%arg) def __str__(self): ret = u'Modstring: "%s"\n' % self.modstr for x in self.exclsigs: ret += u"Excluded signature %s -> %s\n" % (fullkey(self.wot, x[0]), fullkey(self.wot, x[1])) for x in self.add_sigs: ret += u"Included signature %s -> %s\n" % (fullkey(self.wot, x[0]), fullkey(self.wot, x[1])) for x in self.exclkeys: ret += u"Excluded key %s\n" % (fullkey(self.wot, x)) for x in self.add_keys: ret += u"Included key %s\n" % (fullkey(self.wot, x)) for x in self.excllvls: ret += u"Excluded all signatures with level %s\n" % clascii[x] if self.exclunlk: ret += u"Excluded add non-cross signatures.\n" return ret class Wot: """Don't think the module and class interfaces are stable. They are not. There are lots of things that needs cleanup first.""" def __init__(self, dumpfile, fontfile=None, obfuscateemail=False, obfuscatewith='(%)', ttffile=None, ttfsize=16): if fontfile or ttffile: self.initfont(fontfile, ttffile, ttfsize) self.sigs = range(2) self.loadfile(dumpfile) self.sigs[UP] = reversesigs(self.sigs[DN]) self.numofsigs = 0 for x in self.sigs[UP]: self.numofsigs += len(x) self.obfuscateemail = obfuscateemail self.obfuscatewith = obfuscatewith def __len__(self): return len(self.keys) def loadfile(self, dumpfile): (self.names, self.keys, self.sigs[DN], self.readme, self.filename, self.fileversion, self.debug) = loadfile(dumpfile) # XXX Don't make the same mistake as PIL makes, and only take filename. def initfont(self, fontfile=None, ttffile=None, ttffilesize=16): init_pil_get_logo() if ttffile: self.font = ImageFont.truetype(ttffile, ttffilesize) return if fontfile: self.font = ImageFont.load(fontfile) return import StringIO, base64, zlib self.font = ImageFont.ImageFont() pilstr = base64.decodestring(_fontstr[0]) pbmstr = base64.decodestring(_fontstr[1]) #self.font = ImageFont.load_default() # XXX Suggest to PIL hackers to provide this without leading _. # This only works on PIL 1.1.4 (and higher?) if "_load_pilfont_data" in dir(self.font): self.font._load_pilfont_data(StringIO.StringIO(pilstr), Image.open(StringIO.StringIO(pbmstr))) else: # OK, this is not race free, but there is no way to be # race free on Python 2.2. This isn't needed at all with # PIL 1.1.4. import tempfile tmpdir = tempfile.mktemp("-wotsappilhack.tmp") # Not race free. os.mkdir(tmpdir) f0 = tmpdir + "/font.pil" f1 = tmpdir + "/font.pbm" print >>sys.stderr, \ "Warning: Old PIL version, using temp files %s and %s." %\ ( f0, f1) try: open(f0, "wb").write(pilstr) open(f1, "wb").write(pbmstr) self.font = ImageFont.load("/tmp/pilfonthack.pil") finally: for f in f0, f1: try: os.remove(f) except OSError: pass try: os.rmdir(tmpdir) except OSError, (errno, strerror): print >>sys.stderr, \ 'Unable to remove temporary directory "%s": %s' % (tmpdir, strerror) def nametokey(self, name): key = nametokey(self, name) if key is not None: return "0x" + key2str(self.keys[key]) raise wotKeyNotFoundError(name) def creategraph(self, web, config=None, format='txt'): if format=='txt': return textweb(self, web) elif format == 'PIL': (webkeys, webarrw) = webtoordered(web) return create_pil(webarrw, webkeys, config, self) else: raise ValueError def findpaths(self, nbottom, ntop, modstr=None): bottom = nametokey(self, nbottom) if bottom is None: raise wotKeyNotFoundError(nbottom) top = nametokey(self, ntop ) if top is None: raise wotKeyNotFoundError(ntop) if modstr: mod = Mod(self, modstr) else: mod = None return findpaths(self, bottom, top, mod=mod) def keystats(self, name, modstring=None, wanted=0, restrict=None, timer=None): key = nametokey(self, name) if key is None: raise wotKeyNotFoundError(name) if modstring: mod = Mod(self, modstring) else: mod = None return keystats(self, key, mod=mod, wanted=wanted, restrict=restrict, timer=timer) def wotstats(self): return wotstats(self) def groupmatrix(self, keys, searchstring=None, modstring=None, nounknowns=0, maxkeys=200): if keys: keys = [(nametokey(self, key), key) for key in keys.split(",")] else: keys = [] if searchstring: skeys = nametokey(self, searchstring, getall=1) if len(skeys) > maxkeys: raise wotTooManyError(len(skeys)) keys += [(key, "%s not found"%searchstring) for key in skeys] if nounknowns: keys = [(k, s) for (k, s) in keys if k is not None] for line in groupmatrix(self, keys, modstring): yield line def listkeys(self, keys): keylist = nametokey(self, keys, getall=1) yield u'Listing the %d keys matching "%s" out of totally %d keys:' % \ (len(keylist), keys, len(self.keys)) for key in keylist: yield fullkey(self, key) yield 'Comma separated list of KeyIDs:' yield ",".join(["0x"+key2str(self.keys[key]) for key in keylist]) def msd(self, key, modstring=None, forbiddenkeys=None): return msd(self, key, modstring, forbiddenkeys) def wotsapmain(argv): import locale, getopt # Python 2.3 seems to encode to LC_CTYPE automatically, but 2.2 # does not. Leave it for now. if "getpreferredencoding" in dir(locale): encoding = locale.getpreferredencoding() else: locale.setlocale(locale.LC_CTYPE, "") encoding = locale.nl_langinfo(locale.CODESET) def usage(i=0): if i: out = sys.stderr else: out = sys.stdout print >>out, ( u"Usage: %s [OPTION]... [bottomkey [topkey]]\n" \ u"\n" \ u"Options:\n" \ u" -h, --help Show help\n" \ u" --version Show version\n" \ u" -w, --wot=FILE Read web-of-trust information from FILE.\n" \ u" Defaults to ~/.wotsapdb\n" \ u" -m, --modify=STR Use STR as wot modification string.\n" \ u" -g, --group Print signature matrix of comma separated keys.\n" \ u" -G, --nounknowns Don't print unknown keys in signature matrix.\n" \ u" -o, --png=FILE Write .png output to FILE.\n" \ u" -O, --show-png=PRG Use PRG to view (temporary) PNG image\n" \ u" -s, --size=NNNxMMM Set image size to NNN times MMM.\n" \ u" -F, --font=FILE (Only needed in png output) Font file in .pil/.pbm format.\n" \ u" Point it to the .pil file, with the .pbm file in the same\n" \ u" directory.\n" \ u" -T, --ttffont=FILE As -F but with a TrueType font file.\n" \ u" -S, --ttfsize=num Size to use for TrueType font. Defaults to 16.\n" \ u" -p, --print Print the whole web-of-trust in human readable format.\n" \ u" -D, --print-debug Print the debug information in the .wot file.\n" \ u" -d, --diff=FILE Print all differences between two .wot files.\n" \ u" -M, --msd Just show MSD for key.\n" \ u" -W, --wanted[=NUM] Show the NUM(10) 'most wanted signatures' for key.\n" \ u" -r, --restrict=STR Restrict wanted signatures with STR, implies -W.\n" \ % argv[0] ).encode(encoding, 'replace') sys.exit(i) def showversion(): print ( u"wotsap (Web of trust statistics and pathfinder) version %s\n" \ u"\n" \ u"Copyright (C) 2003,2004 Jörgen Cederlöf\n" \ u"\n" u"This is free software; see the source for copying conditions. There is NO\n" \ u"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." \ % VERSION ).encode(encoding, 'replace') sys.exit(0) try: if 'gnu_getopt' in dir(getopt): gopt = getopt.gnu_getopt else: gopt = getopt.getopt opts, args = gopt(argv[1:], "hw:m:gGo:O:s:F:pDd:MW:r:T:S:", ["help", "wot=", "modify=", "group", "nounknowns", "png=", "show-png=", "size=", "font=", "print", "print-debug", "diff=", "version", "msd", "wanted=", "restrict=", "ttffont=", "ttfsize="]) except getopt.GetoptError: usage(2) wotfile = os.path.expanduser("~/.wotsapdb") fontfile = None ttffile = None ttfsize = 16 group = None pngfile = None modifystr = None pngprg = None printmsd = 0 wanted = 0 restrict = None prnt = 0 prnt_debug = 0 diff = None size = None nounknowns = 0 for o, a in opts: if o in ("-h", "--help"): usage(0) elif o in ("--version",): showversion() elif o in ("-w", "--wot"): wotfile = a elif o in ("-m", "--modify"): modifystr = a elif o in ("-g", "--group"): group = 1 elif o in ("-G", "--nounknowns"): nounknowns = 1 elif o in ("-o", "--png"): pngfile = a elif o in ("-O", "--show-png"): pngprg = a elif o in ("-s", "--size"): size = a elif o in ("-F", "--font"): fontfile = a elif o in ("-T", "--ttffile"): ttffile = a elif o in ("-S", "--ttfsize"): ttfsize = float(a) elif o in ("-p", "--print"): prnt=1 elif o in ("-D", "--print-debug"): prnt_debug=1 elif o in ("-d", "--diff"): diff = a elif o in ("-M", "--msd"): printmsd = 1 elif o in ("-W", "--wanted"): if a: wanted = int(a) else: wanted = 10 elif o in ("-r", "--restrict"): restrict = a if not wanted: wanted = 10 top=bottom=None if len(args)==1: top = unicode(args[0], encoding, 'replace') elif len(args)==2: top = unicode(args[1], encoding, 'replace') bottom = unicode(args[0], encoding, 'replace') elif len(args)>=3: usage(1) try: wot = Wot(wotfile) except wotLoadFileError, err: print unicode(err).encode(encoding, 'replace') print "You need a .wot file which contains the list of keys and signatures." print "You can download the latest .wot file from either" print " http://www.lysator.liu.se/~jc/wotsap/wots2/latest.wot" print " or" print " http://www.rubin.ch/wotsap/latest.wot" print "and either name it ~/.wotsapdb or give the filename as the -w argument." sys.exit(1) if group: if bottom: print >>sys.stderr, u"Please supply one argument only." sys.exit(7) if "," in top: keys = top searchstring = None else: keys = None searchstring = top for line in wot.groupmatrix(keys, searchstring=searchstring, nounknowns=nounknowns): print line.encode(encoding, 'replace') sys.exit(0) if prnt: for line in print_wot(wot): print line.encode(encoding, 'replace') sys.exit(0) if prnt_debug: if wot.debug: print wot.debug.encode(encoding, 'replace') else: print u"Sorry, no debug information found in file." \ .encode(encoding, 'replace') sys.exit(0) if printmsd: if not top: usage(2) keyn = nametokey(wot, top) if keyn is None: print >>sys.stderr, (u"Sorry, key \"%s\" not found." % top) \ .encode(encoding, 'replace') # DEBUG: Useful for checking speed in msd() and findnext(). if 0: import time a = time.time() for x in xrange(10): __msd_standalone(wot, x) b = time.time() for x in xrange(10): msd(wot, x) c = time.time() ab,bc = b-a,c-b print ab,bc print ab/ab, bc/ab print msd(wot, keyn, modifystr)[0] sys.exit(0) if diff: try: wot1 = Wot(diff) except wotLoadFileError, err: print unicode(err).encode(encoding, 'replace') sys.exit(1) if top: key = nametokey(wot, top) if key is not None: key = wot.keys[key] else: key = nametokey(wot1, top) if key is not None: key = wot1.keys[key] if key is None: print >>sys.stderr, (u"Sorry, key \"%s\" not found." % top) \ .encode(encoding, 'replace') sys.exit(7) key = [key] else: key = None for line in diff_wots(wot, wot1, key): print line.encode(encoding, 'replace') sys.exit(0) if top and bottom: try: web = wot.findpaths(bottom, top, modstr=modifystr) except (wotKeyNotFoundError, wotModstringError), err: print >>sys.stderr, unicode(err).encode(encoding, 'replace') sys.exit(2) if web is None: print >>sys.stderr, "Sorry, unable to find path." sys.exit(1) print wot.creategraph(web, format='txt').encode(encoding, 'replace') if pngfile or pngprg: config = {} if size is not None: config['size'] = tuple([int(i) for i in size.split('x')]) wot.initfont(fontfile, ttffile, ttfsize) ret = wot.creategraph(web, format="PIL", config=config) if pngprg and not pngfile: import tempfile # Python 2.2 doesn't have mkstemp. if 'mkstemp' in dir(tempfile): pngfile = tempfile.mkstemp(".png", "wotsap-")[1] else: pngfile = tempfile.mktemp(".png") # Not race free print >>sys.stderr, "Writing .png file %s: " % pngfile, try: f = open(pngfile, "wb") except IOError, (errno, strerror): print >>sys.stderr, \ 'Unable to open file: %s' % strerror sys.exit(2) ret.save(f, "png", optimize=1) # XXX Option to use pngcrush or similar to reduce size? print >>sys.stderr, "Done." if pngprg: #ret = os.spawnlp(os.P_WAIT, pngprg, pngprg, pngfile) # sh will print error message if needed. ret = os.system("%s %s" % (pngprg, pngfile)) try: os.remove(pngfile) except OSError, (errno, strerror): # The file is probably already removed. print >>sys.stderr, \ 'Warning: Unable to remove temporary file %s: %s' \ % (pngfile, strerror) elif top: try: timer = {'time' : 3, 'fraction' : 0.50, 'action' : 'stderr'} stats = wot.keystats(top, modstring=modifystr, wanted=wanted, restrict=restrict, timer=timer) except (wotKeyNotFoundError, wotModstringError), err: print >>sys.stderr, unicode(err).encode(encoding, 'replace') sys.exit(2) else: print stats.encode(encoding, 'replace') else: print wot.wotstats().encode(encoding, 'replace') if __name__ == "__main__": wotsapmain(sys.argv) wotsap-0.7/Webware-Main.py0000644000175000017500000003042410656267766013713 0ustar jczjcz#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # This file is meant to be used with Webware (http://webware.sf.net/). # Part of Web of trust statistics and pathfinder, Wotsap # Copyright (C) 2003 Jörgen Cederlöf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. dumpfile = "/home/jc/wots/latest.wot" url = "http://webware.lysator.liu.se/jc/" minsize = 200,50 maxsize = 2000,2000 defaultsize = 980,700 maxwanted = 1000 from WebKit.Page import Page import sys import os import wotsap import re # _foo=[bar] arguments is an idiom for static variables. # An argument gets its default value only once, so if it is mutable it # can be changed. The underscore is there to remind the caller that # the variable is internal. # TODO This should be called once in a while by some sort of timer, to # avoid long load times for the first request. Is that possible with # Webware? def getlatestwot(path=dumpfile, _lastmtime=[0], _wot=[None]): if os.lstat(path).st_mtime != _lastmtime[0]: _lastmtime[0] = os.lstat(path).st_mtime wot = wotsap.Wot(path, obfuscateemail=True) wot.initfont() _wot[0] = wot return _wot[0] def handlelegacyurl(wot, fields, redirect): """Handle the early, now obsolete, URL scheme.""" def fieldtokey(field, regexp="^0x[0-9a-fA-F]{8}$"): if field in fields and re.compile(regexp).search(fields[field]): return fields[field] bottom, top = fieldtokey('bottom'), fieldtokey('top') if bottom and top: size = fieldtokey('size', '^[0-9]+x[0-9]+$') if size: args = "?size=" + size else: args = "" redirect(url+"wotsap/wots/latest/paths/%s-%s.png%s"%(bottom,top,args)) elif top: redirect(url+"wotsap/wots/latest/keystatistics/%s.txt"%top) else: redirect("http://www.lysator.liu.se/~jc/wotsap/") def handleurl(urlpath, fields, write, setheader, redirect, urlencode): """Parse URL and take appropriate action.""" wot = getlatestwot() # Arguments def checkfields(allowedfields, mandatoryfields=()): ret = "" for field in fields: if field not in mandatoryfields: if field not in allowedfields: raise ValueError('Unknown field "%s".' % field) if fields[field]: if not ret: ret += '?' else: ret += '&' ret += '%s=%s' % (urlencode(field), urlencode(fields[field])) for field in mandatoryfields: if field not in fields or not fields[field]: raise ValueError('Mandatory field "%s" not specified.' % field) return ret modstring = fields.get('modstring', None) if 'size' in fields and fields['size']: try: size = fields['size'].split('x', 1) size = int(size[0]), int(size[1]) if not minsize[0] <= size[0] <= maxsize[0] or \ not minsize[1] <= size[1] <= maxsize[1]: raise ValueError( 'Size too large or too small: %s\n'%fields['size'] +\ 'It must be between %dx%d and %dx%d, inclusive.\n' % (minsize+maxsize)) except IndexError, err: raise ValueError('Error in size "%s". Size must be in format ' \ '"1024x768".' % fields['size']) else: size = defaultsize if 'date' in fields and fields['date']: raise NotImplementedError('Error: date not implemented yet.') # Parse URLpath kre = "0x[0-9a-fA-F]{8}" def matches(regexp): return re.compile(regexp).search(urlpath) # Remove double slashes tmp = -1 while tmp != len(urlpath): tmp = len(urlpath) urlpath = urlpath.replace('//', '/') del tmp if urlpath[0:1] == "/": urlpath = urlpath[1:] # Empty URLpath? if matches("^/*$"): redirect("http://www.lysator.liu.se/~jc/") return # Legacy URLpath? elif matches("^wotsap/$"): if not fields: # We dont support statistics this way anymore. redirect("http://www.lysator.liu.se/~jc/wotsap/") else: return handlelegacyurl(wot, fields, redirect) # Action. elif matches('^wotsap/wots/latest/wotinfo.txt$'): checkfields([]) setheader('Content-type', 'text/plain; charset=utf-8' ) write(wot.wotstats().encode('utf-8', 'replace')) elif matches('^wotsap/wots/latest/debuginfo.txt$'): checkfields([]) setheader('Content-type', 'text/plain; charset=utf-8' ) write(wot.debug.encode('utf-8', 'replace')) elif matches('^wotsap/wots/latest/keystatistics/%s\\.txt$'%kre): args = checkfields(['modstring', 'wanted', 'restrict']) wanted = fields.get('wanted', 0) restrict = fields.get('restrict', None) if wanted: wanted = int(wanted) if wanted > maxwanted: raise ValueError( 'Too large wanted value %d. It must be no larger than %d.'\ % (wanted, maxwanted)) key = urlpath[-14:-4] timer = {'time' : 8, 'fraction' : 0.10, 'action' : 'raise'} try: stats = wot.keystats(key, modstring=modstring, wanted=wanted, restrict=restrict, timer=timer).encode('utf-8', 'replace') except wotsap.wotTimeoutError, err: setheader('Content-type', 'text/plain; charset=utf-8' ) err.times['appendstring'] = \ '\nSorry, this is to much. Try restricting to fewer keys or run the stand-alone program.\n'\ 'Or look at http://keyserver.kjsl.com/~jharris/ka/current/top50table.html.\n' raise ValueError(str(err)) setheader('Content-type', 'text/plain; charset=utf-8' ) write(stats) elif matches('^wotsap/wots/latest/paths/%s-%s\\.png$'%(kre,kre)): checkfields(['modstring', 'size']) bottomkey = urlpath[-25:-15] topkey = urlpath[-14: -4] web = wot.findpaths(bottomkey, topkey, modstr=modstring) graph = wot.creategraph(web, config={'size': size}, format="PIL") setheader('Content-type', 'image/png') tmpwrite = write class tmpfile: # PIL documentation says we need seek, tell, and write, # but write seems to be sufficient. write = tmpwrite graph.save(tmpfile, 'png') elif matches('^wotsap/wots/latest/groupmatrix/.*\\.txt$'): checkfields(['modstring']) keys = urlpath[31:-4] setheader('Content-type', 'text/plain; charset=utf-8') for line in wot.groupmatrix(keys, modstring=modstring): write(line.encode('utf-8', 'replace') + '\n') # Searches # raise wot.wotKeyNotFoundError(str2key(path[14:24])) elif matches('^wotsap/search/wotinfo$'): checkfields([]) redirect(url+"wotsap/wots/latest/wotinfo.txt") elif matches('^wotsap/search/debuginfo$'): checkfields([]) redirect(url+"wotsap/wots/latest/debuginfo.txt") elif matches('^wotsap/search/keystatistics$'): args = checkfields(['modstring', 'wanted', 'restrict'], ['key']) key = wot.nametokey(fields['key']) redirect(url+"wotsap/wots/latest/keystatistics/%s.txt"%key +args) elif matches('^wotsap/search/paths$'): args = checkfields(['modstring', 'size'], ['bottom', 'top']) bottom = wot.nametokey(fields['bottom']) top = wot.nametokey(fields['top' ]) redirect(url+"wotsap/wots/latest/paths/%s-%s.png"%(bottom,top) +args) elif matches('^wotsap/search/groupmatrix$'): args = checkfields(['modstring'], ['keys']) namelist = fields['keys'].split(',') if len(namelist) > 1: def nametokey(name): try: return wot.nametokey(name) except wotsap.wotKeyNotFoundError: return name keys = ','.join([nametokey(name) for name in namelist]) redirect(url+"wotsap/wots/latest/groupmatrix/%s.txt"%keys +args) else: setheader('Content-type', 'text/plain; charset=utf-8') for line in wot.groupmatrix(None, searchstring=namelist[0], modstring=modstring): write(line.encode('utf-8', 'replace') + '\n') elif matches('^wotsap/search/listkeys$'): args = checkfields([], ['keys']) setheader('Content-type', 'text/plain; charset=utf-8') for name in wot.listkeys(fields['keys']): write(name.encode('utf-8', 'replace') + '\n') else: # Unknown URLpath redirect("http://www.lysator.liu.se/~jc/wotsap/") #setheader('Content-type', 'text/plain; charset=utf-8') #write("Unknown path: ") #write(urlpath.encode('utf-8', 'replace') + '\n') #write(str(fields) + '\n') def errorwrapper(urlpath, fields, write, setheader, sendredirect, urlencode): try: return handleurl(urlpath, fields, write, setheader, sendredirect, urlencode) except (ValueError, NotImplementedError, wotsap.wotTooManyError, wotsap.wotKeyNotFoundError, wotsap.wotPathNotFoundError, wotsap.wotModstringError), err: setheader('Content-type', 'text/plain; charset=utf-8') write(unicode(err).encode('utf-8', 'replace')) # Every time someone requests a page, Webware calls Main.writeHTML. # Webware is kind enough to give us both the whole URL between # "http://webware.lysator.liu.se/jc/" and "?", and both GET and POST # form entries nicely interpreted and put into a dictionary. All we # have to do is parse these and take appropriate action. class Main(Page): def writeHTML(self): request = self.request() sendredirect = self.response().sendRedirect setheader = self.response().setHeader urlencode = self.urlEncode fields = self.request().fields() urlpath = self.request().originalURLPath() write = self.write # Useful when debugging: #def sendredirect(url): # setheader('Content-type', 'text/plain; charset=utf-8') # write('Intercepted redirect to: %s' % url) # The strings we get from Webware are just the 7bit URL # decoded to 8bit octet streams. HTML4.01 says that that # stream is latin1 encoded. In practice, things are very # complicated. See e.g. # http://ppewww.ph.gla.ac.uk/~flavell/charset/form-i18n.html # . It seems to indicate that browsers often choose the same # encoding as the web page the form is on. If we code the web # page in iso-8859-1, both that rule and the HTML4.01 spec # says that the string should be iso-8859-1. This will make it # impossible to enter other characters. The only other simple # alternative would be to have the web page coded in UTF-8 and # treat all form data as UTF-8, but that would mean that # standards-conforming browsers wouldn't be able to send # non-ASCII iso-8859-1 characters. Tons of other heuristics # exists if someone wants to make it more stable. urlpath = urlpath.decode('iso-8859-1', 'replace') for key in fields: # The field names themselves are ascii. # If a field is supplied more than once, Webware gives us # a list of strings instead of a string. We use the last # one. if type(fields[key]) is type([]): fields[key] = fields[key][-1] fields[key] = fields[key].decode('iso-8859-1', 'replace') return errorwrapper(urlpath, fields, write, setheader, sendredirect, urlencode) wotsap-0.7/README0000644000175000017500000000122210656267766011735 0ustar jczjcz Web of trust statistics and pathfinder, Wotsap This package contains the code used at http://www.lysator.liu.se/~jc/wotsap/. Most documentation is available on the web pages. README: This file. wotsap: The main Wotsap Python program. It also works as a module if you rename it to wotsap.py . Webware-Main.py: Glue between wotsap.py and Webware (http://webware.sourceforge.net/). pks2wot: A program to create .wot files using the pksclient program. You need a local pks keyserver to use it. It should be fairly easy to adopt it to client programs for other keyservers. wotfileformat.txt: Specification for the .wot file format.