[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[minion-cvs] More tests for everything.



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv13122/lib/mixminion

Modified Files:
	BuildMessage.py Common.py Crypto.py Formats.py HashLog.py 
	ServerInfo.py ServerProcess.py benchmark.py test.py 
Log Message:
More tests for everything.

BuildMessage and ServerProcess are now working: we support all of the
non-network-related algorithms and formats in the spec.  We can build
messages and reply blocks, and (if we're a server) process them enough
to pass messages off to the next server.  We handle extended headers,
swapping, everything.

Performance is good: on my Athlon XP1700+, the server core takes 8.5
ms for a non-swap message, and 9 ms for a swap message.  Of this, I
believe around 95% is spent in the C encryption code.

Next up, more documentation and comments and code clean-up.  After
that, get queues and persistent server information working.


Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- BuildMessage.py	29 May 2002 22:51:58 -0000	1.3
+++ BuildMessage.py	31 May 2002 12:47:58 -0000	1.4
@@ -4,6 +4,7 @@
 from mixminion.Formats import *
 import mixminion.Crypto as Crypto
 import mixminion.Modules as Modules
+import operator
 
 __all__ = [ 'buildForwardMessage', 'buildReplyBlock', 'buildReplyMessage',
             'buildStatelessReplyBlock' ]
@@ -12,21 +13,28 @@
     "XXXX"
     return _buildMessage(payload, exitType, exitInfo, path1, path2)
 
-def buildReplyMessage(payload, exitType, exitInfo, path1, replyBlock):
+def buildReplyMessage(payload, path1, replyBlock):
+    # Bad iface; shouldn't take a tuple
     "XXXX"
-    return _buildMessage(payload, exitType, exitInfo, path1, reply=replyBlock)
+    return _buildMessage(payload, None, None,
+                         path1=path1,
+                         reply=replyBlock)
 
 # Bad interface: this shouldn't return a tuple. 
-def buildReplyBlock(path, exitType, exitInfo, prng):
+def buildReplyBlock(path, exitType, exitInfo, secretPRNG=None):
     "XXXX"
-    secrets = [ prng.getBytes(SECRET_LEN) for node in path ]
-    headers = _buildHeaders(path, secrets, exitType, exitInfo)
-    return (headers, path[0]), secrets
+    if secretPRNG == None:
+        secretPRNG = Crypto.AESCounterPRNG()
+    secrets = [ secretPRNG.getBytes(SECRET_LEN) for node in path ]
+    header = _buildHeader(path, secrets, exitType, exitInfo, 
+                          paddingPRNG=Crypto.AESCounterPRNG())
+    return (header, path[0]), secrets
 
 # Bad interface: userkey should only be None if we trust the final node
 # a lot.
-def buildStatelessReplyBlock(path, prng, user, userKey=None, email=0):
+def buildStatelessReplyBlock(path, user, userKey=None, email=0):
     "XXXX"
+    # COMMENT
     if email:
         assert userKey
     seed = Crypto.trng(16)
@@ -34,103 +42,155 @@
         tag = Crypto.ctr_crypt(seed,userKey)
     else:
         tag = seed
-    if emal:
+    if email:
         exitType = Modules.SMTP_TYPE
-        exitInfo = SMTPInfo(user, "RTRN"+key).pack()
+        exitInfo = SMTPInfo(user, "RTRN"+tag).pack()
     else:
         exitType = Modules.LOCAL_TYPE
-        exitInfo = LocalInfo(user, "RTRN"+key).pack()
+        exitInfo = LocalInfo(user, "RTRN"+tag).pack()
 
     prng = Crypto.AESCounterPRNG(seed)
-    return buildReplyBlock(path, exitType, exitInto, prng)
+    return buildReplyBlock(path, exitType, exitInfo, prng)[0]
 
 #----------------------------------------------------------------------
+# needs a more informative name
 def _buildMessage(payload, exitType, exitInfo,
-                  path1, path2=None, reply=None, prng=None, paranoia=0):
+                  path1, path2=None, reply=None, paddingPRNG=None, paranoia=0):
     "XXXX"
     assert path2 or reply
-    if prng == None:
-        prng = Crypto.AESCounterPRNG()
+    assert not (path2 and reply)
+    if paddingPRNG == None:
+        paddingPRNG = Crypto.AESCounterPRNG()
 
     # ???? Payload padding/sizing must be handled in spec.
     if len(payload) < PAYLOAD_LEN:
-        payload += prng.getBytes(PAYLOAD_LEN-len(payload))
+        payload += paddingPRNG.getBytes(PAYLOAD_LEN-len(payload))
+
+    nHops = len(path1)
+    if path2: nHops += len(path2)
 
     if paranoia:
-        secrets1 = [ Crypto.trng(SECRET_LEN) for node in path1 ]
-        if path2: secrets2 = [ Crypto.trng(SECRET_LEN) for node in path2 ]
+        secretRNG = Crypto.TrueRNG(SECRET_LEN*len(nHops))
     else:
-        secrets1 = [ prng.getBytes(SECRET_LEN) for node in path1 ]
-        if path2: secrets2 = [ prng.getBytes(SECRET_LEN) for node in path2 ]
+        secretRNG = paddingPRNG
 
-    if path2:
-        node = path2[0]
+    if reply:
+        path1exitnode = reply[1]
+        path2exit = None
+        reply = reply[0]
     else:
-        node = reply[1]
-    info = IPV4Info(node.getAddr(), node.getPort(), node.getKeyID())
-    headers1 = _buildHeaders(path1, secrets1, Modules.SWAP_FWD_TYPE, info,prng)
+        path1exitnode = path2[0]
+        path2exit = ( exitType, exitInfo )
+    
+    path1exit = ( Modules.SWAP_FWD_TYPE,
+                  path1exitnode.getRoutingInfo().pack() )
+    
+    return _buildMessage_impl(payload, path1, path1exit, path2, path2exit,
+                              reply, nHops, secretRNG, paddingPRNG)
+
+#needs a more informative name
+def _buildMessage_impl(payload, path1, path1exit, path2, path2exit,
+                       reply, nHops, secretRNG, paddingRNG):
+    "XXXX"
+    # XXXX ???? Payload padding/sizing must be handled in spec.
+    if len(payload) < PAYLOAD_LEN:
+        payload += paddingRNG.getBytes(PAYLOAD_LEN-len(payload))
+
+    secrets1 = [ secretRNG.getBytes(SECRET_LEN) for x in range(len(path1)) ]
+    
     if path2:
-        headers2 = _buildHeaders(path2, secrets2, exitType, exitInfo, prng)
+        # Make secrets2 before header1 so we don't use the RNG to pad 
+        # the first header yet.
+        secrets2 = [ secretRNG.getBytes(SECRET_LEN) for x in range(len(path2))]
+        header2 = _buildHeader(path2,secrets2,path2exit[0],path2exit[1],
+                                 paddingRNG)
     else:
-        headers2 = reply[0]
-    return _constructMessage(secrets1, secrets2, headers1, headers2, payload)
+        secrets2 = None
+        header2 = reply
 
+    header1 = _buildHeader(path1,secrets1,path1exit[0],path1exit[1],
+                             paddingRNG)
+    return _constructMessage(secrets1, secrets2, header1, header2, payload)
 
-def _buildHeaders(path, secrets, exitType, exitInfo, prng):
+def _buildHeader(path,secrets,exitType,exitInfo,paddingPRNG):
     "XXXX"
-    hops = len(path)
-
-    #Calculate all routing info
+    # XXXX insist on sane parameters, path lengths.
     routing = []
-    for i in range(hops-1):
+    for i in range(len(path)-1):
         nextNode = path[i+1]
-        info = IPV4Info(nextNode.getAddr(), nextNode.getPort(),
-                        nextNode.getKeyID())
+        info = nextNode.getRoutingInfo()
         routing.append( (Modules.FWD_TYPE, info.pack() ) )
+        
+    routing.append( (exitType, exitInfo) )
     
-    routing.append( (exitType, exitInfo) )                   
-    
-    # size[i] is size, in blocks, of headers for i.
-    size = [ getTotalBlocksForRoutingInfoLen(len(info)) for t, info in routing]
+    return _buildHeader_impl(path,secrets,routing,paddingPRNG)
 
-    totalSize = len(path)+size[-1]-1
+#routing is list of (type,info)
+def _buildHeader_impl(path, secrets, routing, paddingPRNG):
+    "XXXX"
+    hops = len(path)
 
-    # Calculate masks, junk.  mask[i] == the mask used by node i.
-    #   junkSeen[i]==the junk that node i will see, before encryption.
-    #XXXX Comment better
-    headerKeys = []
+    # sizes[i] is size, in blocks, of subheaders for i.
+    sizes =[ getTotalBlocksForRoutingInfoLen(len(info)) for t, info in routing]
+    # totalSize is number total number of blocks.
+    totalSize = reduce(operator.add, sizes)
+
+    # 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 ]
+    
+    # Calculate junk.  
+    #   junkSeen[i]==the junk that node i will see, before it does any
+    #                encryption.   Note that junkSeen[0]=="", because node 0
+    #                sees no junk.
     junkSeen = [ "" ]
-    for secret, size in zip(secrets, size):
-        ks = Crypto.Keyset(secret)
-        headerKey = Crypto.aes_key(ks.get(Crypto.HEADER_SECRET_MODE))
-        sz = size*128
-        newJunk = Crypto.prng(ks.get(Crypto.RANDOM_JUNK_MODE),sz)
+    for secret, headerKey, size in zip(secrets, headerKeys, sizes):
+        # Here we're calculating the junk that node i+1 will see.
+        #
+        # 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)
         nextJunk = junkSeen[-1] + newJunk
-        nextJunk = Crypto.ctr_crypt(nextJunk, headerKey,
-                                    HEADER_LEN-len(nextJunk))
+        # 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]
+        #          The parts of the header that aren't junk.
+        #                                           [HEADER_LEN-len(nextJunk)]
+        #
+        # Simplifying, we find that the PRNG index for the junk is
+        #      HEADER_LEN-len(junkSeen[-1])-128*size+128*size-128
+        #    = HEADER_LEN-len(junkSeen[-1])-128
+        startIdx = HEADER_LEN-len(junkSeen[-1])-128
+        nextJunk = Crypto.ctr_crypt(nextJunk, headerKey, startIdx)
         junkSeen.append(nextJunk)
-        headerKeys.append(headerKey)
-        
-    header = prng.getBytes(HEADER_LEN - totalSize*128)
 
-    #XXXX comment better
+    # We start with the padding.
+    header = paddingPRNG.getBytes(HEADER_LEN - totalSize*128)
+
+    # Now, we build the subheaders, iterating through the nodes backwards.
     for i in range(hops-1, -1, -1):
-        rest = Crypto.ctr_crypt(header, headerKeys[i])
-        digest = Crypto.sha1(rest+junkSeen[i])
-        pubkey = Crypto.pk_from_modulus(path[i].getModulus())
         rt, ri = routing[i]
 
         subhead = Subheader(MAJOR_NO, MINOR_NO,
-                            secrets[i], digest,
-                            rt, ri).pack()
-        esh = Crypto.pk_encrypt(subhead, pubkey)
+                            secrets[i], " "*20,
+                            rt, ri)
+
+        extHeaders = "".join(subhead.getExtraBlocks())
+        rest = Crypto.ctr_crypt("".join((extHeaders,header)), headerKeys[i])
+        subhead.digest = Crypto.sha1(rest+junkSeen[i])
+        pubkey = Crypto.pk_from_modulus(path[i].getModulus())
+        esh = Crypto.pk_encrypt(subhead.pack(), pubkey)
         header = esh + rest
 
     return header
