[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Big pile of changes -- 0.0.4rc1 draws ever closer.



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv21169/src/minion/lib/mixminion

Modified Files:
	BuildMessage.py ClientMain.py Common.py Config.py Crypto.py 
	MMTPClient.py Main.py Packet.py ServerInfo.py __init__.py 
	benchmark.py test.py 
Log Message:
Big pile of changes -- 0.0.4rc1 draws ever closer.  

Main:
- Add a big warning banner to say we aren't compatible with anything
  right now.

BuildMessage, Packet, PacketHandler:
- Change to new packet format (2048-bit RSA keys, overlapping
  encryption)

setup.py:
- Bump version to 0.0.4alpha2

ClientMain:
- Add a handy 'ping' command -- too handy to stay in the codebase, but
  useful for testing.

Common:
- Add a checkPrivateFile function to enforce file permissions.

Config, ServerConfig, ServerInfo, Modules:
- Refactor away a lot of useless code.  I once thought the
  configuration files would be self-reloading, and everybody would use
  some kind of publish/subscribe mechanism to update themselves -- but
  that's kinda silly in practise.
- Add a prevalidate function so that we can freak out sooner if the
  version number doesn't match.

ServerInfo:
- Move IP to Incoming/MMTP section
- Bump Descriptor-Version to 0.2

Crypto:
- Make sure that private keys are stored mode 0600.

MMTPClient:
- Make 'bad authentication' a separate exception
- Check to make sure our certs aren't expired.

EventStats:
- Begin testing, debugging, refactoring.
- Fancier rule for rotation: don't rotate until we've accumulated data
  for a sufficiently long time, even if a long time has passed.  (That
  is, if we've been offline for 23 hours, don't trigger a daily
  rotation.)

MMTPServer:
- Refactor client connection cache
- Use PeerCertificateCache to remember which certificates we've
  already verified.


Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.42
retrieving revision 1.43
diff -u -d -r1.42 -r1.43
--- BuildMessage.py	27 Mar 2003 10:30:59 -0000	1.42
+++ BuildMessage.py	26 Apr 2003 14:39:58 -0000	1.43
@@ -227,10 +227,6 @@
     # message with 99.6% probability.  (Otherwise, we'd need to repeatedly
     # lioness-decrypt the payload in order to see whether the message was
     # a reply.)
-
-    # XXXX D'oh!  This enables an offline password guessing attack for
-    # XXXX anybody who sees multiple tags.  We need to make sure that userKey
-    # XXXX is stored on disk, and isn't a password.  This needs more thought.
     while 1:
         seed = _getRandomTag(secretRNG)
         if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00':
@@ -464,15 +460,23 @@
            paddingPRNG: A pseudo-random number generator to generate padding
     """
     assert len(path) == len(secrets)
-    if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
-        raise MixError("Too many nodes in path")
 
     routing, sizes, totalSize = _getRouting(path, exitType, exitInfo)
+    if totalSize > HEADER_LEN:
+        raise MixError("Path cannot fit in header")
 
     # headerKey[i]==the AES key object node i will use to decrypt the header
     headerKeys = [ Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE)
                        for secret in secrets ]
 
+##     # junkKeys[i]==the AES key object that node i will use to re-pad the
+##     # header.
+##     junkKeys = [ Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
+##                        for secret in secrets ]
+
+    # Length of padding needed for the header
+    paddingLen = HEADER_LEN - totalSize
+
     # Calculate junk.
     #   junkSeen[i]==the junk that node i will see, before it does any
     #                encryption.   Note that junkSeen[0]=="", because node 0
@@ -483,25 +487,28 @@
         #
         # Node i+1 sees the junk that node i saw, plus the junk that i appends,
         # all encrypted by i.
-        prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
-        # NewJunk is the junk that node i will append. (It's as long as
-        #   the subheaders that i removes.)
-        newJunk = Crypto.prng(prngKey,size*128)
+
+        prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE) 
+    
+        # newJunk is the junk that node i will append. (It's as long as
+        #   the data that i removes.)
+        newJunk = Crypto.prng(prngKey,size)
         lastJunk = junkSeen[-1]
         nextJunk = lastJunk + newJunk
-        # Node i encrypts starting with its first extended subheader.  By
-        #   the time it reaches the junk, it's traversed:
-        #          All of its extended subheaders    [(size-1)*128]
-        #          Non-junk parts of the header      [HEADER_LEN-len(nextJunk)]
-        #
-        # Simplifying, we find that the PRNG index for the junk is
-        #    HEADER_LEN-len(lastJunk)-128.
-        startIdx = HEADER_LEN-len(lastJunk)-128
+
+        # Before we encrypt the junk, we'll encrypt all the data, and
+        # all the initial padding, but not the RSA-encrypted part.
+        #    This is equal to - 256
+        #                     + sum(size[current]....size[last])
+        #                     + paddingLen
+        #    This simplifies to:
+        #startIdx = paddingLen - 256 + totalSize - len(lastJunk)
+        startIdx = HEADER_LEN - ENC_SUBHEADER_LEN - len(lastJunk)
         nextJunk = Crypto.ctr_crypt(nextJunk, headerKey, startIdx)
         junkSeen.append(nextJunk)
 
     # We start with the padding.
-    header = paddingPRNG.getBytes(HEADER_LEN - totalSize*128)
+    header = paddingPRNG.getBytes(paddingLen)
 
     # Now, we build the subheaders, iterating through the nodes backwards.
     for i in range(len(path)-1, -1, -1):
@@ -514,12 +521,32 @@
                             None, #placeholder for as-yet-uncalculated digest
                             rt, ri)
 
-        extHeaders = "".join(subhead.getExtraBlocks())
-        rest = Crypto.ctr_crypt(extHeaders+header, headerKeys[i])
-        subhead.digest = Crypto.sha1(rest+junkSeen[i])
+        # Do we need to include some of the remaining header in the
+        # RSA-encrypted portion?
+        underflowLength = subhead.getUnderflowLength()
+        if underflowLength > 0:
+            underflow = header[:underflowLength]
+            header = header[underflowLength:]
+        else:
+            underflow = ""
+
+        # Do we need to spill some of the routing info out from the
+        # RSA-encrypted portion?
+        #XXXX004 most of these asserts are silly.
+        assert not subhead.getOverflow() or not subhead.getUnderflowLength()
+        header = subhead.getOverflow() + header
+
+        header = Crypto.ctr_crypt(header, headerKeys[i])
+
+        assert len(header)+len(junkSeen[i])+ENC_SUBHEADER_LEN == HEADER_LEN
+        subhead.digest = Crypto.sha1(header+junkSeen[i])
         pubkey = path[i].getPacketKey()
-        esh = Crypto.pk_encrypt(subhead.pack(), pubkey)
-        header = esh + rest
+        rsaPart = subhead.pack() + underflow
+        assert len(rsaPart) + OAEP_OVERHEAD == ENC_SUBHEADER_LEN
+        assert pubkey.get_modulus_bytes() == ENC_SUBHEADER_LEN
+        esh = Crypto.pk_encrypt(rsaPart, pubkey)
+        assert len(esh) == ENC_SUBHEADER_LEN == 256
+        header = esh + header
 
     return header
 
@@ -636,12 +663,12 @@
                 node in path[1:] ]
     routing.append((exitType, exitInfo))
 
-    # sizes[i] is size, in blocks, of subheaders for i.
-    sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
+    # sizes[i] is number of bytes added to header for subheader i.
+    sizes = [ len(ri)+OAEP_OVERHEAD+MIN_SUBHEADER_LEN for _, ri in routing]
 
-    # totalSize is the total number of blocks.
+    # totalSize is the total number of bytes needed for header
     totalSize = reduce(operator.add, sizes)
-    if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
+    if totalSize > HEADER_LEN:
         raise MixError("Routing info won't fit in header")
 
     return routing, sizes, totalSize

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.70
retrieving revision 1.71
diff -u -d -r1.70 -r1.71
--- ClientMain.py	4 Apr 2003 20:28:55 -0000	1.70
+++ ClientMain.py	26 Apr 2003 14:39:58 -0000	1.71
@@ -25,7 +25,8 @@
 import mixminion.Crypto
 import mixminion.MMTPClient
 from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
-     MixFatalError, MixProtocolError, UIError, UsageError, ceilDiv, \
+     MixFatalError, MixProtocolError, MixProtocolBadAuth, \
+     UIError, UsageError, ceilDiv, \
      createPrivateDir, isPrintingAscii, \
      isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
      Lockfile, openUnique, previousMidnight, readPossiblyGzippedFile, \
@@ -483,7 +484,7 @@
 
     def getServerInfo(self, name, startAt=None, endAt=None, strict=0):
         """Return the most-recently-published ServerInfo for a given
-           'name' valid over a given time range.  If strict, and no
+           'name' valid over a given time range.  If not strict, and no
            such server is found, return None.
 
            name -- A ServerInfo object, a nickname, or a filename.
@@ -493,7 +494,6 @@
             startAt = time.time()
         if endAt is None:
             endAt = startAt + self.DEFAULT_REQUIRED_LIFETIME
-
             
         if isinstance(name, ServerInfo):
             # If it's a valid ServerInfo, we're done.
@@ -1478,6 +1478,23 @@
         """
         return SURBLog(self.surbLogFilename)
 
+    def pingServer(self, routingInfo):
+        """Given an IPV4Info, try to connect to a server and find out if
+           it's up.  Returns a boolean and a status message."""
+        timeout = self.config['Network'].get('ConnectionTimeout')
+        if timeout:
+            timeout = timeout[2]
+        else:
+            timeout = 60
+
+        try:
+            mixminion.MMTPClient.pingServer(routingInfo, timeout)
+            return 1, "Server seems to be running"
+        except MixProtocolBadAuth: 
+            return 0, "Server seems to be running, but its key is wrong!"
+        except MixProtocolError, e:
+            return 0, "Couldn't connect to server: %s" % e
+
     def sendMessages(self, msgList, routingInfo, noQueue=0, lazyQueue=0,
                      warnIfLost=1):
         """Given a list of packets and an IPV4Info object, sends the
@@ -2200,6 +2217,55 @@
     else:
         client.sendForwardMessage(address, payload, path1, path2,
                                   forceQueue, forceNoQueue)
+
+_PING_USAGE = """\
+Usage: mixminion ping [options] serverName
+Options
+  -h, --help:             Print this usage message and exit.
+  -v, --verbose           Display extra debugging messages.
+  -f FILE, --config=FILE  Use a configuration file other than ~/.mixminionrc
+  -D <yes|no>, --download-directory=<yes|no>
+                          Force the client to download/not to download a
+                            fresh directory.
+"""
+def runPing(cmd, args):
+    #DOCDOC comment me
+
+    if len(args) == 1 and args[0] in ('-h', '--help'):
+        print _PING_USAGE
+        sys.exit(0)
+
+    options, args = getopt.getopt(args, "hvf:D:",
+             ["help", "verbose", "config=", "download-directory=", ])
+
+    if len(args) != 1:
+        raise UsageError("mixminion ping requires a single server name")
+
+
+    print "==========================================================="
+    print "WARNING: Pinging a server is potentially dangerous, since"
+    print "      it might alert people that you plan to use the server"
+    print "      for your messages.  This command is for testing only,"
+    print "      and will go away before Mixminion 1.0.  By then, all"
+    print "      listed servers will be reliable anyway.  <wink>"
+    print "==========================================================="
+
+    parser = CLIArgumentParser(options, wantConfig=1,
+                               wantClientDirectory=1, wantClient=1,
+                               wantLog=1, wantDownload=1)
+
+    parser.init()
+
+    directory = parser.directory
+    client = parser.client
+
+    info = directory.getServerInfo(args[0],
+                                   startAt=time.time(), endAt = time.time(),
+                                   strict=1)
+
+    ok, status = client.pingServer(info.getRoutingInfo())
+    print ">>>", status
+    print info.getNickname(), (ok and "is up" or "is down")
 
 _IMPORT_SERVER_USAGE = """\
 Usage: %(cmd)s [options] <filename> ...

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.69
retrieving revision 1.70
diff -u -d -r1.69 -r1.70
--- Common.py	22 Apr 2003 01:45:22 -0000	1.69
+++ Common.py	26 Apr 2003 14:39:58 -0000	1.70
@@ -7,14 +7,14 @@
 
 __all__ = [ 'IntervalSet', 'Lockfile', 'LOG', 'LogStream', 'MixError',
             'MixFatalError', 'MixProtocolError', 'UIError', 'UsageError',
-            'ceilDiv', 'checkPrivateDir', 'createPrivateDir',
+            'ceilDiv', 'checkPrivateDir', 'checkPrivateFile',
+            'createPrivateDir',
             'encodeBase64', 'floorDiv',
             'formatBase64', 'formatDate', 'formatFnameTime', 'formatTime',
             'installSIGCHLDHandler', 'isSMTPMailbox', 'openUnique',
             'previousMidnight', 'readPossiblyGzippedFile', 'secureDelete',
             'stringContains', 'succeedingMidnight', 'waitForChildren' ]
 
-import base64
 import binascii
 import bisect
 import calendar
@@ -53,6 +53,10 @@
     """Exception class for server-rejected packets."""
     pass
 
+class MixProtocolBadAuth(MixProtocolError):
+    """Exception class for failed authentication to a server."""
+    pass
+
 class UIError(MixError):
     """Exception raised for an error that should be reported to the user,
        not dumped as a stack trace."""
@@ -152,6 +156,30 @@
         return "".join(pieces)
     
 #----------------------------------------------------------------------
+def checkPrivateFile(fn, fix=1):
+    """Checks whether f is a file owned by this uid, set to mode 0600 or
+       0700, and all its parents pass checkPrivateDir.  Raises MixFatalError
+       if the assumtions are not met; else return None.  If 'fix' is true,
+       repair permissions on the file rather than raising MixFatalError."""
+    parent, _ = os.path.split(fn)
+    if not checkPrivateDir(parent):
+        return None
+    st = os.stat(fn)
+    if not st:
+        raise MixFatalError("Nonexistant file %s" % fn)
+    if not os.path.isfile(fn):
+        raise MixFatalError("%s is not a regular file" % fn)
+    me = os.getuid()
+    if st[stat.ST_UID] != me:
+        raise MixFatalError("File %s must have owner %s" % (fn, me))
+    mode = st[stat.ST_MODE] & 0777
+    if mode not in (0700, 0600):
+        if not fix:
+            raise MixFatalError("Bad mode %o on file %s" % mode)
+        newmode = {0:0600,0100:0700}[(newmode & 0100)]
+        LOG.warn("Repairing permissions on file %s" % fn)
+        os.chmod(fn, newmode)
+
 def createPrivateDir(d, nocreate=0):
     """Create a directory, and all parent directories, checking permissions
        as we go along.  All superdirectories must be owned by root or us."""
@@ -168,7 +196,7 @@
 _WARNED_DIRECTORIES = {}
 
 def checkPrivateDir(d, recurse=1):
-    """Return true iff d is a directory owned by this uid, set to mode
+    """Check whether d is a directory owned by this uid, set to mode
        0700. All of d's parents must not be writable or owned by anybody but
        this uid and uid 0.  If any of these conditions are unmet, raise
        MixFatalErrror.  Otherwise, return None."""

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- Config.py	20 Feb 2003 16:57:39 -0000	1.40
+++ Config.py	26 Apr 2003 14:39:58 -0000	1.41
@@ -511,40 +511,22 @@
            If <assumeValid> is true, skip all unnecessary validation
            steps.  (Use this to load a file that's already been checked as
            valid.)"""
-        assert filename is None or string is None
+        assert (filename is None) != (string is None)
+        
         if not hasattr(self, '_callbacks'):
             self._callbacks = {}
 
         self.assumeValid = assumeValid
-        self.fname = filename
+
         if filename:
-            self.reload()
-        elif string:
-            self.__reload(None, string)
+            contents = mixminion.Common.readPossiblyGzippedFile(filename)
+            self.__load(contents)
         else:
-            self.clear()
-
-    def clear(self):
-        """Remove all sections from this _ConfigFile object."""
-        self._sections = {}
-        self._sectionEntries = {}
-        self._sectionNames = []
-
-    def reload(self):
-        """Reload this _ConfigFile object from disk.  If the object is no
-           longer present and correctly formatted, raise an error, but leave
-           the contents of this object unchanged."""
-        if not self.fname:
-            return
-
-        contents = mixminion.Common.readPossiblyGzippedFile(self.fname)
-        self.__reload(None, contents)
+            assert string is not None
+            self.__load(string)
 
-    def __reload(self, file, fileContents):
+    def __load(self, fileContents):
         """As in .reload(), but takes an open file object _or_ a string."""
-        if fileContents is None:
-            fileContents = file.read()
-            file.close()
 
         fileContents = _abnormal_line_ending_re.sub("\n", fileContents)
 
@@ -553,24 +535,24 @@
         else:
             sections = _readConfigFile(fileContents)
 
-        # These will become self.(_sections,_sectionEntries,_sectionNames)
-        # if we are successful.
-        self_sections = {}
-        self_sectionEntries = {}
-        self_sectionNames = []
+        sections = self.prevalidate(sections)
+
+        self._sections = {}
+        self._sectionEntries = {}
+        self._sectionNames = []
         sectionEntryLines = {}
 
         for secName, secEntries in sections:
-            self_sectionNames.append(secName)
+            self._sectionNames.append(secName)
 
-            if self_sections.has_key(secName):
+            if self._sections.has_key(secName):
                 raise ConfigError("Duplicate section [%s]" %secName)
 
             section = {}
             sectionEntries = []
             entryLines = []
-            self_sections[secName] = section
-            self_sectionEntries[secName] = sectionEntries
+            self._sections[secName] = section
+            self._sectionEntries[secName] = sectionEntries
             sectionEntryLines[secName] = entryLines
 
             secConfig = self._syntax.get(secName, None)
@@ -647,20 +629,15 @@
         for secName, secConfig in self._syntax.items():
             secRule = secConfig.get('__SECTION__', ('ALLOW',None,None))
             if (secRule[0] == 'REQUIRE'
-                and not self_sections.has_key(secName)):
+                and not self._sections.has_key(secName)):
                 raise ConfigError("Section [%s] not found." %secName)