- 
+
 # For a reply, secrets2==None
 def _constructMessage(secrets1, secrets2, header1, header2, payload):
     "XXXX"
+    #XXXX comment    
     assert len(payload) == PAYLOAD_LEN
     assert len(header1) == len(header2) == HEADER_LEN
     secrets1 = secrets1[:]

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- Common.py	29 May 2002 03:52:13 -0000	1.1
+++ Common.py	31 May 2002 12:47:58 -0000	1.2
@@ -4,5 +4,8 @@
 class MixError(Exception):
     pass
 
+class MixFatalError(MixError):
+    pass
+
 
 

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- Crypto.py	29 May 2002 22:51:58 -0000	1.4
+++ Crypto.py	31 May 2002 12:47:58 -0000	1.5
@@ -11,14 +11,14 @@
 import sys
 import mixminion._minionlib as _ml
 
-__all__ = [ 'init_crypto', 'sha1',  'ctr_crypt', 'prng',
+__all__ = [ 'init_crypto', 'sha1',  'ctr_crypt', 'prng', 'strxor',
             'lioness_encrypt', 'lioness_decrypt', 'trng', 'pk_encrypt',
             'pk_decrypt', 'pk_generate', 'openssl_seed',
             'pk_get_modulus', 'pk_from_modulus',
             'pk_encode_private_key', 'pk_decode_private_key',
             'Keyset', 'AESCounterPRNG',
             'HEADER_SECRET_MODE', 'PRNG_MODE', 'RANDOM_JUNK_MODE',
-            'HEADER_ENCRYPT_MODE',
+            'HEADER_ENCRYPT_MODE', 'APPLICATION_KEY_MODE',
             'PAYLOAD_ENCRYPT_MODE', 'HIDE_HEADER_MODE' ]
 
 AES_KEY_LEN = 128/8
@@ -109,7 +109,6 @@
     left = s[:20]
     right = s[20:]
     del s
-    #XXXX This slice makes me nervous
     left = _ml.strxor(left,  _ml.sha1("".join([key4,right,key4])))
     right = ctr_crypt(right, _ml.sha1("".join([key3,left,key3]))[:16])
     left = _ml.strxor(left,  _ml.sha1("".join([key2,right,key2])))
@@ -127,10 +126,7 @@
 
     Returns (count) bytes of true random data from a true source of
     entropy (/dev/urandom)"""
-    f = open('/dev/urandom')
-    d = f.read(count)
-    f.close()
-    return d
+    return _theTrueRNG.getBytes(count)
 
 OAEP_PARAMETER = "He who would make his own liberty secure, "+\
                  "must guard even his enemy from oppression."
@@ -194,6 +190,7 @@
 PAYLOAD_ENCRYPT_MODE = "PAYLOAD ENCRYPT"
 HIDE_HEADER_MODE = "HIDE HEADER"
 REPLAY_PREVENTION_MODE = "REPLAY PREVENTION"
+APPLICATION_KEY_MODE = "APPLICATION KEY"
 
 class Keyset:
     """A Keyset represents a set of keys generated from a single master
@@ -230,20 +227,14 @@
 
 #---------------------------------------------------------------------
 
-class AESCounterPRNG:
-    _CHUNKSIZE = 16*1024
-    _KEYSIZE = 16
-    def __init__(self, seed=None):
-        self.counter = 0
-        self.bytes = ""
-        if seed==None: seed=trng(AESCounterPRNG._KEYSIZE)
-        self.key = _ml.aes_key(seed)
-
+class RNG:
+    def __init__(self, chunksize):
+        self.bytes=""
+        self.chunksize=chunksize
     def getBytes(self, n):
         if n > len(self.bytes):
-            nMore = n+AESCounterPRNG._CHUNKSIZE-len(self.bytes)
-            morebytes = prng(self.key,nMore,self.counter)
-            self.counter+=nMore
+            nMore = n+self.chunksize-len(self.bytes)
+            morebytes = self._prng(nMore)
             res = self.bytes+morebytes[:n-len(self.bytes)]
             self.bytes=morebytes[n-len(self.bytes):]
             return res
@@ -251,3 +242,31 @@
             res = self.bytes[:n]
             self.bytes=self.bytes[n:]
             return res
+    def _prng(self, n):
+        assert 0
+
+class AESCounterPRNG(RNG):
+    def __init__(self, seed=None):
+        RNG.__init__(self, 16*1024)
+        self.counter = 0
+        if seed == None:
+            seed = trng(16)
+        self.key = aes_key(seed)
+    def _prng(self, n):
+        c = self.counter
+        self.counter+=n
+        return prng(self.key,n,c)
+
+def trng_uncached(n):
+    f = open('/dev/urandom')
+    d = f.read(n)
+    f.close()
+    return d
+
+class _TrueRNG(RNG):
+    def __init__(self,n):
+        RNG.__init__(self,n)
+    def _prng(self,n):
+        return trng_uncached(n)
+
+_theTrueRNG = _TrueRNG(128)

Index: Formats.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Formats.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- Formats.py	29 May 2002 22:51:58 -0000	1.3
+++ Formats.py	31 May 2002 12:47:58 -0000	1.4
@@ -8,8 +8,8 @@
 __all__ = [ 'ParseError', 'Message', 'Header', 'Subheader',
             'parseMessage', 'parseHeader', 'parseSubheader',
             'getTotalBlocksForRoutingInfoLen',
-            'IPV4Info', 'SMTPInfo',
-            'parseIPV4Info', 'parseSMTPInfo',
+            'IPV4Info', 'SMTPInfo', 'LocalInfo',
+            'parseIPV4Info', 'parseSMTPInfo', 'parseLocalInfo',
             'ENC_SUBHEADER_LEN', 'HEADER_LEN',
             'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO',
             'SECRET_LEN']
@@ -116,8 +116,11 @@
     if len(s) < MIN_SUBHEADER_LEN:
         raise ParseError("Header too short")
 
-    major, minor, secret, digest, rlen, rt = \
-           struct.unpack(SH_UNPACK_PATTERN, s[:MIN_SUBHEADER_LEN])
+    try:
+        major, minor, secret, digest, rlen, rt = \
+               struct.unpack(SH_UNPACK_PATTERN, s[:MIN_SUBHEADER_LEN])
+    except struct.error:
+        raise ParseError("Misformatted subheader")
     ri = s[MIN_SUBHEADER_LEN:]
     if rlen < len(ri):
         ri = ri[:rlen]
@@ -183,7 +186,9 @@
         self.routinginfo = ("".join(raw))[:self.routinglen]
         
     def pack(self):
-        """Returns the (unencrypted) string representation of this Subhead"""
+        """Returns the (unencrypted) string representation of this Subhead.
+
+           Does not include extra blocks"""
         assert self.routinglen == len(self.routinginfo)
         assert len(self.digest) == DIGEST_LEN
         assert len(self.secret) == SECRET_LEN
@@ -229,15 +234,21 @@
 def _unpackIP(s):
     "XXXX"
     if len(s) != 4: raise ParseError("Malformed IP")
-    return ".".join(map(str, struct.unpack("!BBBB", s)))
-
+    try:
+        return ".".join(map(str, struct.unpack("!BBBB", s)))
+    except struct.error:
+        raise ParseError("Misformatted IP address")
+    
 def parseIPV4Info(s):
     """parseIP4VInfo(s) -> IPV4Info
 
        Converts routing info for an IPV4 address into an IPV4Info object."""
     if len(s) != 4+2+DIGEST_LEN:
         raise ParseError("IPV4 information with wrong length")
-    ip, port, keyinfo = struct.unpack(IPV4_PAT, s)
+    try:
+        ip, port, keyinfo = struct.unpack(IPV4_PAT, s)
+    except struct.error:
+        raise ParseError("Misformatted IPV4 routing info")
     ip = _unpackIP(ip)
     return IPV4Info(ip, port, keyinfo)
 
@@ -274,10 +285,11 @@
         
 def parseLocalInfo(s):
     "XXXX"
-    nil = s.find('\000')
-    user = s[:nil]
-    tag = s[nil+1]
-    return LocalInfo(user,tag)
+    lst = s.split("\000",1)
+    if len(lst) == 1:
+        return LocalInfo(s,None)
+    else:
+        return LocalInfo(lst[0], lst[1])
     
 class LocalInfo:
     "XXXX"
@@ -287,4 +299,7 @@
         self.tag = tag
 
     def pack(self):
-        return self.user+"\000"+self.tag
+        if self.tag:
+            return self.user+"\000"+self.tag
+        else:
+            return self.user

Index: HashLog.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/HashLog.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- HashLog.py	29 May 2002 03:52:13 -0000	1.1
+++ HashLog.py	31 May 2002 12:47:58 -0000	1.2
@@ -2,6 +2,7 @@
 # $Id$
 
 import anydbm
+from mixminion.Common import MixFatalError
 
 __all__ = [ 'HashLog' ]
 
@@ -30,8 +31,7 @@
         self.log = anydbm.open(filename, 'c')
         try:
             if self.log["KEYID"] != keyid:
-                print "Mismatch on keyid"
-                #XXXX Need warning mechanism
+                raise MixFatalError("Log KEYID does not match current KEYID")
         except KeyError:
             self.log["KEYID"] = keyid
             

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- ServerInfo.py	29 May 2002 22:51:58 -0000	1.2
+++ ServerInfo.py	31 May 2002 12:47:58 -0000	1.3
@@ -1,15 +1,20 @@
 # Copyright 2002 Nick Mathewson.  See LICENSE for licensing information.
 # $Id$
 
-#XXXX DOC
+"XXXX"
 
 __all__ = [ 'ServerInfo' ]
 
+from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
+from mixminion.Formats import IPV4Info
+
 #
 # Stub class till we have the real thing
 #
 class ServerInfo:
+    "XXXX"
     def __init__(self, addr, port, modulus, keyid):
+        "XXXX"
         self.addr = addr
         self.port = port
         self.modulus = modulus
@@ -19,4 +24,7 @@
     def getPort(self): return self.port
     def getModulus(self): return self.modulus
     def getKeyID(self): return self.keyid
+    
+    def getRoutingInfo(self, swap=0):
+        return IPV4Info(self.addr, self.port, self.keyid)
     

Index: ServerProcess.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerProcess.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- ServerProcess.py	29 May 2002 22:51:58 -0000	1.3
+++ ServerProcess.py	31 May 2002 12:47:58 -0000	1.4
@@ -3,14 +3,19 @@
 
 import mixminion.Crypto as Crypto
 import mixminion.Formats as Formats
+from mixminion.Formats import MAJOR_NO, MINOR_NO
 import mixminion.Modules as Modules
 import mixminion.Common as Common
 
+__all__ = [ 'ServerProcess', 'ContentError' ]
+
 class ContentError(Common.MixError):
+    """XXXX"""
     pass
 
 class ServerProcess:
     def __init__(self, privatekey, hashlog, exitHandler, forwardHandler):
+        """XXXX"""
         self.privatekey = privatekey
         self.hashlog = hashlog
         self.exitHandler = exitHandler
@@ -18,83 +23,90 @@
 
     # Raises ParseError, ContentError.
     def processMessage(self, msg):
+        """XXXX"""
         r = self._processMessage(msg)
         if r != None:
             m, a = r
-            apply(m, a)
+            assert m in ("EXIT", "QUEUE")
+            if m == "EXIT":
+                self.exitHandler.handle(*a)
+            else:
+                self.forwardHandler.handle(*a)
 
     # Raises ParseError, ContentError, SSLError.
-    #  Returns oneof (None), (method, argl)
+    #  Returns oneof (None), (EXIT, argl), ("QUEUE", (ipv4info, msg))
     def _processMessage(self, msg):
+        """XXXX"""
+        # XXXX Comment better
         msg = Formats.parseMessage(msg)
         header1 = Formats.parseHeader(msg.header1)
         subh = header1[0]
         subh = Crypto.pk_decrypt(subh, self.privatekey)
         subh = Formats.parseSubheader(subh)
 
-        if subh.major != 3 or subh.minor != 0:
+        if subh.major != MAJOR_NO or subh.minor != MINOR_NO:
             raise ContentError("Invalid protocol version")
 
-        # XXX Check digest, then decrypt the rest?  Or decrypt the rest,
-        # XXX then check the digest???  The spec is inconsistant.
         digest = Crypto.sha1(header1[1:])
         if digest != subh.digest:
             raise ContentError("Invalid digest")
 
-        # XXXX Need to decrypt extra routing info.
-        if subh.isExtended():
-            nExtra = subh.getNExtraBlocks() 
-            if nExtra > 15:
-                raise ContentError("Impossibly long routing info length")
-            extra = header1[1:1+nExtra]
-            subh.appendExtraBlocks(extra)
-            remainingHeader = header1[1+nExtra:]
-        else:
-            remainingHeader = header1[1:]
-
+        keys = Crypto.Keyset(subh.secret)
         # Replay prevention
-        keys = Crypto.Keyset(subh.master)
         replayhash = keys.get(Crypto.REPLAY_PREVENTION_MODE, 20)
         if self.hashlog.seenHash(replayhash):
             raise ContentError("Duplicate message detected.")
         else:
             self.hashlog.logHash(replayhash)
-            
-        if type == Modules.DROP_TYPE:
+
+        rt = subh.routingtype
+        if rt == Modules.DROP_TYPE:
             return None
 
+        header_sec_key = Crypto.aes_key(keys.get(Crypto.HEADER_SECRET_MODE))
+
+        if subh.isExtended():
+            nExtra = subh.getNExtraBlocks() 
+            if (rt < Modules.MIN_EXIT_TYPE) or (nExtra > 15):
+                raise ContentError("Impossibly long routing info length")
+                
+            extra = Crypto.ctr_crypt(header1[1:1+nExtra], header_sec_key)
+            subh.appendExtraBlocks(extra)
+            remainingHeader = header1[1+nExtra:]
+        else:
+            nExtra = 0
+            remainingHeader = header1[1:]
+
         payload = Crypto.lioness_decrypt(msg.payload,
-                                         keys.get(Crypto.PAYLOAD_ENCRYPT_MODE))
+                              keys.getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE))
 
-        # XXXX This doesn't match what George said.
-        if type > Modules.MIN_EXIT_TYPE:
-            return (self.exitHandler.processMessage,
-                    (subh.routingtype, subh.routinginfo,
+        # XXXX This doesn't match what George said: it bails out too early.
+        # XXXX Also, it doesn't return the headers.
+        if rt >= Modules.MIN_EXIT_TYPE:
+            return ("EXIT",
+                    (rt, subh.routinginfo,
                      keys.get(Crypto.APPLICATION_KEY_MODE),
                      payload))
 
-        if type not in (SWAP_FWD_TYPE, FWD_TYPE):
+        if rt not in (Modules.SWAP_FWD_TYPE, Modules.FWD_TYPE):
             raise ContentError("Unrecognized mixminion type")
 
         remainingHeader = remainingHeader +\
                           Crypto.prng(keys.get(Crypto.PRNG_MODE),
-                                     FORMATS.HEADER_LEN-len(remainingHeader))
-        header1 = Crypto.ctr_crypt(remainingHeader,
-                                   keys.get(Crypto.HEADER_SECRET_MODE))
-        
+                                      Formats.HEADER_LEN-len(remainingHeader))
+
+        header1 = Crypto.ctr_crypt(remainingHeader, header_sec_key, nExtra*128)
+
         header2 = Crypto.lioness_decrypt(msg.header2,
-                                         keys.get(Crypto.HEADER_ENCRYPT_MODE))
+                           keys.getLionessKeys(Crypto.HEADER_ENCRYPT_MODE))
 
-        if type == Modules.SWAP_FWD_TYPE:
-            hkey = Crypto.get_lioness_keys_from_payload(payload)
-            header2 = Crypto.lioness_decrypt(msg.header2, hkey)
+        if rt == Modules.SWAP_FWD_TYPE:
+            hkey = Crypto.lioness_keys_from_payload(payload)
+            header2 = Crypto.lioness_decrypt(header2, hkey)
             header1, header2 = header2, header1
 
         address = Formats.parseIPV4Info(subh.routinginfo)
 
         msg = Formats.Message(header1, header2, payload).pack()
 
-        return (self.forwardHandler.queue, (address.ip,
-                                            address.port,
-                                            address.keyid,
-                                            msg))
+        return ("QUEUE", (address, msg))

Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- benchmark.py	29 May 2002 22:51:58 -0000	1.3
+++ benchmark.py	31 May 2002 12:47:58 -0000	1.4
@@ -2,15 +2,23 @@
 # $Id$
 from time import time
 
+__all__ = [ 'timeAll', 'testLeaks1', 'testLeaks2' ]
+
+PRECISION_FACTOR = 1
+
+primitives = {}#XXXXDOC
 loop_overhead = {}
-def timeit_(fn, iters, ov=1):
+def timeit_(fn, iters, ov=1, p=None):
     """XXXX"""