-            elif not self_sections.has_key(secName):
-                self_sections[secName] = {}
-                self_sectionEntries[secName] = []
+            elif not self._sections.has_key(secName):
+                self._sections[secName] = {}
+                self._sectionEntries[secName] = []
 
         if not self.assumeValid:
             # Call our validation hook.
-            self.validate(self_sections, self_sectionEntries,
-                          sectionEntryLines, fileContents)
-
-        self._sections = self_sections
-        self._sectionEntries = self_sectionEntries
-        self._sectionNames = self_sectionNames
+            self.validate(sectionEntryLines, fileContents)
 
     def _addCallback(self, section, cb):
         """For use by subclasses.  Adds a callback for a section"""
@@ -668,8 +645,14 @@
             self._callbacks = {}
         self._callbacks[section] = cb
 
-    def validate(self, sections, sectionEntries, entryLines,
-                 fileContents):
+    def prevalidate(self, contents):
+        """Given a list of (SECTION-NAME, [(KEY, VAL, LINENO)]), makes
+           decision on whether to parse sections.  Subclasses should
+           override.  Returns a revised version of its input.
+        """
+        return contents
+
+    def validate(self, entryLines, fileContents):
         """Check additional semantic properties of a set of configuration
            data before overwriting old data.  Subclasses should override."""
         pass