+    iters *= PRECISION_FACTOR
     nones = [None]*iters
     overhead = [0, loop_overhead.get(iters, 0)][ov]
     t = time()
     for n in nones: fn()
     t2 = time()-t
-    return (t2-overhead) / float(iters)
+    t_each = (t2-overhead) / float(iters)
+    if p: primitives[p]=t_each
+    return t_each
 
 min_o = 1.0
 max_o = 0.0
@@ -31,9 +39,9 @@
     else:
         return "%f psec" % (t*1000000000L)
 
-def timeit(fn,times):
+def timeit(fn,times, p=None):
     """XXXX"""
-    return timestr(timeit_(fn,times))
+    return timestr(timeit_(fn,times,p=p))
 
 def spacestr(n):
     if abs(n) < 1e4:
@@ -51,7 +59,6 @@
 from Crypto import OAEP_PARAMETER
 
 def cryptoTiming():
-    loop_overhead = {}
     short = "Hello, Dali!"
     s1K = "8charstr"*128
     s2K = s1K*2
@@ -64,8 +71,9 @@
     print "Timing overhead: %s...%s" % (timestr(min_o),timestr(max_o))
 
     print "SHA1 (short)", timeit((lambda : sha1(short)), 100000)
+    print "SHA1 (2K)", timeit((lambda : sha1(s8K)), 10000, p="SHA2")
     print "SHA1 (8K)", timeit((lambda : sha1(s8K)), 10000)
-    print "SHA1 (28K)", timeit((lambda : sha1(s28K)), 1000)
+    print "SHA1 (28K)", timeit((lambda : sha1(s28K)), 1000, p="SHA28")
     print "SHA1 (32K)", timeit((lambda : sha1(s32K)), 1000)
 
     shakey = "8charstr"*2
@@ -73,7 +81,7 @@
     #print timeit((lambda : _ml.sha1(short,shakey)), 100000)
     #print "Keyed SHA1 (8K)", timeit((lambda : _ml.sha1(s8K, shakey)), 10000)
     #print "Keyed SHA1 (32K)", timeit((lambda : _ml.sha1(s32K, shakey)), 1000)
-    print "Lioness-keyed SHA1 (28K, unoptimized)", timeit(
+    print "Keyed SHA1 for lioness (28K, unoptimized)", timeit(
         (lambda : _ml.sha1("".join([shakey,s28K,shakey]))), 1000)
 
     print "TRNG (20 byte)", timeit((lambda: trng(20)), 100)
@@ -86,7 +94,8 @@
     key = "8charstr"*2
     print "aes (short)", timeit((lambda: ctr_crypt(short,key)), 100000)
     print "aes (1K)", timeit((lambda: ctr_crypt(s1K,key)), 10000)
-    print "aes (28K)", timeit((lambda: ctr_crypt(s28K,key)), 100)
+    print "aes (2K)", timeit((lambda: ctr_crypt(s1K,key)), 10000, p="AES2")
+    print "aes (28K)", timeit((lambda: ctr_crypt(s28K,key)), 100, p="AES28")
     print "aes (32K)", timeit((lambda: ctr_crypt(s32K,key)), 100)
 
     key = _ml.aes_key(key)
@@ -99,17 +108,21 @@
         (lambda: _ml.strxor(prng(key,32768),s32K)), 100)
 
     print "prng (short)", timeit((lambda: prng(key,8)), 100000)
+    print "prng (128b)", timeit((lambda: prng(key,18)), 10000, p="PRNG128b")
     print "prng (1K)", timeit((lambda: prng(key,1024)), 10000)
-    print "prng (28K)", timeit((lambda: prng(key,28678)), 100)
+    print "prng (2K)", timeit((lambda: prng(key,1024)), 10000, p="PRNG2")
+    print "prng (28K)", timeit((lambda: prng(key,28678)), 100, p="PRNG28")
     print "prng (32K)", timeit((lambda: prng(key,32768)), 100)
     print "prng (32K, unoptimized)", timeit(
         (lambda: ctr_crypt('\x00'*32768, key)), 100)
 
     lkey = Keyset("keymaterial foo bar baz").getLionessKeys("T")
     print "lioness E (1K)", timeit((lambda: lioness_encrypt(s1K, lkey)), 1000)
-    print "lioness E (2K)", timeit((lambda: lioness_encrypt(s1K, lkey)), 1000)
+    print "lioness E (2K)", timeit((lambda: lioness_encrypt(s1K, lkey)), 1000,
+                                   p="LIONESS2")
     print "lioness E (4K)", timeit((lambda: lioness_encrypt(s4K, lkey)), 1000)
-    print "lioness E (28K)", timeit((lambda: lioness_encrypt(s28K, lkey)), 100)
+    print "lioness E (28K)", timeit((lambda: lioness_encrypt(s28K, lkey)), 100,
+                                    p="LIONESS28")
     print "lioness E (32K)", timeit((lambda: lioness_encrypt(s32K, lkey)), 100)
     print "lioness D (1K)", timeit((lambda: lioness_decrypt(s1K, lkey)), 1000)
     print "lioness D (2K)", timeit((lambda: lioness_decrypt(s1K, lkey)), 1000)
@@ -127,30 +140,42 @@
     print "RSA generate (1024 bit)", timeit((lambda: pk_generate()),10)
     rsa = pk_generate()
     print "Pad+RSA public encrypt",
-    print timeit((lambda: pk_encrypt(s70b, rsa)),1000)
+    print timeit((lambda: pk_encrypt(s70b, rsa)),1000,p="RSAENC")
+    
     enc = pk_encrypt(s70b, rsa)
     print "Pad+RSA private decrypt", timeit((lambda: pk_decrypt(enc, rsa)),100)
 
-    for (bits,it) in ((2048,10),(4096,3)):
+    for (bits,it) in ((2048,10),(4096,10)):
+        t = time()
         rsa2 = pk_generate(bits)
+        t = time()-t
+        print "RSA genrate (%d bit)"%bits, timestr(t)
         enc = pk_encrypt(s70b, rsa2)
         print "Pad+RSA private decrypt (%d bit)"%bits,
         print timeit((lambda: pk_decrypt(enc, rsa2)),it)
 
 #----------------------------------------------------------------------
 def hashlogTiming():
+    import tempfile, os
+    print "#==================== HASH LOGS ======================="
     for load in (100, 1000, 10000, 100000):
-        _hashlogTiming(load)
+        fname = tempfile.mktemp(".db")
+        try:
+            _hashlogTiming(fname,load)
+        finally:
+            try:
+                os.unlink(fname)
+            except:
+                pass
 
-def _hashlogTiming(load):
-    import tempfile, os
+def _hashlogTiming(fname, load):
+    import os
     from mixminion.Crypto import AESCounterPRNG
     from mixminion.HashLog import HashLog
     prng = AESCounterPRNG("a"*16)
-    fname = tempfile.mktemp(".db")
-    
+        
     h = HashLog(fname, "A")
-    hashes = [ prng.getBytes(20) for i in range(load) ]
+    hashes = [ prng.getBytes(20) for i in xrange(load) ]
 
     t = time()
     for hash in hashes:
@@ -164,7 +189,7 @@
     t = time()-t    
     print "Check entry [hit] (%s entries)" %load, timestr( t/1000.0 )
 
-    hashes =[ prng.getBytes(20) for i in range(1000) ]
+    hashes =[ prng.getBytes(20) for i in xrange(1000) ]
     t = time()
     for hash in hashes:
         h.seenHash(hash)
@@ -173,9 +198,75 @@
 
     h.close()
     print "File size (%s entries)"%load, spacestr(os.stat(fname).st_size)
-    os.unlink(fname)
 
 #----------------------------------------------------------------------
+
+def buildMessageTiming():
+    import mixminion.BuildMessage as BMsg
+    from mixminion.ServerInfo import ServerInfo
+    print "#================= BUILD MESSAGE ====================="
+    pk = pk_generate()
+    payload = ("Junky qoph flags vext crwd zimb."*1024)[:22*1024]
+    serverinfo = [ ServerInfo("127.0.0.1", 48099, pk_get_modulus(pk),
+                              "x"*20) ] * 16
+    def bh(np,it):
+        ctr = AESCounterPRNG()
+        
+        tm = timeit_( \
+              lambda: BMsg._buildHeader(serverinfo[:np], ["Z"*16]*np,
+                                        99, "Hello", ctr), it )
+        
+        print "Build header (%s)" %(np), timestr(tm)
+
+    bh(1,100)
+    bh(4,40)
+    bh(8,20)
+    bh(16,10)
+    
+    def bm(np1,np2,it):
+        tm = timeit_( \
+              lambda: BMsg.buildForwardMessage(payload,
+                                               1,
+                                               "Hello",
+                                               serverinfo[:np1],
+                                               serverinfo[:np2]), it)
+        print "Build forward message (%sx%s)" %(np1,np2), timestr(tm)
+
+
+    bm(1,1,100)
+    bm(8,1,40)
+    bm(8,8,20)
+    bm(16,16,10)
+#----------------------------------------------------------------------
+
+def serverProcessTiming():
+    print "#================= SERVER PROCESS ====================="
+    from mixminion.ServerProcess import ServerProcess
+    from mixminion.ServerInfo import ServerInfo
+    from mixminion.BuildMessage import buildForwardMessage
+    from mixminion.Modules import SMTP_TYPE
+    class DummyLog:
+        def seenHash(self,h): return 0
+        def logHash(self,h): pass
+
+    pk = pk_generate()
+    n = pk_get_modulus(pk)
+    server = ServerInfo("127.0.0.1", 1, n, "X"*20)
+    sp = ServerProcess(pk, DummyLog(), None, None)
+
+    m_noswap = buildForwardMessage("Hello world", SMTP_TYPE, "f@invalid",
+                                   [server, server], [server, server])
+
+    print "Server process (no swap, no log)", timeit(
+        lambda : sp._processMessage(m_noswap), 100)
+
+    m_swap = buildForwardMessage("Hello world", SMTP_TYPE, "f@invalid",
+                                 [server], [server, server])
+
+    print "Server process (swap, no log)", timeit(
+        lambda : sp._processMessage(m_swap), 100)
+    
+#----------------------------------------------------------------------
 def testLeaks1():
     print "Trying to leak (sha1,aes,xor,seed,oaep)"
     s20k="a"*20*1024
@@ -235,7 +326,9 @@
 
 def timeAll():
     cryptoTiming()
+    buildMessageTiming()
     hashlogTiming()
+    serverProcessTiming()
 
 if __name__ == '__main__':
     timeAll()

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- test.py	29 May 2002 22:51:58 -0000	1.4
+++ test.py	31 May 2002 12:47:58 -0000	1.5
@@ -3,134 +3,172 @@
 
 import unittest
 
+def hexread(s):
+    r = []
+    hexvals = "0123456789ABCDEF"
+    for i in range(len(s) // 2):
+        v1 = hexvals.index(s[i*2])
+        v2 = hexvals.index(s[i*2+1])
+        c = (v1 << 4) + v2
+        assert 0 <= c < 256
+        r.append(chr(c))
+    return "".join(r)
+
 #----------------------------------------------------------------------
 import mixminion._minionlib as _ml
 
 class MinionlibCryptoTests(unittest.TestCase):
-    def hexread(self,s):
-        r = []
-        hexvals = "0123456789ABCDEF"
-        for i in range(len(s) // 2):
-            v1 = hexvals.index(s[i*2])
-            v2 = hexvals.index(s[i*2+1])
-            c = (v1 << 4) + v2
-            assert 0 <= c < 256
-            r.append(chr(c))
-        return "".join(r)
-
+    """Tests for cryptographic C extensions."""
     def test_sha1(self):
         s1 = _ml.sha1
 
+        # A test vector from the SHA1 spec
         self.assertEquals(s1("abc"),
-               self.hexread("A9993E364706816ABA3E25717850C26C9CD0D89D"))
+              hexread("A9993E364706816ABA3E25717850C26C9CD0D89D"))
 
+        # Another test vector from the SHA1 spec
         s = s1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
         self.assertEquals(s,
-               self.hexread("84983E441C3BD26EBAAE4AA1F95129E5E54670F1"))
+               hexread("84983E441C3BD26EBAAE4AA1F95129E5E54670F1"))
 
+        # Make sure that we fail gracefully on non-string input.
         self.failUnlessRaises(TypeError, s1, 1)
 
     def test_xor(self):
         xor = _ml.strxor
-        
+
+        # Try a few known-value XORs.
         self.assertEquals(xor("abc", "\000\000\000"), "abc")
         self.assertEquals(xor("abc", "abc"), "\000\000\000")
         self.assertEquals(xor("\xEF\xF0\x12", "\x11\x22\x35"), '\xFE\xD2\x27')
 
-        # Make sure that the C doesn't (cringe) modify the strings.
+        # Make sure that the C doesn't (cringe) modify the strings out from
+        # under us.
         a = "aaaa"
         self.assertEquals(xor(a,"\000\000\000a"), "aaa\000")
         self.assertEquals(a, "aaaa")
         self.assertEquals(xor("\000\000\000a",a), "aaa\000")
         self.assertEquals(a, "aaaa")
-        
+
+        # Check for error msg on XORing strings of unequal length.    
         self.failUnlessRaises(TypeError, xor, "a", "bb")
         
     def test_aes(self):
         crypt = _ml.aes_ctr128_crypt
 
-        # One of the test vectors from AES.
+        # First, try one of the test vectors from the AES spec.
         key = txt = "\x80" + "\x00" * 15
         key = _ml.aes_key(key)
-
-        expected = self.hexread("8EDD33D3C621E546455BD8BA1418BEC8")
+        expected = hexread("8EDD33D3C621E546455BD8BA1418BEC8")
         self.failUnless(crypt(key, txt, 0) == expected)
         self.failUnless(crypt(key, txt) == expected)
+        
+        # Now, make sure that the counter implementation is sane.
         self.failUnless(crypt(key, " "*100, 0)[1:] == crypt(key, " "*99, 1))
+        self.failUnless(crypt(key, " "*100, 0)[30:] == crypt(key, " "*70, 30))
+
+        # Counter mode is its own inverse
         self.failUnless(crypt(key,crypt(key, " "*100, 0),0) == " "*100)
 
+        # Try a different key to be sure.
         teststr = """I have seen the best ciphers of my generation
                      Destroyed by cryptanalysis, broken, insecure,
                      Implemented still in cryptographic libraries"""
-
         key2 = _ml.aes_key("xyzz"*4)
         self.assertEquals(teststr,crypt(key2,crypt(key2,teststr)))
 
-        # PRNG mode
-        expected2 = self.hexread("0EDD33D3C621E546455BD8BA1418BEC8")
+        # Try generating the same test vector again, but this time in PRNG
+        # mode
+        expected2 = hexread("0EDD33D3C621E546455BD8BA1418BEC8")
         self.assertEquals(expected2, crypt(key, "", 0, len(expected2)))
+        # PRNG mode ignores input.
         self.assertEquals(expected2, crypt(key, "Z", 0, len(expected2)))
+        # Try an offset with prng mode.
         self.assertEquals(expected2[5:], crypt(key, "", 5, len(expected2)-5))
+        # Make sure that PRNG mode with negative count yields ""
+        self.assertEquals("", crypt(key,"",0,-1))
 
-        # Failing cases
+        # Can't use a non-key object.
         self.failUnlessRaises(TypeError, crypt, "a", teststr)
-        self.failUnlessRaises(TypeError, crypt, "a"*17, teststr)
-
-        self.assertEquals("", crypt(key,"",0,-1))
+        # Can't make a key from a short string...
+        self.failUnlessRaises(TypeError, _ml.aes_key, "a")
+        # ...or a long string.
+        self.failUnlessRaises(TypeError, _ml.aes_key, "a"*17)
 
     def test_openssl_seed(self):
+        # Just try seeding openssl a couple of times, and make sure it
+        # doesn't crash.
         _ml.openssl_seed("Hello")
         _ml.openssl_seed("")
 
     def test_oaep(self):
+        strxor = _ml.strxor
+        # Check_oaep inverts add_oaep successfully.
         x = _ml.add_oaep_padding("A", "B", 128)
+
         self.assertEquals("A",_ml.check_oaep_padding(x, "B", 128))
-        
+
+        # 86 bytes can be used with size=128
         _ml.add_oaep_padding("A"*86, "B",128)
+        # But 300 is too much,
         self.failUnlessRaises(TypeError,
                               _ml.add_oaep_padding,"A"*300, "B", 128)
+        # And so is even 87.
         self.failUnlessRaises(_ml.SSLError,
                               _ml.add_oaep_padding,"A"*87, "B", 128)
+        # Changing a character at the beginning keeps it from checking.
+        ch = strxor(x[0], '\x01')
         self.failUnlessRaises(_ml.SSLError,
-                              _ml.check_oaep_padding,x[1:]+"Y","B",128)
+                              _ml.check_oaep_padding,ch+x[1:],"B",128)
+        # Changing a character at the end keeps it from checking.
+        ch = strxor(x[-1], '\x01')
         self.failUnlessRaises(_ml.SSLError,
-                              _ml.check_oaep_padding,x[:-1]+"Y","B",128)
+                              _ml.check_oaep_padding,x[:-1]+ch,"B",128)
 
     def test_rsa(self):
         p = _ml.rsa_generate(1024, 65535)
         def sslerr(*args): self.failUnlessRaises(_ml.SSLError, *args)
 
+        #For all of SIGN, CHECK_SIG, ENCRYPT, DECRYPT...
         for pub1 in (0,1):
             for enc1 in (0,1):
                 msg = "Now is the time for all anonymous parties"
                 x = _ml.add_oaep_padding(msg, "B", 128)
                 x2 = _ml.rsa_crypt(p, x, pub1, enc1);
+                # ...Encryption inverts decryption...
                 x3 = _ml.rsa_crypt(p, x2, [1,0][pub1], [1,0][enc1]);
                 self.failUnless(x3 == x)
+                # ...And oaep is preserved.
                 x4 = _ml.check_oaep_padding(x3, "B", 128)
                 self.failUnless(x4 == msg)
 
-        # Too short
+        # Fail if there is not enough padding
         self.failUnlessRaises(_ml.SSLError,_ml.rsa_crypt,p,"X",1,1)
-        # Too long
+        # Fail if there is too much padding
         self.failUnlessRaises(_ml.SSLError,_ml.rsa_crypt,p,x+"ZZZ",1,1)
 
+        ####
+        # Test key encoding
         padhello = _ml.add_oaep_padding("Hello", "B", 128)
         for public in (0,1):
+            #encode(decode(encode(x))) == x.
             x = _ml.rsa_encode_key(p,public)
             p2 = _ml.rsa_decode_key(x,public)
             x3 = _ml.rsa_encode_key(p2,public)
             self.assertEquals(x,x3)