@@ -723,10 +706,10 @@
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self, fname, string)
 
-    def validate(self, sections, entries, lines, contents):
-        _validateHostSection(sections.get('Host', {}))
+    def validate(self, lines, contents):
+        _validateHostSection(self['Host'])
 
-        security = sections.get('Security', {})
+        security = self['Security']
         p = security.get('PathLength', 8)
         if not 0 < p <= 16:
             raise ConfigError("Path length must be between 1 and 16")
@@ -734,7 +717,7 @@
             LOG.warn("Your default path length is frighteningly low."
                           "  I'll trust that you know what you're doing.")
 
-        t = sections['Network'].get('ConnectionTimeout')
+        t = self['Network'].get('ConnectionTimeout')
         if t:
             if t[2] < 5:
                 LOG.warn("Very short connection timeout")

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.41
retrieving revision 1.42
diff -u -d -r1.41 -r1.42
--- Crypto.py	16 Feb 2003 04:50:55 -0000	1.41
+++ Crypto.py	26 Apr 2003 14:39:58 -0000	1.42
@@ -272,7 +272,8 @@
 def pk_PEM_save(rsa, filename, password=None):
     """Save a PEM-encoded private key to a file.  If <password> is provided,
        encrypt the key using the password."""
-    f = open(filename, 'w')
+    fd = os.open(filename, os.O_WRONLY|os.O_CREAT, 0600)
+    f = os.fdopen(fd, 'wb')
     if password:
         rsa.PEM_write_key(f, 0, password)
     else:

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.27
retrieving revision 1.28
diff -u -d -r1.27 -r1.28
--- MMTPClient.py	22 Apr 2003 01:45:22 -0000	1.27
+++ MMTPClient.py	26 Apr 2003 14:39:58 -0000	1.28
@@ -22,8 +22,8 @@
 import socket
 import mixminion._minionlib as _ml
 from mixminion.Crypto import sha1, getCommonPRNG
-from mixminion.Common import MixProtocolError, MixProtocolReject, LOG, \
-     MixError, formatBase64
+from mixminion.Common import MixProtocolError, MixProtocolReject, \
+     MixProtocolBadAuth, LOG, MixError, formatBase64
 
 class TimeoutError(MixProtocolError):
     """Exception raised for protocol timeout."""
@@ -256,6 +256,13 @@
     finally:
         con.shutdown()
 
+def pingServer(routing, connectTimeout=5):
+    """Try to connect to a server and send a junk packet.
+    
+       May raise MixProtocolBadAuth, or other MixProtocolError if server
+       isn't up."""
+    sendMessages(routing, ["JUNK"], connectTimeout=connectTimeout)
+    
 class PeerCertificateCache:
     #XXXX004 use this properly; flush it to disk.
     "DOCDOC"
@@ -267,9 +274,14 @@
         if targetKeyID is None:
             return
 
+        try:
+            tls.check_cert_alive()
+        except _ml.TLSError, e:
+            raise MixProtocolBadAuth("Invalid certificate: %s", str(e))
+
         peer_pk = tls.get_peer_cert_pk()
         hashed_peer_pk = sha1(peer_pk.encode_key(public=1))
-        #XXXX005 Remove this option
+        #XXXX004 Remove this option
         if targetKeyID == hashed_peer_pk:
             LOG.warn("Non-rotatable keyid from server at %s", address)
             return # raise MixProtocolError
@@ -277,7 +289,8 @@
             if self.cache[hashed_peer_pk] == targetKeyID:
                 return # All is well.
             else:
-                raise MixProtocolError("Mismatch between expected and actual key id")
+                raise MixProtocolBadAuth(
+                    "Mismatch between expected and actual key id")
         except KeyError:
             # We haven't found an identity for this pk yet.
             pass
@@ -285,11 +298,11 @@
         try:
             identity = tls.verify_cert_and_get_identity_pk()
         except _ml.TLSError, e:
-            raise MixProtocolError("Invalid KeyID from server at %s: %s"
+            raise MixProtocolBadAuth("Invalid KeyID from server at %s: %s"
                                    %(address, e))
 
         hashed_identity = sha1(identity.encode_key(public=1))
         self.cache[hashed_peer_pk] = hashed_identity
         if hashed_identity != targetKeyID:
-            raise MixProtocolError("Invalid KeyID for server at %s" % address)
+            raise MixProtocolBadAuth("Invalid KeyID for server at %s" %address)
 

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.39
retrieving revision 1.40
diff -u -d -r1.39 -r1.40
--- Main.py	4 Apr 2003 20:59:53 -0000	1.39
+++ Main.py	26 Apr 2003 14:39:58 -0000	1.40
@@ -131,7 +131,8 @@
     "flush" :          ( 'mixminion.ClientMain', 'flushQueue' ),
     "inspect-queue" :   ( 'mixminion.ClientMain', 'listQueue' ),
     # XXXX Obsolete; use "inspect-queue"; remove in 0.0.5
-    "inspect-pool" :   ( 'mixminion.ClientMain', 'listQueue' ),    
+    "inspect-pool" :   ( 'mixminion.ClientMain', 'listQueue' ),
+    "ping" :           ( 'mixminion.ClientMain', 'runPing' ),    
     # XXXX Obsolete; use "server-start"; remove in 0.0.5
     "server" :         ( 'mixminion.server.ServerMain', 'runServer' ),
     "server-start" :   ( 'mixminion.server.ServerMain', 'runServer' ),
@@ -158,6 +159,7 @@
   "       decode         [Decode or decrypt a received message]\n"+
   "       generate-surb  [Generate a single-use reply block]\n"+
   "       inspect-surbs  [Describe a single-use reply block]\n"+
+  "       ping           [Quick and dirty check whether a server is running]\n"
   "                               (For Servers)\n"+
   "       server-start   [Begin running a Mixminion server]\n"+
   "       server-stop    [Halt a running Mixminion server]\n"+
@@ -201,6 +203,14 @@
     if len(args) == 1 or not _COMMANDS.has_key(args[1]):
         printUsage()
         sys.exit(1)
+
+    if args[1] not in ('unittests', 'benchmarks'):
+        print "==========================================================="
+        print "                     TURN  BACK  NOW  !!!"
+        print "This version of Mixminion (0.0.4alpha2) is compatible with no"
+        print "other version.  Go check out the maintenance branch if you"
+        print "want to use this software to run a server or send messages."
+        print "==========================================================="
 
     # Read the 'common' module to get the UIError class.  To simplify
     # command implementation code, we catch all UIError exceptions here.

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.38
retrieving revision 1.39
diff -u -d -r1.38 -r1.39
--- Packet.py	20 Feb 2003 16:57:39 -0000	1.38
+++ Packet.py	26 Apr 2003 14:39:58 -0000	1.39
@@ -12,13 +12,14 @@
 __all__ = [ 'compressData', 'CompressedDataTooLong', 'DROP_TYPE',
             'ENC_FWD_OVERHEAD', 'ENC_SUBHEADER_LEN',
             'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_TYPE', 'FragmentPayload',
-            'HEADER_LEN', 'Header', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
-            'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE', 'Message',
+            'HEADER_LEN', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
+            'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE',
+            'MIN_SUBHEADER_LEN', 'Message',
             'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
             'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
             'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_TYPE', 'SingletonPayload',
             'Subheader', 'TAG_LEN', 'TextEncodedMessage',
-            'getTotalBlocksForRoutingInfoLen', 'parseHeader', 'parseIPV4Info',
+            'parseHeader', 'parseIPV4Info',
             'parseMBOXInfo', 'parseMessage', 'parsePayload', 'parseReplyBlock',
             'parseReplyBlocks', 'parseSMTPInfo', 'parseSubheader',
             'parseTextEncodedMessage', 'parseTextReplyBlocks', 'uncompressData'
@@ -38,7 +39,7 @@
     import mixminion._zlibutil as zlibutil
 
 # Major and minor number for the understood packet format.
-MAJOR_NO, MINOR_NO = 0,2
+MAJOR_NO, MINOR_NO = 0,3
 
 # Length of a Mixminion message
 MESSAGE_LEN = 1 << 15
@@ -51,12 +52,13 @@
 OAEP_OVERHEAD = 42
 
 # Length of a subheader, once RSA-encoded.
-ENC_SUBHEADER_LEN = 128
+ENC_SUBHEADER_LEN = 256
 # Smallest possible size for a subheader
 MIN_SUBHEADER_LEN = 42
 # Most information we can fit into a subheader before padding
 MAX_SUBHEADER_LEN = ENC_SUBHEADER_LEN - OAEP_OVERHEAD
-# Longest routing info that will fit in the main subheader
+# Longest routing info that will fit into the RSA-encrypted portion of
+# the subheader.
 MAX_ROUTING_INFO_LEN = MAX_SUBHEADER_LEN - MIN_SUBHEADER_LEN
 
 # Length of a digest
@@ -66,9 +68,6 @@
 # Length of end-to-end message tag
 TAG_LEN = 20
 
-# Most info that fits in a single extened subheader
-ROUTING_INFO_PER_EXTENDED_SUBHEADER = ENC_SUBHEADER_LEN
-
 #----------------------------------------------------------------------
 # Values for the 'Routing type' subheader field
 # Mixminion types
@@ -118,35 +117,7 @@
     if len(s) != HEADER_LEN:
         raise ParseError("Bad header length")
 
-    return Header(s)
-
-class Header:
-    """Represents a 2K Mixminion header, containing up to 16 subheaders."""
-    def __init__(self, contents):
-        """Initialize a new header from its contents"""
-        self.contents = contents
-
-    def __getitem__(self, i):
-        """header[i] -> str
-
-           Returns the i'th encoded subheader of this header, for i in 0..15"""
-        if i < 0: i = 16+i
-        return self.contents[i*ENC_SUBHEADER_LEN:
-                             (i+1)*ENC_SUBHEADER_LEN]
-
-    def __getslice__(self, i, j):
-        """header[i:j] -> str
-
-           Returns a slice of the i-j'th subheaders of this header."""
-        if j > 16: j = 16
-        if i < 0: i += 16
-        if j < 0: j += 16
-        return self.contents[i*ENC_SUBHEADER_LEN:
-                             j*ENC_SUBHEADER_LEN]
-
-    def __len__(self):
-        """Return the number of subheaders in this header (always 16)"""
-        return 16
+    return s
 
 # A subheader begins with: a major byte, a minor byte, SECRET_LEN secret
 # bytes, DIGEST_LEN digest bytes, a routing_len short, and a routing_type
@@ -166,20 +137,13 @@
     except struct.error:
         raise ParseError("Misformatted subheader")
     ri = s[MIN_SUBHEADER_LEN:]
+    underflow = ""
     if rlen < len(ri):
-        ri = ri[:rlen]
+        ri, underflow = ri[:rlen], ri[rlen:]
     if rt >= MIN_EXIT_TYPE and rlen < 20:
         raise ParseError("Subheader missing tag")
-    return Subheader(major,minor,secret,digest,rt,ri,rlen)
-
-def getTotalBlocksForRoutingInfoLen(bytes):
-    """Return the number of subheaders that will be needed for a hop
-       whose routinginfo is (bytes) long."""
-    if bytes <= MAX_ROUTING_INFO_LEN:
-        return 1
-    else:
-        extraBytes = bytes - MAX_ROUTING_INFO_LEN
-        return 2 + floorDiv(extraBytes,ROUTING_INFO_PER_EXTENDED_SUBHEADER)
+    #XXXX004 test underflow
+    return Subheader(major,minor,secret,digest,rt,ri,rlen,underflow)
 
 class Subheader:
     """Represents a decoded Mixminion subheader
@@ -190,10 +154,11 @@
        A Subheader can exist in a half-initialized state where routing
        info has been read from the first header, but not from the
        extened headers.  If this is so, routinglen will be > len(routinginfo).
+       DOCDOC underflow
        """
 
     def __init__(self, major, minor, secret, digest, routingtype,
-                 routinginfo, routinglen=None):
+                 routinginfo, routinglen=None, underflow=""):
         """Initialize a new subheader"""
         self.major = major
         self.minor = minor
@@ -205,6 +170,7 @@
             self.routinglen = routinglen
         self.routingtype = routingtype
         self.routinginfo = routinginfo
+        self.underflow = underflow
 
     def __repr__(self):
         return ("Subheader(major=%(major)r, minor=%(minor)r, "+
@@ -231,33 +197,33 @@
         self.routinginfo = info
         self.routinglen = len(info)
 
-    def isExtended(self):
-        """Return true iff the routinginfo is too long to fit in a single
-           subheader."""
-        return self.routinglen > MAX_ROUTING_INFO_LEN
-
-    def getNExtraBlocks(self):
-        """Return the number of extra blocks that will be needed to fit
-           the routinginfo."""
-        return getTotalBlocksForRoutingInfoLen(self.routinglen)-1
-
-    def appendExtraBlocks(self, data):
-        """Given a string containing additional (decoded) blocks of
-           routing info, add them to the routinginfo of this
+    def appendOverflow(self, data):
+        """Given a string containing additional 
+           routing info, add it to the routinginfo of this
            object.
+           DOCDOC
         """
-        nBlocks = self.getNExtraBlocks()
-        assert len(data) == nBlocks * ENC_SUBHEADER_LEN
-        raw = [self.routinginfo]
-        for i in range(nBlocks):
-            block = data[i*ENC_SUBHEADER_LEN:(i+1)*ENC_SUBHEADER_LEN]
-            raw.append(block)
-        self.routinginfo = ("".join(raw))[:self.routinglen]
+        #XXXX004 test
+        self.routinginfo += data
+        assert len(self.routinginfo) <= self.routinglen
+
+    def getUnderflowLength(self):
+        return max(0, MAX_ROUTING_INFO_LEN - self.routinglen)
+
+    def getOverflowLength(self):
+        """DOCDOC"""
+        #XXXX004 test
+        return max(0, self.routinglen - MAX_ROUTING_INFO_LEN)
+
+    def getOverflow(self):
+        """DOCDOC"""
+        #XXXX004 test
+        return self.routinginfo[MAX_ROUTING_INFO_LEN:]
 
     def pack(self):
         """Return the (unencrypted) string representation of this Subhead.
 
-           Does not include extra blocks"""
+           Does not include overflow or underflow"""
         assert self.routinglen == len(self.routinginfo)
         assert len(self.digest) == DIGEST_LEN
         assert len(self.secret) == SECRET_LEN
@@ -267,22 +233,6 @@
                            self.major,self.minor,self.secret,self.digest,
                            self.routinglen, self.routingtype)+info
 
-    def getExtraBlocks(self):
-        """Return a list of (unencrypted) blocks of extra routing info."""
-        if not self.isExtended():
-            return []
-        else:
-            info = self.routinginfo[MAX_ROUTING_INFO_LEN:]
-            result = []
-            for i in range(self.getNExtraBlocks()):
-                content = info[i*ROUTING_INFO_PER_EXTENDED_SUBHEADER:
-                               (i+1)*ROUTING_INFO_PER_EXTENDED_SUBHEADER]
-                missing = ROUTING_INFO_PER_EXTENDED_SUBHEADER-len(content)
-                if missing > 0:
-                    content += '\000'*missing
-                result.append(content)
-            return result
-
 #----------------------------------------------------------------------
 # UNENCRYPTED PAYLOADS
 
@@ -535,7 +485,7 @@
     """Converts routing info for an IPV4 address into an IPV4Info object,
        suitable for use by FWD or SWAP_FWD modules."""
     if len(s) != 4+2+DIGEST_LEN:
-        raise ParseError("IPV4 information with wrong length")
+        raise ParseError("IPV4 information with wrong length (%d)" % len(s))
     try:
         ip, port, keyinfo = struct.unpack(IPV4_PAT, s)
     except struct.error:
@@ -579,7 +529,7 @@
         r = cmp(self.ip, other.ip)
         if r: return r
         r = cmp(self.port, other.port)
-        if r: return n
+        if r: return r
         return cmp(self.keyinfo, other.keyinfo)
 
 def parseSMTPInfo(s):

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- ServerInfo.py	18 Apr 2003 17:41:38 -0000	1.40
+++ ServerInfo.py	26 Apr 2003 14:39:58 -0000	1.41
@@ -26,12 +26,16 @@
 MAX_CONTACT = 256
 # Longest allowed Comments field
 MAX_COMMENTS = 1024
+# Longest allowed Contact-Fingerprint field
+MAX_FINGERPRINT = 128
 # Shortest permissible identity key
 MIN_IDENTITY_BYTES = 2048 >> 3
 # Longest permissible identity key
 MAX_IDENTITY_BYTES = 4096 >> 3
 # Length of packet key
-PACKET_KEY_BYTES = 1024 >> 3
+PACKET_KEY_BYTES = 2048 >> 3
+# Length of MMTP key
+MMTP_KEY_BYTES = 1024 >> 3
 
 # tmp alias to make this easier to spell.
 C = mixminion.Config
@@ -47,7 +51,6 @@
     _syntax = {
         "Server" : { "__SECTION__": ("REQUIRE", None, None),
                      "Descriptor-Version": ("REQUIRE", None, None),
-                     "IP": ("REQUIRE", C._parseIP, None),
                      "Nickname": ("REQUIRE", C._parseNickname, None),
                      "Identity": ("REQUIRE", C._parsePublicKey, None),
                      "Digest": ("REQUIRE", C._parseBase64, None),
@@ -58,7 +61,7 @@
                      "Contact": ("ALLOW", None, None),
                      "Comments": ("ALLOW", None, None),
                      "Packet-Key": ("REQUIRE", C._parsePublicKey, None),
-                     "Contact-Fingerptint": ("ALLOW", None, None),
+                     "Contact-Fingerprint": ("ALLOW", None, None),
                      # XXXX010 change these next few to "REQUIRE".
                      "Packet-Formats": ("ALLOW", None, None),
                      "Software": ("ALLOW", None, None),
@@ -66,6 +69,7 @@
                      },
         "Incoming/MMTP" : {
                      "Version": ("REQUIRE", None, None),
+                     "IP": ("REQUIRE", C._parseIP, None),
                      "Port": ("REQUIRE", C._parseInt, None),
                      "Key-Digest": ("REQUIRE", C._parseBase64, None),
                      "Protocols": ("REQUIRE", None, None),
@@ -105,16 +109,28 @@
                        self['Server']['Nickname'],
                        fname or "<string>")
 
-    def validate(self, sections, entries, lines, contents):
+    def prevalidate(self, contents):
+        for name, ents in contents:
+            if name == 'Server':
+                for k,v,_ in ents:
+                    if k == 'Descriptor-Version' and v.strip() != '0.2':
+                        raise ConfigError("Unrecognized descriptor version %s"
+                                          % v.strip())
+            #XXXX Remove sections with unrecognized versions.
+
+        return contents
+
+
+    def validate(self, lines, contents):
         ####
         # Check 'Server' section.
-        server = sections['Server']
-        if server['Descriptor-Version'] != '0.1':
+        server = self['Server']
+        if server['Descriptor-Version'] != '0.2':
             raise ConfigError("Unrecognized descriptor version %r",
                               server['Descriptor-Version'])
 
         ####
-        # Check the igest of file
+        # Check the digest of file
         digest = getServerInfoDigest(contents)
         if digest != server['Digest']:
             raise ConfigError("Invalid digest")
@@ -138,11 +154,14 @@
             raise ConfigError("Contact too long")
         if server['Comments'] and len(server['Comments']) > MAX_COMMENTS:
             raise ConfigError("Comments too long")
+        if server['Contact-Fingerprint'] and \
+               len(server['Contact-Fingerprint']) > MAX_FINGERPRINT:
+            raise ConfigError("Contact-Fingerprint too long")
+
         packetKeyBytes = server['Packet-Key'].get_modulus_bytes()
         if packetKeyBytes != PACKET_KEY_BYTES:
             raise ConfigError("Invalid length on packet key")
 
-
         ####
         # Check signature
         try:
@@ -154,7 +173,7 @@
             raise ConfigError("Signed digest is incorrect")
 
         ## Incoming/MMTP section
-        inMMTP = sections['Incoming/MMTP']
+        inMMTP = self['Incoming/MMTP']
         if inMMTP:
             if inMMTP['Version'] != '0.1':
                 raise ConfigError("Unrecognized MMTP descriptor version %s"%
@@ -164,7 +183,7 @@
                                   formatBase64(inMMTP['Key-Digest']))
 
         ## Outgoing/MMTP section
-        outMMTP = sections['Outgoing/MMTP']
+        outMMTP = self['Outgoing/MMTP']
         if outMMTP:
             if outMMTP['Version'] != '0.1':
                 raise ConfigError("Unrecognized MMTP descriptor version %s"%
@@ -186,7 +205,7 @@
 
     def getAddr(self):
         """Returns this server's IP address"""
-        return self['Server']['IP']
+        return self['Incoming/MMTP']['IP']
 
     def getPort(self):
         """Returns this server's IP port"""
@@ -369,8 +388,17 @@
         self.expectedDigest = expectedDigest
         mixminion.Config._ConfigFile.__init__(self, string=contents)
 
-    def validate(self, sections, entries, lines, contents):
-        direc = sections['Directory']
+    def prevalidate(self, contents):
+        for name, ents in contents:
+            if name == 'Directory':
+                for k,v,_ in ents:
+                    if k == 'Version' and v.strip() != '0.1':
+                        raise ConfigError("Unrecognized directory version")
+
+        return contents
+
+    def validate(self, lines, contents):
+        direc = self['Directory']
         if direc['Version'] != "0.1":
             raise ConfigError("Unrecognized directory version")
         if direc['Published'] > time.time() + 600:
@@ -378,7 +406,7 @@
         if direc['Valid-Until'] <= direc['Valid-After']:
             raise ConfigError("Directory is never valid")
 
-        sig = sections['Signature']
+        sig = self['Signature']
         identityKey = sig['DirectoryIdentity']
         identityBytes = identityKey.get_modulus_bytes()
         if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):

Index: __init__.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/__init__.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- __init__.py	5 Mar 2003 21:34:04 -0000	1.30
+++ __init__.py	26 Apr 2003 14:39:59 -0000	1.31
@@ -7,7 +7,7 @@
    """
 
 # This version string is generated from setup.py; don't edit it.
-__version__ = "0.0.4alpha"
+__version__ = "0.0.4alpha2"
 # This 5-tuple encodes the version number for comparison.  Don't edit it.
 # The first 3 numbers are the version number; the 4th is:
 #          0 for alpha
@@ -18,7 +18,7 @@
 # The 4th or 5th number may be a string.  If so, it is not meant to
 #   succeed or preceed any other sub-version with the same a.b.c version
 #   number.
-version_info = (0, 0, 4, 0, -1)
+version_info = (0, 0, 4, 0, 2)
 __all__ = [ 'server', 'directory' ]
 
 def version_tuple_to_string(t):

Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.32
retrieving revision 1.33
diff -u -d -r1.32 -r1.33
--- benchmark.py	10 Apr 2003 03:03:16 -0000	1.32
+++ benchmark.py	26 Apr 2003 14:39:59 -0000	1.33
@@ -406,7 +406,7 @@
 
 def buildMessageTiming():
     print "#================= BUILD MESSAGE ====================="
-    pk = pk_generate()
+    pk = pk_generate(2048)
 
     for payload in "Hello!!!"*128, "Hello!!!"*(128*28):
         print "Compress %sK" % (len(payload)/1024), \
@@ -470,7 +470,7 @@
 def serverProcessTiming():
     print "#================= SERVER PROCESS ====================="
 
-    pk = pk_generate()
+    pk = pk_generate(2048)
     server = FakeServerInfo("127.0.0.1", 1, pk, "X"*20)
     sp = PacketHandler(pk, DummyLog())
 
@@ -550,7 +550,7 @@
     print "              short xor: %3.1f%%" % (100*2*strxor_20b/lioness_e)
 
     ##### SERVER PROCESS
-    pk = pk_generate(1024)
+    pk = pk_generate(2048)
 
     # Typical (no swap) server process is:
     #  pk_decrypt (128b)
@@ -825,8 +825,6 @@
         sock.close()
 
 #----------------------------------------------------------------------
-
-
 def timeAll(name, args):
     cryptoTiming()
     buildMessageTiming()
@@ -838,6 +836,3 @@
     timeEfficiency()
     #import profile
     #profile.run("import mixminion.benchmark; mixminion.benchmark.directoryTiming()")
-
-def timeAll(name,args):
-    testLeaks6_2()

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.99
retrieving revision 1.100
diff -u -d -r1.99 -r1.100
--- test.py	18 Apr 2003 17:41:38 -0000	1.99
+++ test.py	26 Apr 2003 14:39:59 -0000	1.100
@@ -992,21 +992,23 @@
                    "ABCDEFGHIJABCDEFGHIJ\000\005\000\001Hello"
         # test packing
         self.assertEquals(s.pack(), expected)
-        self.failUnless(not s.isExtended())
-        self.assertEquals(s.getNExtraBlocks(), 0)
-        self.assertEquals(s.getExtraBlocks(), [])
+        self.assertEquals(s.getOverflow(), "")
+        self.assertEquals(s.getUnderflowLength(), 256 - 42 - 42 - 5)
+        self.assertEquals(s.getOverflowLength(), 0)
 
         # test unpacking,
-        s = parseSubheader(s.pack())
-        self.assertEquals(s.major, 3)
-        self.assertEquals(s.minor, 0)
-        self.assertEquals(s.secret, "abcde"*3+"f")
-        self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
-        self.assertEquals(s.routingtype, 1)
-        self.assertEquals(s.routinglen, 5)
-        self.assertEquals(s.routinginfo, "Hello")
-        self.failUnless(not s.isExtended())
-        self.assertEquals(s.pack(), expected)
+        for more in "", "There is more":
+            s = parseSubheader(s.pack()+more)
+            self.assertEquals(s.major, 3)
+            self.assertEquals(s.minor, 0)
+            self.assertEquals(s.secret, "abcde"*3+"f")
+            self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
+            self.assertEquals(s.routingtype, 1)
+            self.assertEquals(s.routinglen, 5)
+            self.assertEquals(s.routinginfo, "Hello")
+            self.assertEquals(s.pack(), expected)
+            self.assertEquals(s.underflow, more)
+            self.assertEquals(s.getUnderflowLength(), 256-42-42-s.routinglen)
 
         ts_eliot = ("Who is the third who walks always beside you? / "+
                     "When I count, there are only you and I together / "+
@@ -1022,17 +1024,15 @@
         # test extended subeaders
         expected = ("\003\011abcdeabcdeabcdefABCDEFGHIJABCDEFGHIJ"+
                     "\000\272\001\054Who is the third who walks always"+
-                    " beside you")
+                    " beside you? / When I count, there are only you and I"+
+                    " together / But when I look ahead up the white road /"+
+                    " There is always another one walk")
         self.assertEquals(len(expected), mixminion.Packet.MAX_SUBHEADER_LEN)
         self.assertEquals(s.pack(), expected)
 
-        extra = s.getExtraBlocks()
-        self.assertEquals(len(extra), 2)
-        self.assertEquals(extra[0],
-                 ("? / When I count, there are only you "+
-                  "and I together / But when I look ahead up the white "+
-                  "road / There is always another one walk"))
-        self.assertEquals(extra[1], "ing beside you"+(114*'\000'))
+        overflow = s.getOverflow()
+        self.assertEquals(len(overflow), 14)
+        self.assertEquals(overflow, "ing beside you")
 
         # test parsing extended subheaders
         s = parseSubheader(expected)
@@ -1042,36 +1042,20 @@
         self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
         self.assertEquals(s.routingtype, 300)
         self.assertEquals(s.routinglen, 186)
-        self.failUnless(s.isExtended())
-        self.assertEquals(s.getNExtraBlocks(), 2)
+        self.assertEquals(s.getOverflowLength(), 14)
 
-        s.appendExtraBlocks("".join(extra))
+        s.appendOverflow("ing beside you")
         self.assertEquals(s.routinginfo, ts_eliot)
         self.assertEquals(s.getExitAddress(), ts_eliot[20:])
         self.assertEquals(s.getTag(), ts_eliot[:20])
         self.assertEquals(s.pack(), expected)
-        self.assertEquals(s.getExtraBlocks(), extra)
 
         # Underlong subheaders must fail
         self.failUnlessRaises(ParseError,
                               parseSubheader, "a"*(41))
         # overlong subheaders must fail
         self.failUnlessRaises(ParseError,
-                              parseSubheader, "a"*(99))
-
-    def test_headers(self):
-        # Make sure we extract the subheaders from a header correctly.
-        # (Generate a nice random string to make sure we're slicing right.)
-        header = Crypto.prng("onefish, twofish", 2048)
-        h = parseHeader(header)
-        self.failUnless(h[0] == header[:128])
-        self.failUnless(h[4] == header[128*4:128*5])
-        self.failUnless(h[:1] == h[0])
-        self.failUnless(h[1:] == header[128:])
-        self.failUnless(h[1:4] == header[128:128*4])
-        self.failUnless(h[15] == header[-128:])
-        self.failUnless(h[15] == h[-1])
-        self.failUnless(h[14:] == h[-2:])
+                              parseSubheader, "a"*(256))
 
     def test_message(self):
         # Make sure we can pull the headers and payload of a message apart
@@ -1370,8 +1354,9 @@
     """Represents a Mixminion server, and the information needed to send
        messages to it."""
     def __init__(self, addr, port, key, keyid):
+        assert key.get_modulus_bytes() == 256
         self.addr = addr
-        self.port = port
+        self.port = port        
         self.key = key
         self.keyid = keyid
 
@@ -1388,9 +1373,9 @@
 
 class BuildMessageTests(unittest.TestCase):
     def setUp(self):
-        self.pk1 = getRSAKey(0,1024)
-        self.pk2 = getRSAKey(1,1024)
-        self.pk3 = getRSAKey(2,1024)
+        self.pk1 = getRSAKey(0,2048)
+        self.pk2 = getRSAKey(1,2048)
+        self.pk3 = getRSAKey(2,2048)
         self.pk512 = getRSAKey(0,512)
         self.server1 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         self.server2 = FakeServerInfo("127.0.0.2", 3, self.pk2, "Z"*20)
@@ -1515,7 +1500,7 @@
         # Go through the hops one by one, simulating the decoding process.
         for pk, secret, rt, ri in zip(pks, secrets,rtypes,rinfo):
             # Decrypt the first subheader.
-            subh = mixminion.Packet.parseSubheader(pk_decrypt(head[:128], pk))
+            subh = mixminion.Packet.parseSubheader(pk_decrypt(head[:256], pk))
             # If we're expecting a given secret in this subheader, check it.
             if secret:
                 self.assertEquals(subh.secret, secret)
@@ -1525,7 +1510,7 @@
             # Check the version, the digest, and the routing type.
             self.assertEquals(subh.major, mixminion.Packet.MAJOR_NO)
             self.assertEquals(subh.minor, mixminion.Packet.MINOR_NO)
-            self.assertEquals(subh.digest, sha1(head[128:]))
+            self.assertEquals(subh.digest, sha1(head[256:]))
             self.assertEquals(subh.routingtype, rt)
 
             # Key to decrypt the rest of the header
@@ -1543,20 +1528,19 @@
 
             # Check the routinginfo.  This is a little different for regular
             # and extended subheaders...
-            if not subh.isExtended():
+            if not subh.getOverflowLength():
                 if ri:
                     self.assertEquals(subh.routinginfo[ext:], ri)
                     self.assertEquals(subh.routinglen, len(ri)+ext)
                 else:
                     retinfo.append(subh.routinginfo)
-                size = 128
-                n = 0
+                size = 256
             else:
-                self.assert_(len(ri)+ext>mixminion.Packet.MAX_ROUTING_INFO_LEN)
-                n = subh.getNExtraBlocks()
-                size = (1+n)*128
-                more = ctr_crypt(head[128:128+128*n], key)
-                subh.appendExtraBlocks(more)
+                more = ctr_crypt(head[256:256+subh.getOverflowLength()], key)
+                self.assert_(len(ri+more)>
+                             mixminion.Packet.MAX_ROUTING_INFO_LEN)
+                subh.appendOverflow(more)
+                size = 256 + subh.getOverflowLength()
                 if ri:
                     self.assertEquals(subh.routinginfo[ext:], ri)
                     self.assertEquals(subh.routinglen, len(ri)+ext)
@@ -1565,15 +1549,18 @@
 
             # Decrypt and pad the rest of the header.
             prngkey = ks.get(RANDOM_JUNK_MODE)
-            head = ctr_crypt(head[size:]+prng(prngkey,size), key, 128*n)
+            head = ctr_crypt(head[size:]+
+                       prng(prngkey,size-len(subh.underflow)), key, size-256)
+            head = subh.underflow + head
+            self.assertEquals(len(head), 2048)
 
         return retsecrets, retinfo, tag
 
-    def test_extended_routinginfo(self):
+    def test_header_overflow(self):
         bhead = BuildMessage._buildHeader
 
         secrets = ["9"*16]
-        longStr = "Foo"*50
+        longStr = "Foo"*100
         head = bhead([self.server1], secrets, 99, longStr, AESCounterPRNG())
         pks = (self.pk1,)
         rtypes = (99,)
@@ -1584,7 +1571,7 @@
         # Now try a header with extended **intermediate** routing info.
         # Since this never happens in the wild, we fake it.
         tag = "dref"*5
-        longStr2 = longStr * 2
+        longStr2 = "BAR"*100
 
         def getLongRoutingInfo(longStr2=longStr2,tag=tag):
             return MBOXInfo(tag+longStr2)
@@ -1979,7 +1966,7 @@
         # Encoded forward message
         efwd = (comp+"RWE/HGW"*4096)[:28*1024-22-38]
         efwd = '\x00\x6D'+sha1(efwd)+efwd
-        rsa1 = self.pk1
+        rsa1 = getRSAKey(0,1024)
         key1 = Keyset("RWE "*4).getLionessKeys("END-TO-END ENCRYPT")
         efwd_rsa = pk_encrypt(("RWE "*4)+efwd[:70], rsa1)
         efwd_lioness = lioness_encrypt(efwd[70:], key1)
@@ -2032,7 +2019,7 @@
         if 1:
             for p in (passwd, None):
                 self.assertEquals(payload,
-                        decodePayload(efwd_p, efwd_t, self.pk1, p))
+                        decodePayload(efwd_p, efwd_t, rsa1, p))
                 self.assertEquals(None,
                         decodePayload(efwd_p, efwd_t, None, p))
                 self.assertEquals(None,
@@ -2080,11 +2067,11 @@
         efwd_pbad = efwd_p[:-1] + chr(ord(efwd_p[-1])^0xaa)
         self.failUnlessRaises(MixError,
                               BuildMessage._decodeEncryptedForwardPayload,
-                              efwd_pbad, efwd_t, self.pk1)
+                              efwd_pbad, efwd_t, rsa1)
 
         for p in (passwd, None):
             self.failUnlessRaises(MixError, decodePayload,
-                                  efwd_pbad, efwd_t, self.pk1, p)
+                                  efwd_pbad, efwd_t, rsa1, p)
             self.assertEquals(None,
                       decodePayload(efwd_pbad, efwd_t, self.pk2, p))
 
@@ -2101,9 +2088,9 @@
 
 class PacketHandlerTests(unittest.TestCase):
     def setUp(self):
-        self.pk1 = getRSAKey(0,1024)
-        self.pk2 = getRSAKey(1,1024)
-        self.pk3 = getRSAKey(2,1024)
+        self.pk1 = getRSAKey(0,2048)
+        self.pk2 = getRSAKey(1,2048)
+        self.pk3 = getRSAKey(2,2048)
         self.tmpfile = mix_mktemp(".db")
         h = self.hlog = HashLog(self.tmpfile, "Z"*20)
 
@@ -2283,7 +2270,7 @@
         m = bfm("Z", MBOX_TYPE, "hello\000bye",
                 [self.server2, server1X, self.server3],
                 [server1X, self.server2, self.server3])
-        self.failUnlessRaises(ContentError, self.sp2.processMessage, m)
+        self.failUnlessRaises(ParseError, self.sp2.processMessage, m)
 
         # Duplicate messages need to fail.
         m = bfm("Z", SMTP_TYPE, "nobody@invalid",
@@ -2344,39 +2331,26 @@
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Subhead we can't parse
-        m_x = pk_encrypt("foo", self.pk1)+m[128:]
+        m_x = pk_encrypt("foo", self.pk1)+m[256:]
         self.failUnlessRaises(ParseError, self.sp1.processMessage, m_x)
 
         # Bad IPV4 info
-        subh_real = pk_decrypt(m[:128], self.pk1)
+        subh_real = pk_decrypt(m[:256], self.pk1)
         subh = parseSubheader(subh_real)
         subh.setRoutingInfo(subh.routinginfo + "X")
-        m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+        m_x = pk_encrypt(subh.pack()+subh.underflow[:-1], self.pk1)+m[256:]
         self.failUnlessRaises(ParseError, self.sp1.processMessage, m_x)
 
-        # Subhead that claims to be impossibly long: FWD case
-        subh = parseSubheader(subh_real)
-        subh.setRoutingInfo("X"*100)
-        m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
-        self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
-
-        # Subhead that claims to be impossibly long: exit case
-        subh = parseSubheader(subh_real)
-        subh.routingtype = MBOX_TYPE
-        subh.setRoutingInfo("X"*10000)
-        m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
-        self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
-
         # Bad Major or Minor
         subh = parseSubheader(subh_real)
         subh.major = 255
-        m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+        m_x = pk_encrypt(subh.pack()+subh.underflow, self.pk1)+m[256:]
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Bad digest
         subh = parseSubheader(subh_real)
         subh.digest = " "*20
-        m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+        m_x = pk_encrypt(subh.pack()+subh.underflow, self.pk1)+m[256:]
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Corrupt payload
@@ -2388,8 +2362,6 @@
         m_x = self.sp2.processMessage(m_x).getPacket()
         self.failUnlessRaises(CryptoError, self.sp3.processMessage, m_x)
 
-        
-
 #----------------------------------------------------------------------
 # QUEUE
 
@@ -3261,20 +3233,6 @@
         self.assertEquals(f['Sec3']['IntAMD2'], [5,2])
         self.assertEquals(f['Sec3']['IntRS'], 5)
 
-        # Test failing reload
-        writeFile(fn, "[Sec1]\nFoo=99\nBadEntry 3\n\n")
-        self.failUnlessRaises(ConfigError, f.reload)
-        self.assertEquals(f['Sec1']['Foo'], 'abcde f')
-        self.assertEquals(f['Sec1']['Bar'], 'bar')
-        self.assertEquals(f['Sec2']['Quz'], ['99 99', '88 88'])
-
-        # Test 'reload' operation
-        writeFile(fn, shorterString)
-        f.reload()
-        self.assertEquals(f['Sec1']['Foo'], 'a')
-        self.assertEquals(f['Sec1']['Bar'], "default")
-        self.assertEquals(f['Sec2'], {})
-
         # Test restricted mode
         s = "[Sec1]\nFoo: Bar\nBaz: Quux\n[Sec3]\nIntRS: 9\n"
         f = TCF(string=s, restrict=1)
@@ -3546,8 +3504,8 @@
                                               d)
         info = mixminion.ServerInfo.ServerInfo(string=inf)
         eq = self.assertEquals
-        eq(info['Server']['Descriptor-Version'], "0.1")
-        eq(info['Server']['IP'], "192.168.0.1")
+        eq(info['Server']['Descriptor-Version'], "0.2")
+        eq(info['Incoming/MMTP']['IP'], "192.168.0.1")
         eq(info['Server']['Nickname'], "The_Server")
         self.failUnless(0 <= time.time()-info['Server']['Published'] <= 120)
         self.failUnless(0 <= time.time()-info['Server']['Valid-After']
@@ -3555,8 +3513,8 @@
         eq(info['Server']['Valid-Until']-info['Server']['Valid-After'],
            10*24*60*60)
         eq(info['Server']['Contact'], "a@b.c")
-        eq(info['Server']['Software'], "Mixminion 0.0.4alpha")
-        eq(info['Server']['Packet-Formats'], "0.2")
+        eq(info['Server']['Software'], "Mixminion %s"%mixminion.__version__)
+        eq(info['Server']['Packet-Formats'], "0.3")
         eq(info['Server']['Comments'],
            "This is a test of the emergency broadcast system")
 
@@ -3703,6 +3661,17 @@
         self.assert_(not bobs[1].isSupersededBy([bobs[1]]))
         self.assert_(not bobs[1].isSupersededBy([freds[2]]))
 
+        # Test whether we ignore server descriptors with unknown versions.
+        try:
+            inf2 = inf.replace("Descriptor-Version: 0.2\n",
+                               "Descriptor-Version: 0.99\n")
+            inf2 = inf2.replace("\nIP: ", "\nIP: x.")
+            inf2 = inf2.replace("\nNickname: ", "\nNickname-XX: ")
+            ServerInfo(string=inf2)
+            self.fail("Missing ConfigError")
+        except ConfigError, p:
+            self.assertEquals(str(p), "Unrecognized descriptor version 0.99")
+
     def test_directory(self):
         eq = self.assertEquals
         examples = getExampleServerDescriptors()
@@ -3857,6 +3826,106 @@
         eq(5, len(lst.servers))
         eq(2, len(os.listdir(archiveDir)))
 
+
+#----------------------------------------------------------------------
+# EventStats
+class EventStatsTests(unittest.TestCase):
+    def testNilLog(self):
+        import mixminion.server.EventStats as ES
+        ES.configureLog({'Server': {'LogStats' : 0}})
+        self.failUnless(isinstance(ES.log, ES.NilEventLog))
+        ES.log.receivedPacket()
+        ES.log.attemptedRelay()
+        ES.log.failedRelay()
+        ES.log.unretriableRelay()
+
+    def testEventLog(self):
+        import mixminion.server.EventStats as ES
+        eq = self.assertEquals
+        homedir = mix_mktemp()
+        ES.configureLog({'Server':
+                         {'LogStats' : 1,
+                          'Homedir' : homedir,
+                 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+        self.failUnless(isinstance(ES.log, ES.EventLog))
+        # Test all commands
+        ES.log.receivedPacket()
+        ES.log.attemptedRelay()
+        ES.log.failedRelay()
+        ES.log.unretriableRelay()
+        ES.log.attemptedDelivery()
+        ES.log.attemptedDelivery()
+        ES.log.attemptedDelivery("Y")
+        ES.log.attemptedDelivery("Y")
+        ES.log.successfulDelivery()
+        ES.log.failedDelivery()
+        ES.log.failedDelivery("Y")
+        ES.log.unretriableDelivery("Y")
+        ES.log.save()
+        eq(ES.log.count['UnretriableDelivery']['Y'], 1)
+        eq(ES.log.count['AttemptedDelivery'][None], 2)
+        # Test reload.
+        ES.configureLog({'Server':
+                         {'LogStats' : 1,
+                          'Homedir' : homedir,
+                 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+        eq(ES.log.count['AttemptedDelivery']['Y'], 2)
+        # Test dump
+        buf = cStringIO.StringIO()
+        ES.log.dump(buf)
+        sOrig = buf.getvalue()
+        s = "\n".join(sOrig.split("\n")[1:])
+        expected = """\
+  ReceivedPacket: 1
+  AttemptedRelay: 1
+  SuccessfulRelay: 0
+  FailedRelay: 1
+  UnretriableRelay: 1
+  AttemptedDelivery:
+     {Unknown}: 2
+             Y: 2
+         Total: 4
+  SuccessfulDelivery: 1
+  FailedDelivery:
+     {Unknown}: 1
+             Y: 1
+         Total: 2
+  UnretriableDelivery:
+             Y: 1
+         Total: 1
+"""
+        eq(s, expected)
+        # Test time accumultion.
+        ES.log.save(now=time.time()+1800)
+        self.assert_(abs(1800-ES.log.accumulatedTime) < 10)
+        ES.log.lastSave = time.time()+1900
+        ES.log.save(now=time.time()+2000)
+        self.assert_(abs(1900-ES.log.accumulatedTime) < 10)
+        # Test rotate
+        ES.log.save(now=time.time()+3600*24)
+        s2 = readFile(os.path.join(homedir, "stats"))
+        eq(s2, sOrig)
+        eq(ES.log.count['UnretriableDelivery'], {})
+        ES.configureLog({'Server':
+                         {'LogStats' : 1,
+                          'Homedir' : homedir,
+                 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+        eq(ES.log.count['UnretriableDelivery'], {})
+        # Test time configured properly.
+        # XXXX
+        
+        # Test no rotation if not enough accumulated time
+        #XXXX THIS NEXT PART DOESN'T WORK....
+        ES.log.lastSave = time.time()-1000
+        ES.log._setNextRotation()
+        #print ES.log.nextRotation, time.time()+4000
+        r = ES.log._rotate
+        try:
+            ES.log._rotate = self.fail
+            ES.log.save(now=time.time()+9000)
+        finally:
+            ES.log._rotate = r
+
 #----------------------------------------------------------------------
 # Modules and ModuleManager
 
@@ -3977,53 +4046,6 @@
 
         self.assert_(exampleMod.processedAll[0].isPlaintext())
 
-#### All these tests belong as tests of DeliveryPacket
-
-##         # Try a real message, to make sure that we really decode stuff properly
-##         msg = mixminion.BuildMessage._encodePayload(
-##             "A man disguised as an ostrich, actually.",
-##             0, Crypto.getCommonPRNG())
-##         manager.queueMessage(msg, "A"*20, 1234, "Hello")
-##         exampleMod.processedAll = []
-##         manager.sendReadyMessages()
-##         # The retriable message got sent again; the other one, we care about.
-##         pos = None
-##         for i in xrange(len(exampleMod.processedAll)):
-##             if not exampleMod.processedAll[i][0].startswith('Hello'):
-##                 pos = i
-##         self.assert_(pos is not None)
-##         self.assertEquals(exampleMod.processedAll[i],
-##                           ("A man disguised as an ostrich, actually.",
-##                            None, 1234, "Hello" ))
-
-##         # Now a non-decodeable message
-##         manager.queueMessage("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches")
-##         exampleMod.processedAll = []
-##         manager.sendReadyMessages()
-##         pos = None
-##         for i in xrange(len(exampleMod.processedAll)):
-##             if not exampleMod.processedAll[i][0].startswith('Hello'):
-##                 pos = i
-##         self.assert_(pos is not None)
-##         self.assertEquals(exampleMod.processedAll[i],
-##                           ("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches"))
-
-##         # Now a message that compressed too much.
-##         # (first, erase the pending message.)
-##         manager.queues[exampleMod.getName()].removeAll()
-##         manager.queues[exampleMod.getName()]._rescan()
-
-##         p = "For whom is the funhouse fun?"*8192
-##         msg = mixminion.BuildMessage._encodePayload(
-##             p, 0, Crypto.getCommonPRNG())
-##         manager.queueMessage(msg, "Z"*20, 1234, "Buenas noches")
-##         exampleMod.processedAll = []
-##         self.assertEquals(len(exampleMod.processedAll), 0)
-##         manager.sendReadyMessages()
-##         self.assertEquals(len(exampleMod.processedAll), 1)
-##         self.assertEquals(exampleMod.processedAll[0],
-##             (compressData(p), 'long', 1234, "Buenas noches"))
-
         # Check serverinfo generation.
         try:
             suspendLog()
@@ -4275,7 +4297,7 @@
         """Check out the SMTP module.  (We temporarily relace sendSMTPMessage
            with a stub function so that we don't actually send anything.)"""
         FDP = FakeDeliveryPacket
-        
+
         blacklistFile = mix_mktemp()
         writeFile(blacklistFile, "Deny onehost wangafu.net\nDeny user fred\n")
 
@@ -4560,6 +4582,7 @@
         msg = resumeLog()
         self.failUnless(len(msg))
         Crypto.pk_PEM_save(identity, fn)
+        self.assertEquals(os.stat(fn)[stat.ST_MODE] & 0777, 0600)
 
         # Now create a keyset
         now = time.time()
@@ -5266,7 +5289,7 @@
         keyring = mixminion.ClientMain.ClientKeyring(keydir)
         # Check for some nonexistant keys.
         self.assertEquals({}, keyring.getSURBKeys(password="pwd"))
-        
+
         self.assertEquals(None, keyring.getSURBKey(create=0))
         # Reload, try again:
         kfirst = None
@@ -5299,7 +5322,7 @@
         finally:
             s = resumeLog()
         self.assert_(stringContains(s, "Incorrect password"))
-        
+
     def testMixminionClient(self):
         # Create and configure a MixminionClient object...
         parseAddress = mixminion.ClientMain.parseAddress
@@ -5442,7 +5465,9 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(MMTPTests))
+        suite.addTest(tc(ServerInfoTests))
+        #suite.addTest(tc(EventStatsTests))
+        #suite.addTest(tc(MMTPTests))
         #suite.addTest(tc(MiscTests))
         return suite
 
@@ -5457,6 +5482,7 @@
     suite.addTest(tc(BuildMessageTests))
     suite.addTest(tc(PacketHandlerTests))
     suite.addTest(tc(QueueTests))
+    suite.addTest(tc(EventStatsTests))
     suite.addTest(tc(ModuleTests))
 
     suite.addTest(tc(ClientMainTests))