+            # decode(encode(x)) encrypts the same as x.
             self.assertEquals(_ml.rsa_crypt(p,padhello,public,1),
                               _ml.rsa_crypt(p2,padhello,public,1))
 
+        # encoding public keys to/from their moduli.
+        self.assertEquals(_ml.rsa_get_modulus_bytes(p),1024/8)
         n,e = _ml.rsa_get_public_key(p)
         p2 = _ml.rsa_make_public_key(n,e)
         self.assertEquals((n,e), _ml.rsa_get_public_key(p2))
         self.assertEquals(65535,e)
         self.assertEquals(_ml.rsa_encode_key(p,1), _ml.rsa_encode_key(p,1))
         
-        # Try private-key ops with public key
+        # Try private-key ops with public key p3.
         p3 = _ml.rsa_decode_key(_ml.rsa_encode_key(p,1),1)
         msg1 = _ml.rsa_crypt(p, padhello, 1,1)
         msg2 = _ml.rsa_crypt(p, padhello, 1,1)
@@ -147,10 +185,13 @@
 from mixminion.Crypto import *
 
 class CryptoTests(unittest.TestCase):
+    """Tests for Python cryptographic library"""
     def test_initcrypto(self):
         init_crypto()
 
     def test_wrappers(self):
+        # Test simple wrappers over _minionlib functionality.  Mainly, just
+        # test that _ml.foo and foo do the same thing.
         self.assertEquals(_ml.sha1("xyzzy"), sha1("xyzzy"))
         k = _ml.aes_key("xyzy"*4)
         self.assertEquals(_ml.aes_ctr128_crypt(k,"hello",0),
@@ -162,6 +203,7 @@
         self.assertEquals(prng(k,100,0),prng(k,50,0)+prng(k,50,50))
 
     def test_rsa(self):
+
         eq = self.assertEquals
         k512 = pk_generate(512)
         k1024 = pk_generate()
@@ -169,9 +211,12 @@
         eq(512/8, _ml.rsa_get_modulus_bytes(k512))
         eq(1024/8, _ml.rsa_get_modulus_bytes(k1024))
 
+        # Check pk_get_modulus sanity
         self.failUnless((1L<<511) < pk_get_modulus(k512) < (1L<<513))
         self.failUnless((1L<<1023) < pk_get_modulus(k1024) < (1L<<1024))
 
+        # Make sure that public keys can be made from moduli, and used to
+        # encrypt and decrypt.
         msg="Good hello"
         pub512 = pk_from_modulus(pk_get_modulus(k512))
         pub1024 = pk_from_modulus(pk_get_modulus(k1024))
@@ -181,22 +226,28 @@
         eq(msg, pk_decrypt(pk_encrypt(msg, k1024),k1024))
         eq(msg, pk_decrypt(pk_encrypt(msg, pub1024),k1024))
 
+        # Make sure that CH_OAEP(RSA( )) inverts pk_encrypt.
         eq(msg, _ml.check_oaep_padding(
                     _ml.rsa_crypt(k512, pk_encrypt(msg,k512), 0, 0),
                     mixminion.Crypto.OAEP_PARAMETER, 64))
 
+        # Make sure we can still encrypt after we've encoded/decoded a
+        # key.
         encoded = pk_encode_private_key(k512)
         decoded = pk_decode_private_key(encoded)
         eq(msg, pk_decrypt(pk_encrypt(msg, pub512),decoded))
         
     def test_trng(self):
+        # Make sure that the true rng is at least superficially ok.
         self.assertNotEquals(trng(40), trng(40))
 
     def test_lioness(self):
         enc = lioness_encrypt
         dec = lioness_decrypt
+
+        # Check basic cipher properties.
         key = ("ABCDE"*4,) *4
-        plain = mixminion.Crypto.OAEP_PARAMETER*100
+        plain = "The more it snows the more it goes on snowing"*10
         self.assertNotEquals(plain, enc(plain,key))
         self.assertNotEquals(plain, dec(plain,key))
         self.assertEquals(len(plain), len(enc(plain,key)))
@@ -204,7 +255,24 @@
         self.assertEquals(plain, dec(enc(plain,key),key))
         self.assertEquals(plain, enc(dec(plain,key),key))
 
-        #XXXX check for correct values
+        # Walk through a LIONESS encryption to check for correct values.
+        # Check getLionessKeys too.
+        s = "ABCDE"*4
+        key1 = sha1(s+"foo")
+        key2 = key1[:-1]+strxor(key1[-1], chr(1))
+        key3 = key1[:-1]+strxor(key1[-1], chr(2))
+        key4 = key1[:-1]+strxor(key1[-1], chr(3))
+        
+        left = plain[:20]
+        right = plain[20:]
+        right = ctr_crypt( right, sha1(key1+left+key1)[:16] )
+        left  = strxor(left, sha1(key2+right+key2)) 
+        right = ctr_crypt( right, sha1(key3+left+key3)[:16] )
+        left  = strxor(left, sha1(key4+right+key4)) 
+
+        key = (key1,key2,key3,key4)
+        self.assertEquals(left+right, lioness_encrypt(plain,key))
+        self.assertEquals(key, Keyset("ABCDE"*4).getLionessKeys("foo"))
 
     def test_keyset(self):
         s = sha1
@@ -220,6 +288,7 @@
             k.getLionessKeys("Baz"))
 
     def test_aesprng(self):
+        # Make sure that AESCounterPRNG is really repeatable.
         key ="aaaa"*4
         PRNG = AESCounterPRNG(key)
         self.assert_(prng(key,100000) == (
@@ -294,10 +363,18 @@
         self.assertEquals(s.pack(), expected)
         self.assertEquals(s.getExtraBlocks(), extra)
 
-        #XXXX Need failing tests, routinginfo tests.
+        self.failUnlessRaises(ParseError,
+                              parseSubheader, "a"*(41))
 
     def test_headers(self):
-        pass #XXXX
+        header = ("abcdefghi"*(256))[: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:])
 
     def test_message(self):
         # 9 is relatively prime to all pwrs of 2.
@@ -307,16 +384,46 @@
         self.assert_(msg.header1 == m[:2048])
         self.assert_(msg.header2 == m[2048:4096])
         self.assert_(msg.payload == m[4096:])
-        # FAILING CASES XXXX
+        self.failUnlessRaises(ParseError, parseMessage, m[:-1])
+        self.failUnlessRaises(ParseError, parseMessage, m+"x")
             
     def test_ipv4info(self):
-        pass #XXXX
+        ri = hexread("12F400BCBBE30011223344556677889900112233445566778899")
+        inf = parseIPV4Info(ri)
+        self.assertEquals(inf.ip, "18.244.0.188")
+        self.assertEquals(inf.port, 48099)
+        self.assertEquals(inf.keyinfo, ri[-20:])
+        self.assertEquals(len(inf.pack()), 26)
+        self.assertEquals(inf.pack(), ri)
+        self.assertEquals(IPV4Info("18.244.0.188", 48099, ri[-20:]).pack(),
+                          ri)
 
-    def test_smtpinfo(self):
-        pass #XXXX
+        self.failUnlessRaises(ParseError, parseIPV4Info, ri[:-1])
+        self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
 
-    def test_localinfo(self):
-        pass #XXXX
+    def test_smtpinfolocalinfo(self):
+        for _class, _parse, _key in ((SMTPInfo, parseSMTPInfo, 'email'),
+                                     (LocalInfo, parseLocalInfo, 'user')):
+            ri = "no-such-user@wangafu.net\x00xyzzy"
+            inf = _parse(ri)
+            self.assertEquals(getattr(inf,_key), "no-such-user@wangafu.net")
+            self.assertEquals(inf.tag, "xyzzy")
+            self.assertEquals(inf.pack(), ri)
+            inf = _class("no-such-user@wangafu.net","xyzzy")
+            self.assertEquals(inf.pack(), ri)
+            # No tag
+            ri = "no-such-user@wangafu.net"
+            inf = _parse(ri)
+            self.assertEquals(inf.tag, None)
+            self.assertEquals(getattr(inf,_key), 'no-such-user@wangafu.net')
+            self.assertEquals(inf.pack(), ri)
+            # NUL in tag
+            ri = "no-such-user@wangafu.net\x00xyzzy\x00plover"
+            inf = _parse(ri)
+            self.assertEquals(getattr(inf,_key), "no-such-user@wangafu.net")
+            self.assertEquals(inf.tag, "xyzzy\x00plover")
+            self.assertEquals(inf.pack(), ri)
+        
         
 #----------------------------------------------------------------------
 from mixminion.HashLog import HashLog
@@ -392,26 +499,37 @@
 
 #----------------------------------------------------------------------
 import mixminion.BuildMessage
+from mixminion.Modules import *
 
 class FakePRNG:
     def getBytes(self,n):
         return "\x00"*n
 
-class BuildMessageTests(unittest.TestCase):
-    def setUp(self):
-        from ServerInfo import ServerInfo
+class _BMTSupport:
+    def __init__(self):
+        # We do this trick to keep from re-generating the keypairs
+        # for every message test.
         self.pk1 = pk_generate()
         self.pk2 = pk_generate()
         self.pk3 = pk_generate()
-        self.n_1 = pk_get_modulus(self.pk1)
-        self.n_2 = pk_get_modulus(self.pk2)
-        self.n_3 = pk_get_modulus(self.pk3)
-        self.server1 = ServerInfo("127.0.0.1", 1, self.n_1, "X"*20)
-        self.server2 = ServerInfo("127.0.0.2", 3, self.n_2, "Z"*20)
-        self.server3 = ServerInfo("127.0.0.3", 5, self.n_3, "Q"*20)
+
+BMTSupport = _BMTSupport()
+
+class BuildMessageTests(unittest.TestCase):
+    def setUp(self):
+        from mixminion.ServerInfo import ServerInfo
+        self.pk1 = BMTSupport.pk1
+        self.pk2 = BMTSupport.pk2
+        self.pk3 = BMTSupport.pk3
+        n_1 = pk_get_modulus(self.pk1)
+        n_2 = pk_get_modulus(self.pk2)
+        n_3 = pk_get_modulus(self.pk3)
+        self.server1 = ServerInfo("127.0.0.1", 1, n_1, "X"*20)
+        self.server2 = ServerInfo("127.0.0.2", 3, n_2, "Z"*20)
+        self.server3 = ServerInfo("127.0.0.3", 5, n_3, "Q"*20)        
 
     def test_buildheader_1hop(self):
-        bhead = mixminion.BuildMessage._buildHeaders
+        bhead = mixminion.BuildMessage._buildHeader
 
         head = bhead([self.server1], ["9"*16], 99, "Hi mom", AESCounterPRNG())
         self.do_header_test(head,
@@ -421,7 +539,7 @@
                             ("Hi mom",))
 
     def test_buildheader_2hops(self):
-        bhead = mixminion.BuildMessage._buildHeaders
+        bhead = mixminion.BuildMessage._buildHeader
         # 2 hops
         head = bhead([self.server1, self.server2],
                      ["9"*16, "1"*16], 99, "Hi mom", AESCounterPRNG()) 
@@ -430,43 +548,93 @@
         self.do_header_test(head,
                             (self.pk1, self.pk2),
                             ["9"*16, "1"*16],
-                            (mixminion.Modules.FWD_TYPE, 99),
+                            (FWD_TYPE, 99),
                             (ipv4("127.0.0.2",3,"Z"*20).pack(),
                              "Hi mom"))
                             
     def test_buildheader_3hops(self):
-        bhead = mixminion.BuildMessage._buildHeaders
+        bhead = mixminion.BuildMessage._buildHeader
         # 3 hops
         secrets = ["9"*16, "1"*16, "z"*16]
         head = bhead([self.server1, self.server2, self.server3], secrets,
                       99, "Hi mom", AESCounterPRNG())
         pks = (self.pk1,self.pk2,self.pk3)
-        rtypes = (mixminion.Modules.FWD_TYPE,
-                  mixminion.Modules.FWD_TYPE,
-                  99)
+        rtypes = (FWD_TYPE, FWD_TYPE, 99)
         rinfo = (mixminion.Formats.IPV4Info("127.0.0.2", 3, "Z"*20).pack(),
                  mixminion.Formats.IPV4Info("127.0.0.3", 5, "Q"*20).pack(),
                  "Hi mom")
         self.do_header_test(head, pks, secrets, rtypes, rinfo)
 
     def do_header_test(self, head, pks, secrets, rtypes, rinfo):
+        retsecrets = []
+        retinfo = []
+        if secrets == None:
+            secrets = [ None ] * len(pks)
         self.assertEquals(len(head), mixminion.Formats.HEADER_LEN)
         for pk, secret, rt, ri in zip(pks, secrets,rtypes,rinfo):
             subh = mixminion.Formats.parseSubheader(pk_decrypt(head[:128], pk))
-            self.assertEquals(subh.secret, secret)
+            if secret:
+                self.assertEquals(subh.secret, secret)
+            else:
+                secret = subh.secret
+                retsecrets.append(secret)
             self.assertEquals(subh.major, mixminion.Formats.MAJOR_NO)
             self.assertEquals(subh.minor, mixminion.Formats.MINOR_NO)
-            self.assertEquals(subh.routingtype, rt)
-            self.assertEquals(subh.routinginfo, ri)
+            
             self.assertEquals(subh.digest, sha1(head[128:]))
+            self.assertEquals(subh.routingtype, rt)
             ks = Keyset(secret)
             key = ks.get(HEADER_SECRET_MODE)
             prngkey = ks.get(RANDOM_JUNK_MODE)
-            head = ctr_crypt(head[128:]+prng(prngkey,128), key)
+            if not subh.isExtended():
+                if ri:
+                    self.assertEquals(subh.routinginfo, ri)
+                    self.assertEquals(subh.routinglen, len(ri))
+                else:
+                    retinfo.append(subh.routinginfo)
+                size = 128
+                n = 0
+            else:
+                self.assert_(len(ri) > mixminion.Formats.MAX_ROUTING_INFO_LEN)
+                n = subh.getNExtraBlocks()
+                size = (1+n)*128
+                more = ctr_crypt(head[128:128+128*n], key)
+                subh.appendExtraBlocks(more)
+                if ri:
+                    self.assertEquals(subh.routinginfo, ri)
+                    self.assertEquals(subh.routinglen, len(ri))
+                else:
+                    retinfo.append(subh.routinginfo)
+
+            head = ctr_crypt(head[size:]+prng(prngkey,size), key, 128*n)
+
+        if retinfo:
+            return retsecrets, retinfo
+        else:
+            return retsecrets
 
     def test_extended_routinginfo(self):
-        #XXXX!!!! Code doesn't work 
-        pass
+        bhead = mixminion.BuildMessage._buildHeader
+        bhead_impl = mixminion.BuildMessage._buildHeader_impl
+        secrets = ["9"*16 ]
+        longStr = "Foo"*50
+        head = bhead([self.server1 ], secrets, 99, longStr, AESCounterPRNG())
+        pks = (self.pk1,)
+        rtypes = (99,)
+        rinfo = (longStr,)
+
+        self.do_header_test(head, pks, secrets, rtypes, rinfo)
+
+        secrets.append("1"*16)
+        longStr2 = longStr*2
+
+        head = bhead_impl([self.server1,self.server2], secrets,
+                          [ (99,longStr2) , (99,longStr) ], AESCounterPRNG())
+
+        pks = (self.pk1,self.pk2)
+        rtypes = (99,99)
+        rinfo = (longStr2,longStr)
+        self.do_header_test(head, pks, secrets, rtypes, rinfo)
 
     def test_constructmessage(self):
         consMsg = mixminion.BuildMessage._constructMessage
@@ -509,7 +677,8 @@
         self.assert_(head2 == h2)
         self.assert_(payload == pld)
 
-        #### Reply case
+        ######
+        ### Reply case
         message = consMsg(secrets1, None, h1, h2, pld)
         self.assertEquals(len(message), mixminion.Formats.MESSAGE_LEN)
         msg = mixminion.Formats.parseMessage(message)
@@ -527,21 +696,278 @@
 
         self.assert_(head2 == h2)
         self.assert_(payload == pld)
+
+    def do_message_test(self, msg,
+                        header_info_1, #XXXX doc
+                        header_info_2,
+                        payload):
+        # Check header 1, and get secrets
+        sec = self.do_header_test(msg[:2048], *header_info_1)
+        h2 = msg[2048:4096]
+        p = msg[4096:]
+        # Do decryption steps for header 1.
+        for s in sec:
+            ks = Keyset(s)
+            p = lioness_decrypt(p,ks.getLionessKeys(PAYLOAD_ENCRYPT_MODE))
+            h2 = lioness_decrypt(h2,ks.getLionessKeys(HEADER_ENCRYPT_MODE))
+        h2 = lioness_decrypt(h2,mixminion.Crypto.lioness_keys_from_payload(p))
+
+        sec = self.do_header_test(h2, *header_info_2)
+        for s in sec:
+            ks = Keyset(s)
+            p = lioness_decrypt(p,ks.getLionessKeys(PAYLOAD_ENCRYPT_MODE))
+
+        # XXXX Need to do something about size encoding.
+        self.assertEquals(payload, p[:len(payload)])
+
         
-    def test_buildmessage(self):
-        #XXXX
-        pass
+    def test_build_fwd_message(self):
+        bfm = mixminion.BuildMessage.buildForwardMessage
+        payload = "Hello"
+        
+        m = bfm(payload, 99, "Goodbye",
+                [self.server1, self.server2],
+                [self.server3, self.server2])
+
+        self.do_message_test(m,
+                             ( (self.pk1, self.pk2), None,
+                               (FWD_TYPE, SWAP_FWD_TYPE),
+                               (self.server2.getRoutingInfo().pack(),
+                                self.server3.getRoutingInfo().pack()) ),
+                             ( (self.pk3, self.pk2), None,
+                               (FWD_TYPE, 99),
+                               (self.server2.getRoutingInfo().pack(),
+                                "Goodbye") ),
+                             "Hello" )
+
+        m = bfm(payload, 99, "Goodbye",
+                [self.server1, ],
+                [self.server3, ])
+
+        self.do_message_test(m,
+                             ( (self.pk1,), None,
+                               (SWAP_FWD_TYPE, ),
+                               ( self.server3.getRoutingInfo().pack(), ) ),
+                             ( (self.pk3, ), None,
+                               (99,),
+                               ("Goodbye",) ),
+                             "Hello" )
 
     def test_buildreply(self):
-        #XXXX
-        pass
+        brb = mixminion.BuildMessage.buildReplyBlock
+        bsrb = mixminion.BuildMessage.buildStatelessReplyBlock
+        brm = mixminion.BuildMessage.buildReplyMessage
 
-    def test_buildstatelessreply(self):
-        #XXXX
-        pass
+        ## Stateful reply blocks.
+        (rb, node1), secrets = \
+             brb([ self.server3, self.server1, self.server2,
+                   self.server1, self.server3 ],
+                 SMTP_TYPE,
+                 SMTPInfo("no-such-user@invalid", None).pack())
+        pks_1 = (self.pk3, self.pk1, self.pk2, self.pk1, self.pk3)
+        infos = (self.server1.getRoutingInfo().pack(),
+                 self.server2.getRoutingInfo().pack(),
+                 self.server1.getRoutingInfo().pack(),
+                 self.server3.getRoutingInfo().pack())
+
+        self.assert_(node1 is self.server3)
+                
+        m = brm("Information?",
+                [self.server3, self.server1],
+                (rb,node1))
+
+        self.do_message_test(m,
+                             ((self.pk3, self.pk1), None,
+                              (FWD_TYPE,SWAP_FWD_TYPE),
+                              (self.server1.getRoutingInfo().pack(),
+                               self.server3.getRoutingInfo().pack())),
+                             (pks_1, secrets,
+                              (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,SMTP_TYPE),
+                              infos+(
+                               SMTPInfo("no-such-user@invalid",None).pack(),
+                               )),
+                             "Information?")
+                                   
+        ## Stateless replies
+        rb,node1 = bsrb([ self.server3, self.server1, self.server2,
+                          self.server1, self.server3 ],
+                        "fred", "Galaxy Far Away.", 1)
+
+        sec,(loc,) = self.do_header_test(rb, pks_1, None,
+                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,SMTP_TYPE),
+                            infos+(None,))
+        s = "fred\x00RTRN"
+        self.assert_(loc.startswith(s))
+        seed = ctr_crypt(loc[len(s):], "Galaxy Far Away.")
+        prng = AESCounterPRNG(seed)
+        self.assert_(sec == [ prng.getBytes(16) for i in range(5)])
+
+        ## Stateless reply, no user key (trusted server)
+        rb,node1 = bsrb([ self.server3, self.server1, self.server2,
+                          self.server1, self.server3 ],
+                        "fred" )
+        sec,(loc,) = self.do_header_test(rb, pks_1, None,
+                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,LOCAL_TYPE),
+                                         infos+(None,))
+        self.assert_(loc.startswith(s))
+        seed = loc[len(s):]
+        prng = AESCounterPRNG(seed)
+        self.assert_(sec == [ prng.getBytes(16) for i in range(5)])
             
 #----------------------------------------------------------------------
-import mixminion.ServerProcess
+# Having tested BuildMessage without using ServerProcess, we can now use
+# BuildMessage to see whether ServerProcess is doing the right thing.
+#
+# (of course, we still need to build failing messages by hand)
+
+class ServerProcessTests(unittest.TestCase):
+    def setUp(self):
+        from mixminion.ServerProcess import ServerProcess
+        from mixminion.ServerInfo import ServerInfo
+        from tempfile import mktemp
+        self.pk1 = BMTSupport.pk1
+        self.pk2 = BMTSupport.pk2
+        self.pk3 = BMTSupport.pk3
+        self.tmpfile = mktemp(".db")
+        h = self.hlog = HashLog(self.tmpfile, "Z"*20)
+        n_1 = pk_get_modulus(self.pk1)
+        n_2 = pk_get_modulus(self.pk2)
+        n_3 = pk_get_modulus(self.pk3)
+        self.server1 = ServerInfo("127.0.0.1", 1, n_1, "X"*20)
+        self.server2 = ServerInfo("127.0.0.2", 3, n_2, "Z"*20)
+        self.server3 = ServerInfo("127.0.0.3", 5, n_3, "Q"*20)
+        self.sp1 = ServerProcess(self.pk1, h, None, None)
+        self.sp2 = ServerProcess(self.pk2, h, None, None)
+        self.sp3 = ServerProcess(self.pk3, h, None, None)
+
+    def tearDown(self):
+        import os
+        try:
+            os.unlink(self.tmpfile)
+        except:
+            pass
+
+    def do_test_chain(self, m, sps, routingtypes, routinginfo, payload):
+        for sp, rt, ri in zip(sps,routingtypes,routinginfo):
+            res = sp._processMessage(m)
+            self.assertEquals(len(res), 2)
+            if rt in (FWD_TYPE, SWAP_FWD_TYPE):
+                self.assertEquals(res[0], "QUEUE")
+                self.assertEquals(res[1][0].pack(), ri)
+                self.assertEquals(FWD_TYPE, rt)
+                m = res[1][1]
+            else:
+                self.assertEquals(res[0], "EXIT")
+                self.assertEquals(res[1][0], rt)
+                self.assertEquals(res[1][1], ri)
+                # XXXX TEST application key
+                self.assert_(res[1][3].startswith(payload))
+                break
+
+    def test_successful(self):
+        bfm = mixminion.BuildMessage.buildForwardMessage
+        # A two-hop/one-hop message.
+        p = "Now is the time for all good men to come to the aid"
+        m = bfm(p, SMTP_TYPE, "nobody@invalid",
+                [self.server1, self.server2], [self.server3])
+
+        self.do_test_chain(m,
+                           [self.sp1,self.sp2,self.sp3],
+                           [FWD_TYPE, FWD_TYPE, SMTP_TYPE],
+                           [self.server2.getRoutingInfo().pack(),
+                            self.server3.getRoutingInfo().pack(),
+                            "nobody@invalid"],
+                           p)
+
+        # A one-hop/one-hop message.
+        m = bfm(p, SMTP_TYPE, "nobody@invalid",
+                [self.server1], [self.server3])
+
+        self.do_test_chain(m,
+                           [self.sp1,self.sp3],
+                           [FWD_TYPE, SMTP_TYPE],
+                           [self.server3.getRoutingInfo().pack(),
+                            "nobody@invalid"],
+                           p)
+
+        # A 3/3 message with a long exit header.
+        for i in (100,300):
+            longemail = "f"*i+"@invalid"
+            m = bfm(p, SMTP_TYPE, longemail,
+                    [self.server1, self.server2, self.server1],
+                    [self.server3, self.server1, self.server2])
+            
+            self.do_test_chain(m,
+                               [self.sp1,self.sp2,self.sp1,
+                                self.sp3,self.sp1,self.sp2],
+                               [FWD_TYPE,FWD_TYPE,FWD_TYPE,
+                                FWD_TYPE,FWD_TYPE,SMTP_TYPE],
+                               [self.server2.getRoutingInfo().pack(),
+                                self.server1.getRoutingInfo().pack(),
+                                self.server3.getRoutingInfo().pack(),
+                                self.server1.getRoutingInfo().pack(),
+                                self.server2.getRoutingInfo().pack(),
+                                longemail],
+                               p)
+
+
+    def test_rejected(self):
+        bfm = mixminion.BuildMessage.buildForwardMessage
+        brm = mixminion.BuildMessage.buildReplyMessage
+        brb = mixminion.BuildMessage.buildReplyBlock
+        from mixminion.ServerProcess import ContentError
+
+        # A long intermediate header needs to fail.
+        from mixminion.ServerInfo import ServerInfo
+        server1X = ServerInfo("127.0.0.1", 1, pk_get_modulus(self.pk1), "X"*20)
+        class _packable:
+            def pack(self): return "x"*200
+        server1X.getRoutingInfo = lambda : _packable()
+        
+        m = bfm("Z", LOCAL_TYPE, "hello\000bye",
+                [self.server2, server1X, self.server3],
+                [server1X, self.server2, self.server3])
+        self.failUnlessRaises(ContentError, self.sp2._processMessage, m)
+        
+        # Duplicate messages need to fail.
+        m = bfm("Z", SMTP_TYPE, "nobody@invalid",
+                [self.server1, self.server2], [self.server3])
+        self.sp1._processMessage(m)
+        self.failUnlessRaises(ContentError, self.sp1._processMessage, m)
+
+        # Duplicate reply blocks need to fail
+        (r,n),s = brb([self.server3], SMTP_TYPE, "fred@invalid")
+        m = brm("Y", [self.server2], (r,n))
+        m2 = brm("Y", [self.server1], (r,n))
+        q, (a,m) = self.sp2._processMessage(m)
+        self.sp3._processMessage(m)
+        q, (a,m2) = self.sp1._processMessage(m2)
+        self.failUnlessRaises(ContentError, self.sp3._processMessage, m2)
+
+        # Even duplicate secrets need to go.
+        prng = AESCounterPRNG(" "*16)
+        (r1,n),s = brb([self.server1], SMTP_TYPE, "fred@invalid",prng)
+        prng = AESCounterPRNG(" "*16)
+        (r2,n),s = brb([self.server2], LOCAL_TYPE, "foo",prng)
+        m = brm("Y", [self.server3], (r1,n))
+        m2 = brm("Y", [self.server3], (r2,n))
+        q, (a,m) = self.sp3._processMessage(m)
+        self.sp1._processMessage(m)
+        q, (a,m2) = self.sp3._processMessage(m2)
+        self.failUnlessRaises(ContentError, self.sp2._processMessage, m2)
+
+        # Drop gets dropped.
+        m = bfm("Z", DROP_TYPE, "", [self.server2], [self.server2])
+        q, (a,m) = self.sp2._processMessage(m)
+        res = self.sp2._processMessage(m)
+        self.assertEquals(res,None)
+
+        # XXXX Bogus types
+        # XXXX Non-parsing information
+        # XXXX Bad Major or Minor
+        # XXXX Bad digest
+        # XXXX Bad payload
+
 #----------------------------------------------------------------------
 
 def testSuite():
@@ -553,6 +979,7 @@
     suite.addTest(tc(FormatTests))
     suite.addTest(tc(HashLogTests))
     suite.addTest(tc(BuildMessageTests))
+    suite.addTest(tc(ServerProcessTests))
     return suite
 
 def testAll():