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

[minion-cvs] More testing, docs: some bulletproofing, some bugfixing



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

Modified Files:
	BuildMessage.py Main.py Modules.py Packet.py test.py 
Log Message:
More testing, docs: some bulletproofing, some bugfixing

BuildMessage:
	- Bugfixing, slight doc improvement.
	- Some (not enough) zlib bulletproofing

Main:
	- More verboseness on import failure

Modules:
	- Start of e2e work for exit modules

Packet:
	- Loads of docs; more symbolic consts

test:
	- More tests for e2e message encoding
	- Cache RSA keys and avoid DH retesting to speed tests by a factor 
          of 3 to 4.
	- Make 'Gravity's Rainbow' example more explicit

aes_ctr.c:
	- Remove ancient if-0'd coded

crypt.c:
	- Bulletproof rsa_generate to withstandd silly values of n,e.
	



Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- BuildMessage.py	14 Oct 2002 03:03:42 -0000	1.15
+++ BuildMessage.py	16 Oct 2002 23:12:11 -0000	1.16
@@ -12,13 +12,13 @@
 import mixminion.Crypto as Crypto
 import mixminion.Modules as Modules
 
-__all__ = [ 'Address', 
+__all__ = [ 'Address',
            'buildForwardMessage', 'buildEncryptedMessage', 'buildReplyMessage',
            'buildStatelessReplyBlock', 'buildReplyBlock', 'decodePayload',
-	   'decodeForwardPayload', 'decodeEncryptedForwardPayload', 
+	   'decodeForwardPayload', 'decodeEncryptedForwardPayload',
 	   'decodeReplyPayload', 'decodeStatelessReplyPayload' ]
 
-def buildForwardMessage(payload, exitType, exitInfo, path1, path2, 
+def buildForwardMessage(payload, exitType, exitInfo, path1, path2,
 			paddingPRNG=None):
     """Construct a forward message.
             payload: The payload to deliver.  Must compress to under 28K-22b.
@@ -28,17 +28,17 @@
             path2: Sequence of ServerInfo objects for the 2nd leg of the path
 	    paddingPRNG
 
-        Note: If either path is empty, the message is vulnerable to tagging 
+        Note: If either path is empty, the message is vulnerable to tagging
          attacks! (FFFF we should check this.)
     """
     if paddingPRNG is None: paddingPRNG = Crypto.AESCounterPRNG()
 
     payload = _encodePayload(payload, 0, paddingPRNG)
     tag = _getRandomTag(paddingPRNG)
-    exitInfo = tag + exitInfo 
+    exitInfo = tag + exitInfo
     return _buildMessage(payload, exitType, exitInfo, path1, path2,paddingPRNG)
 
-def buildEncryptedForwardMessage(payload, exitType, exitInfo, path1, path2, 
+def buildEncryptedForwardMessage(payload, exitType, exitInfo, path1, path2,
 				 key, paddingPRNG=None, secretRNG=None):
     """XXXX
     """
@@ -63,7 +63,7 @@
     payload = encrypted + lionessPart
     tag = payload[:TAG_LEN]
     payload = payload[TAG_LEN:]
-    exitInfo = tag + exitInfo 
+    exitInfo = tag + exitInfo
     assert len(payload) == 28*1024
     return _buildMessage(payload, exitType, exitInfo, path1, path2,paddingPRNG)
 
@@ -74,7 +74,7 @@
     if paddingPRNG is None: paddingPRNG = Crypto.AESCounterPRNG()
 
     payload = _encodePayload(payload, 0, paddingPRNG)
-   
+
     # XXXX Document this mode
     k = Crypto.Keyset(replyBlock.encryptionKey).getLionessKeys(
 	                 Crypto.PAYLOAD_ENCRYPT_MODE)
@@ -84,11 +84,11 @@
     return _buildMessage(payload, None, None,
                          path1=path1, path2=replyBlock)
 
-def buildReplyBlock(path, exitType, exitInfo, expiryTime=0, secretPRNG=None, 
+def buildReplyBlock(path, exitType, exitInfo, expiryTime=0, secretPRNG=None,
                     tag=None):
     """Return a 3-tuple containing (1) a newly-constructed reply block, (2)
        a list of secrets used to make it, (3) a tag.
-       
+
               path: A list of ServerInfo
               exitType: Routing type to use for the final node
               exitInfo: Routing info for the final node, not including tag.
@@ -97,7 +97,7 @@
                  provided, uses an AES counter-mode stream seeded from our
                  entropy source.  Note: the secrets are generated so that they
                  will be used to encrypt the message in reverse order.
-              tag: If provided, a 159-bit tag.  If not provided, a new one 
+              tag: If provided, a 159-bit tag.  If not provided, a new one
                  is generated.
        """
     if secretPRNG is None:
@@ -115,7 +115,7 @@
     if tag is None:
 	tag = _getRandomTag(secretPRNG)
 
-    header = _buildHeader(path, headerSecrets, exitType, tag+exitInfo, 
+    header = _buildHeader(path, headerSecrets, exitType, tag+exitInfo,
                           paddingPRNG=Crypto.AESCounterPRNG())
 
     return ReplyBlock(header, expiryTime,
@@ -123,7 +123,7 @@
                       path[0].getRoutingInfo().pack(), sharedKey), secrets, tag
 
 # Maybe we shouldn't even allow this to be called with userKey==None.
-def buildStatelessReplyBlock(path, exitType, exitInfo, userKey, 
+def buildStatelessReplyBlock(path, exitType, exitInfo, userKey,
 			     expiryTime=0, secretRNG=None):
     """XXXX DOC IS NOW WRONG HERE
        Construct a 'stateless' reply block that does not require the
@@ -146,7 +146,7 @@
        """
     #XXXX Out of sync with the spec.
     if secretRNG is None: secretRNG = Crypto.AESCounterPRNG()
-    
+
     while 1:
 	seed = _getRandomTag(secretRNG)
 	if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00':
@@ -164,17 +164,20 @@
         Contract: return payload on success; raise MixError on certain failure,
           return None if neither.
     """
+    if len(payload) != PAYLOAD_LEN or len(tag) != TAG_LEN:
+	raise MixError("Wrong payload or tag length")
+
     if _checkPayload(payload):
 	return decodeForwardPayload(payload)
 
-    if storedKeysFn is not None:
+    if storedKeys is not None:
 	secrets = storedKeys.get(tag)
 	if secrets is not None:
 	    del storedKeys[tag]
 	    return decodeReplyPayload(payload, secrets)
 
     if userKey is not None:
-	if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00': 
+	if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
 	    try:
 		return decodeStatelessReplyPayload(payload, tag, userKey)
 	    except MixError, _:
@@ -189,10 +192,12 @@
 
 def decodeForwardPayload(payload):
     "XXXX"
-    return _decodePayload(payload)
+    return decodePayloadImpl(payload)
 
 def decodeEncryptedForwardPayload(payload, tag, key):
     "XXXX"
+    assert len(tag) == TAG_LEN
+    assert len(payload) == PAYLOAD_LEN
     msg = tag+payload
     try:
 	rsaPart = Crypto.pk_decrypt(msg[:key.get_modulus_bytes()], key)
@@ -202,7 +207,7 @@
     #XXXX magic string
     k = Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys("End-to-end encrypt")
     rest = rsaPart[SECRET_LEN:] + Crypto.lioness_decrypt(rest, k)
-    return _decodePayload(rest)
+    return decodePayloadImpl(rest)
 
 def decodeReplyPayload(payload, secrets, check=0):
     "XXXX"
@@ -213,14 +218,14 @@
 	if check and _checkPayload(payload):
 	    break
 
-    return _decodePayload(payload)
+    return decodePayloadImpl(payload)
 
 def decodeStatelessReplyPayload(payload, tag, userKey):
     "XXXX"
     seed = Crypto.sha1(tag+userKey+"Generate")[:16]
     prng = Crypto.AESCounterPRNG(seed)
     secrets = [ prng.getBytes(SECRET_LEN) for _ in xrange(17) ]
-			 
+			
     return decodeReplyPayload(payload, secrets, check=1)
 
 #----------------------------------------------------------------------
@@ -244,12 +249,12 @@
 
     The following fields are optional:
        paddingPRNG: A pseudo-random number generator used to pad the headers.
-         If not provided, we use a counter-mode AES stream seeded from our 
-         entropy source. 
-         
+         If not provided, we use a counter-mode AES stream seeded from our
+         entropy source.
+
        paranoia: If this is false, we use the padding PRNG to generate
          header secrets too.  Otherwise, we read all of our header secrets
-         from the true entropy source. 
+         from the true entropy source.
     """
     assert len(payload) == PAYLOAD_LEN
     reply = None
@@ -279,7 +284,7 @@
 
     # Generate secrets for path1.
     secrets1 = [ secretRNG.getBytes(SECRET_LEN) for _ in path1 ]
-    
+
     if path2:
         # Make secrets for header 2, and construct header 2.  We do this before
         # making header1 so that our rng won't be used for padding yet.
@@ -308,14 +313,14 @@
     if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
         raise MixError("Too many nodes in path")
 
-    # Construct a list 'routing' of exitType, exitInfo.  
+    # Construct a list 'routing' of exitType, exitInfo.
     routing = [ (Modules.FWD_TYPE, node.getRoutingInfo().pack()) for
                 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]
-    
+
     # totalSize is the total number of blocks.
     totalSize = reduce(operator.add, sizes)
     if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
@@ -324,8 +329,8 @@
     # 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.  
+
+    # 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.
@@ -383,7 +388,7 @@
     """
     assert len(payload) == PAYLOAD_LEN
     assert len(header1) == len(header2) == HEADER_LEN
-    
+
     if secrets2:
         # (Copy secrets2 so we don't reverse the original)
         secrets2 = secrets2[:]
@@ -409,7 +414,7 @@
     secrets1 = secrets1[:]
 
     # Now, encrypt header2 and the payload for each node in path1, reversed.
-    secrets1.reverse()    
+    secrets1.reverse()
     for secret in secrets1:
         ks = Crypto.Keyset(secret)
         hkey = ks.getLionessKeys(Crypto.HEADER_ENCRYPT_MODE)
@@ -438,7 +443,7 @@
     paddingLen = PAYLOAD_LEN - SINGLETON_PAYLOAD_OVERHEAD - overhead - length
     if paddingLen < 0:
 	raise MixError("Payload too long for singleton message")
-    
+
     payload += paddingPRNG.getBytes(paddingLen)
 
     return SingletonPayload(length, Crypto.sha1(payload), payload).pack()
@@ -448,7 +453,7 @@
     b = ord(rng.getBytes(1)) & 0x7f
     return chr(b) + rng.getBytes(TAG_LEN-1)
 
-def _decodePayload(payload):
+def decodePayloadImpl(payload):
     if not _checkPayload(payload):
 	raise MixError("Hash doesn't match")
     payload = parsePayload(payload)
@@ -468,29 +473,52 @@
 _ZLIB_LIBRARY_OK = 0
 
 def compressData(payload):
-    """Given a string 'payload', compress it with zlib as specified in the
-       remailer spec."""
+    """Given a string 'payload', compress it with the 'deflate' method
+       as specified in the remailer spec and in RFC1951."""
     if not _ZLIB_LIBRARY_OK:
 	_validateZlib()
-    # Don't change any of these options; they're all mandated.  
+    # Don't change any of these options; they're all mandated.
     zobj = zlib.compressobj(zlib.Z_BEST_COMPRESSION, zlib.DEFLATED,
-			    zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 
+			    zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL,
 			    zlib.Z_DEFAULT_STRATEGY)
     s1 = zobj.compress(payload)
     s2 = zobj.flush()
-    return s1 + s2
+    s = s1 + s2
+
+    # Now we check the 2 bytes of zlib header.  Strictly speaking,
+    # these are irrelevant, as are the 4 bytes of adler-32 checksum at
+    # the end.  Still, we can afford 6 bytes per payload, and
+    # reconstructing the checksum to keep zlib happy is a bit of a pain.
+    # XXXX doc manditory '\x78\xDA' beginning in spec.
+    assert s[0] == '\x78' # deflate, 32K window
+    assert s[1] == '\xda' # no dict, max compression
+    return s
 
 def uncompressData(payload):
     """Uncompress a string 'payload'; raise ParseError if it is not valid
        compressed data."""
+    # XXXX ???? How to prevent zlib bombing?  Somebody could compress 28MB of
+    # XXXX ???? zero bytes down to fit in a single payload and use us to
+    # XXXX ???? mailbomb people, hard.
+    if len(payload) < 6 or payload[0:2] != '\x78\xDA':
+	raise ParseError("Invalid zlib header")
     try:
-	return zlib.decompress(payload)
+	# We can't just call zlib.decompress(payload), since we'll eventually
+	# want to limit the output size.
+	zobj = zlib.decompressobj(zlib.MAX_WBITS)
+	# Decompress the payload.
+	d = zobj.decompress(payload)
+	# Get any leftovers, which shouldn't exist.
+	nil = zobj.flush()
+	if nil != '':
+	    raise ParseError("Error in compressed data")
+	return d
     except zlib.error, _:
 	raise ParseError("Error in compressed data")
 
 def _validateZlib():
-    """Internal function:  Make sure that zlib is a recognized version, and 
-       that it compresses things as expected.  (This check is important, 
+    """Internal function:  Make sure that zlib is a recognized version, and
+       that it compresses things as expected.  (This check is important,
        because using a zlib version that compressed differently from zlib1.1.4
        would make senders partitionable by payload compression.)
     """
@@ -498,6 +526,8 @@
     ver = getattr(zlib, "ZLIB_VERSION")
     if ver and ver < "1.1.2":
 	raise MixFatalError("Zlib version %s is not supported"%ver)
+
+    _ZLIB_LIBRARY_OK = 0.5
     if ver in ("1.1.2", "1.1.3", "1.1.4"):
 	_ZLIB_LIBRARY_OK = 1
 	return
@@ -506,10 +536,10 @@
     # This test is inadequate, but it _might_ catch future incompatible
     # changes.
     _ZLIB_LIBRARY_OK = 0.5
-    good = 'x\xda\xed\xc6A\x11\x00 \x08\x00\xb0l\xd4\xf0\x87\x02\xf6o'+\
+    good = '\x78\xda\xed\xc6A\x11\x00 \x08\x00\xb0l\xd4\xf0\x87\x02\xf6o'+\
 	   '`\x0e\xef\xb6\xd7r\xed\x88S=7\xcd\xcc\xcc\xcc\xcc\xcc\xcc'+\
-	   '\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xbe\xdd\x03q'+\
-	   '\x8d\n\x93'
+	   '\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xbe\xdd\x03'+\
+	   'q\x8d\n\x93'
     if compressData("aZbAAcdefg"*1000) == good:
 	_ZLIB_LIBRARY_OK = 1
     else:

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- Main.py	31 Aug 2002 04:12:36 -0000	1.4
+++ Main.py	16 Oct 2002 23:12:12 -0000	1.5
@@ -64,6 +64,7 @@
     try:
 	import mixminion.Main as _
     except ImportError, _:
+	print >>sys.stderr, _
 	print >>sys.stderr,"Unable to find correct path for mixminion."
 	sys.exit(1)
 

Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Modules.py,v
retrieving revision 1.12
retrieving revision 1.13
diff -u -d -r1.12 -r1.13
--- Modules.py	10 Sep 2002 14:45:30 -0000	1.12
+++ Modules.py	16 Oct 2002 23:12:12 -0000	1.13
@@ -19,6 +19,7 @@
 import mixminion.Config
 import mixminion.Packet
 import mixminion.Queue
+import mixminion.BuildMessage 
 from mixminion.Config import ConfigError, _parseBoolean, _parseCommand
 from mixminion.Common import getLog, createPrivateDir, MixError
 
@@ -99,6 +100,7 @@
 	    DELIVER_FAIL_NORETRY (if the message shouldn't be tried later)."""
         raise NotImplementedError("processMessage")
 
+
 class ImmediateDeliveryQueue:
     """Helper class usable as delivery queue for modules that don't
        actually want a queue.  Such modules should have very speedy
@@ -270,7 +272,6 @@
             getLog().error("Unable to handle message with unknown type %s",
                            exitType)
 	    return
-	    
 	queue = self.queues[mod.getName()]
 	queue.queueMessage((exitType,exitInfo), message)
 
@@ -384,7 +385,7 @@
 	except KeyError, _:
             getLog.warn("Unknown MBOX user %r", info.user)
 
-        msg = _escapeMessageForEmail(message)
+        msg = _escapeMessageForEmail(message, info.tag)
 
         fields = { 'user': address,
                    'return': self.returnAddress,
@@ -424,9 +425,19 @@
 
 #----------------------------------------------------------------------
 
+#XXXX DOCDOC
 _allChars = "".join(map(chr, range(256)))
 _nonprinting = "".join(map(chr, range(0x00, 0x07)+range(0x0E, 0x20)))
-def _escapeMessageForEmail(msg):
+def _escapeMessageForEmail(msg, tag):
+    m = decodePayload(tag, msg)
+    if m is None:
+	junk = 1
+	msg = base64.encodestring(msg)
+	tag = base64.encodestring(tag)
+    else:
+	printable = m.translate(_allChars, _nonprinting)
+	allChars
+
     printable = msg.translate(_allChars, _nonprinting)
     if msg[len(printable):] == '\x00'*(len(msg)-len(printable)):
         msg = msg[len(printable)]
@@ -445,3 +456,33 @@
 %s
 ============ BASE-64 ENCODED ANONYMOUS MESSAGE ENDS\n""" % msg
 
+def _decodeMessage(self, message, exitInfo, text=0):
+    """XXXX DOCDOC
+	  -> ("TXT"|"BIN"|"ENC", payload, RI, tag|None) or None
+    """
+    if len(exitInfo) < 20:
+	return None
+    tag = exitInfo[:20]
+    ri = exitInfo[20:]
+
+    try:
+	m = mixminion.BuildMessage.decodePayload(message, tag)
+    except MixError, e:
+	return None
+
+    if m is None:
+	code = "ENC"
+    else:
+	tag = None
+	printable = msg.translate(_allChars, _nonprinting)
+	if msg[len(printable):] == '\x00'*(len(msg)-len(printable)):
+	    code = "TXT"
+	else:
+	    code = "BIN"
+
+    if text and (code != "TXT") :
+	message = base64.encodestring(message)
+    if text and tag:
+	tag = base64.encodestring(tag)
+
+    return code, message, ri, tag

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.12
retrieving revision 1.13
diff -u -d -r1.12 -r1.13
--- Packet.py	14 Oct 2002 03:03:42 -0000	1.12
+++ Packet.py	16 Oct 2002 23:12:12 -0000	1.13
@@ -30,15 +30,18 @@
 # Length of a single payload
 PAYLOAD_LEN = MESSAGE_LEN - HEADER_LEN*2
 
+# Bytes taken up by OAEP padding in RSA-encrypted data
+OAEP_OVERHEAD = 42
+
+# Length of a subheader, once RSA-encoded.
+ENC_SUBHEADER_LEN = 128
 # Smallest possible size for a subheader
 MIN_SUBHEADER_LEN = 42
-# Most information we can fit into a subheader
-MAX_SUBHEADER_LEN = 86
+# 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
 MAX_ROUTING_INFO_LEN = MAX_SUBHEADER_LEN - MIN_SUBHEADER_LEN
 
-# Length of a subheader, once RSA-encoded.
-ENC_SUBHEADER_LEN = 128
 # Length of a digest
 DIGEST_LEN = 20
 # Length of a secret key
@@ -107,7 +110,7 @@
            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 
+        if j < 0: j += 16
         return self.contents[i*ENC_SUBHEADER_LEN:
                              j*ENC_SUBHEADER_LEN]
 
@@ -237,19 +240,25 @@
 #----------------------------------------------------------------------
 # UNENCRYPTED PAYLOADS
 
-# XXXX DOCUMENT CONTENTS
+# Length of the 'MessageID' field in a fragment payload
 FRAGMENT_MESSAGEID_LEN = 20
+# Maximum number of fragments associated with a given message
 MAX_N_FRAGMENTS = 0x7ffff
 
-#XXXX DOCDOC
+# Number of bytes taken up by header fields in a singleton payload.
 SINGLETON_PAYLOAD_OVERHEAD = 2 + DIGEST_LEN
+# Number of bytes taken up by header fields in a fragment payload.
 FRAGMENT_PAYLOAD_OVERHEAD = 2 + DIGEST_LEN + FRAGMENT_MESSAGEID_LEN + 4
-OAEP_OVERHEAD = 42
-#XXXX DOC DOC  and e2e note is off by 4.
+# Number of bytes taken up from OAEP padding in an encrypted forward
+# payload, minus bytes saved by spilling the RSA-encrypted block into the
+# tag, minus the bytes taken by the session key.
+# XXXX (The e2e note is off by 4.)
 ENC_FWD_OVERHEAD = OAEP_OVERHEAD - TAG_LEN + SECRET_LEN
 
 def parsePayload(payload):
-    "XXXX"
+    """Convert a decoded mixminion payload into a SingletonPayload or a 
+       FragmentPayload object.  Raise ParseError on failure or data
+       corruption."""
     if len(payload) not in (PAYLOAD_LEN, PAYLOAD_LEN-ENC_FWD_OVERHEAD):
 	raise ParseError("Payload has bad length")
     bit0 = ord(payload[0]) & 0x80
@@ -258,13 +267,13 @@
 	idx, hash, msgID, msgLen = struct.unpack(FRAGMENT_UNPACK_PATTERN,
 					 payload[:FRAGMENT_PAYLOAD_OVERHEAD])
 	idx &= 0x7f
-	contents = payload[FRAGMENT_PAYLOAD_OVERHEAD:] 
+	contents = payload[FRAGMENT_PAYLOAD_OVERHEAD:]
 	if msgLen <= len(contents):
 	    raise ParseError("Payload has an invalid size field")
 	return FragmentPayload(idx,hash,msgID,msgLen,contents)
     else:
 	# We have a singleton
-	size, hash = struct.unpack(SINGLETON_UNPACK_PATTERN, 
+	size, hash = struct.unpack(SINGLETON_UNPACK_PATTERN,
 				   payload[:SINGLETON_PAYLOAD_OVERHEAD])
 	contents = payload[SINGLETON_PAYLOAD_OVERHEAD:]
 	if size > len(contents):
@@ -279,22 +288,24 @@
 FRAGMENT_UNPACK_PATTERN = "!H%ds%dsL" % (DIGEST_LEN, FRAGMENT_MESSAGEID_LEN)
 
 class SingletonPayload:
-    "XXXX"
+    """Represents the payload for a standalone mixminion message.
+       Fields:  size, hash, data.  (Note that data is padded.)"""
     def __init__(self, size, hash, data):
 	self.size = size
 	self.hash = hash
 	self.data = data
 
     def isSingleton(self):
-	"XXXX"
+	"""Returns true; this is a singleton payload."""
 	return 1
 
     def getContents(self):
-	"XXXX"
+	"""Returns the non-padding portion of this payload's data""" 
 	return self.data[:self.size]
 
     def pack(self):
-	"XXXX"
+	"""Check for reasonable values of fields, and return a packed payload.
+        """
 	assert (0x8000 & self.size) == 0
 	assert 0 <= self.size <= len(self.data)
 	assert len(self.hash) == DIGEST_LEN
@@ -304,7 +315,7 @@
 	return "%s%s" % (header, self.data)
 
 class FragmentPayload:
-    "XXXX"
+    """Represents the fields of a decoded fragment payload."""
     def __init__(self, index, hash, msgID, msgLen, data):
 	self.index = index
 	self.hash = hash
@@ -313,11 +324,11 @@
 	self.data = data
 
     def isSingleton(self):
-	"XXXX"
+	"""Return false; not a singleton"""
 	return 0
 
     def pack(self):
-	"XXXX"
+	"""Returns the string value of this payload."""
 	assert 0 <= self.index <= MAX_N_FRAGMENTS
 	assert len(self.hash) == DIGEST_LEN
 	assert len(self.msgID) == FRAGMENT_MESSAGEID_LEN
@@ -374,10 +385,10 @@
     def pack(self):
         """Returns the external representation of this reply block"""
         return struct.pack(RB_UNPACK_PATTERN,
-                           "SURB", 0x00, 0x01, self.timestamp, self.header, 
-                           len(self.routingInfo), self.routingType, 
+                           "SURB", 0x00, 0x01, self.timestamp, self.header,
+                           len(self.routingInfo), self.routingType,
 			   self.encryptionKey) + self.routingInfo
-                           
+
 #----------------------------------------------------------------------
 # Routing info
 
@@ -413,9 +424,9 @@
     def pack(self):
         """Return the routing info for this address"""
         assert len(self.keyinfo) == DIGEST_LEN
-        return struct.pack(IPV4_PAT, inet_aton(self.ip), 
+        return struct.pack(IPV4_PAT, inet_aton(self.ip),
 			   self.port, self.keyinfo)
-    
+
     def __hash__(self):
 	return hash(self.pack())
 

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- test.py	14 Oct 2002 03:03:42 -0000	1.30
+++ test.py	16 Oct 2002 23:12:12 -0000	1.31
@@ -26,12 +26,17 @@
 
 from mixminion.testSupport import mix_mktemp
 from mixminion.Common import MixError, MixFatalError, MixProtocolError, getLog
+import mixminion.Crypto as Crypto
 
 try:
     import unittest
 except ImportError:
     import mixminion._unittest as unittest
 
+# Set this flag to 1 in order to have all RSA keys and diffie-hellman params
+# generated independently.
+USE_SLOW_MODE = 0
+
 def hexread(s):
     assert (len(s) % 2) == 0
     r = []
@@ -71,6 +76,39 @@
     del log._storedHandlers
     return str(buf)
 
+# RSA key caching functionality
+_generated_rsa_keys = {}
+def getRSAKey(n,bits):
+    """Return the n'th of an arbitrary number of cached 'bits'-bit RSA keys,
+       generating them as necessary."""
+    try:
+	return _generated_rsa_keys[(n,bits)]
+    except KeyError, _:
+	if bits > 1024:
+	    print "[generating %d-bit key #%d..."%(bits,n),
+	    sys.stdout.flush()
+	k = _pk_generate_orig(bits)
+	if bits > 1024:
+	    print "done]",
+	    sys.stdout.flush()
+	_generated_rsa_keys[(n,bits)] = k
+	return k
+
+# Functions to override Crypto.pk_generate to avoid generating a zillion RSA
+# keys.
+_pk_generate_orig = Crypto.pk_generate
+_pk_generate_idx = 0
+def _pk_generate_replacement(bits=1024,e=65537):
+    if bits == 1024:
+	global _pk_generate_idx
+	_pk_generate_idx = (_pk_generate_idx + 1) % 4
+	return getRSAKey(_pk_generate_idx, bits)
+    else:
+	return getRSAKey(0, bits)
+
+if not USE_SLOW_MODE:
+    Crypto.pk_generate = _pk_generate_replacement
+
 #----------------------------------------------------------------------
 # Tests for common functionality
 
@@ -239,7 +277,7 @@
                               check,x[:-1]+ch,"B",128)
 
     def test_rsa(self):
-        p = _ml.rsa_generate(1024, 65537)
+        p = getRSAKey(1,1024)
 
         #for all of SIGN, CHECK_SIG, ENCRYPT, DECRYPT...
         for pub1 in (0,1):
@@ -303,17 +341,18 @@
         p.PEM_write_key(open(tf_enc,'w'), 0, "top sekrit")
         p2 = _ml.rsa_PEM_read_key(open(tf_pub, 'r'), 1)
         self.assertEquals(p.get_public_key(), p2.get_public_key())
-        
+
         p2 = _ml.rsa_PEM_read_key(open(tf_prv, 'r'), 0)
         self.assertEquals(p.encode_key(0), p2.encode_key(0))
 
         self.failUnlessRaises(_ml.CryptoError,
                               _ml.rsa_PEM_read_key,
                               open(tf_enc, 'r'), 0)
-                                  
+
         p2 = _ml.rsa_PEM_read_key(open(tf_prv, 'r'), 0, "top sekrit")
         self.assertEquals(p.encode_key(0), p2.encode_key(0))
-            
+
+
 #----------------------------------------------------------------------
 import mixminion.Crypto
 from mixminion.Crypto import *
@@ -339,8 +378,8 @@
     def test_rsa(self):
 
         eq = self.assertEquals
-        k512 = pk_generate(512)
-        k1024 = pk_generate()
+        k512 = getRSAKey(0,512)
+        k1024 = getRSAKey(0,1024)
 
         eq(512>>3, k512.get_modulus_bytes())
         eq(1024>>3, k1024.get_modulus_bytes())
@@ -386,7 +425,7 @@
 	# Test pickling
 	init_crypto()
 	s = cPickle.dumps(k512)
-	self.assertEquals(cPickle.loads(s).get_public_key(), 
+	self.assertEquals(cPickle.loads(s).get_public_key(),
 			  k512.get_public_key())
 
     def test_trng(self):
@@ -500,6 +539,7 @@
 	    s.sort()
 	    self.assertEquals(s, range(100))
 
+
 #----------------------------------------------------------------------
 import mixminion.Packet
 from mixminion.Packet import *
@@ -612,7 +652,7 @@
 
         self.failUnlessRaises(ParseError, parseIPV4Info, ri[:-1])
         self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
-    
+
 
     def test_smtpinfomboxinfo(self):
 	tag = "\x01\x02"+"Z"*18
@@ -719,6 +759,7 @@
 	self.failUnlessRaises(ParseError,parsePayload,bad_payload_1)
 	self.failUnlessRaises(ParseError,parsePayload,bad_payload_2)
 
+
 #----------------------------------------------------------------------
 from mixminion.HashLog import HashLog
 
@@ -781,6 +822,7 @@
 
         h[0].close()
 
+
 #----------------------------------------------------------------------
 import mixminion.BuildMessage as BuildMessage
 from mixminion.Modules import *
@@ -789,11 +831,6 @@
     def getBytes(self,n):
         return "\x00"*n
 
-class BMTSupport:
-    pk1 = pk_generate()
-    pk2 = pk_generate()
-    pk3 = pk_generate()
-    
 class FakeServerInfo:
     """Represents a Mixminion server, and the information needed to send
        messages to it."""
@@ -807,7 +844,7 @@
     def getPort(self): return self.port
     def getPacketKey(self): return self.key
     def getKeyID(self): return self.keyid
-    
+
     def getRoutingInfo(self):
         """Returns a mixminion.Packet.IPV4Info object for routing messages
            to this server."""
@@ -815,9 +852,10 @@
 
 class BuildMessageTests(unittest.TestCase):
     def setUp(self):
-        self.pk1 = BMTSupport.pk1
-        self.pk2 = BMTSupport.pk2
-        self.pk3 = BMTSupport.pk3
+        self.pk1 = getRSAKey(0,1024)
+        self.pk2 = getRSAKey(1,1024)
+        self.pk3 = getRSAKey(2,1024)
+	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)
         self.server3 = FakeServerInfo("127.0.0.3", 5, self.pk3, "Q"*20)
@@ -825,7 +863,7 @@
     def test_compression(self):
 	p = AESCounterPRNG()
 	longMsg = p.getBytes(100)*2 + str(dir(mixminion.Crypto))
-                     
+
 	# Make sure compression is reversible.
 	for m in ("", "a", "\000", "xyzzy"*10, ("glossy glossolalia.."*2)[32],
 		  longMsg):
@@ -861,11 +899,21 @@
 		comp = BuildMessage.compressData(m)
 		self.assert_(pld[22:].startswith(comp))
 		self.assertEquals(sha1(pld[22:]),pld[2:22])
+		self.assert_(BuildMessage._checkPayload(pld))
 		self.assertEquals(len(comp), ord(pld[0])*256+ord(pld[1]))
 		self.assertEquals(0, ord(pld[0])&0x80)
-		self.assertEquals(m, BuildMessage._decodePayload(pld))
+		self.assertEquals(m, BuildMessage.decodePayloadImpl(pld))
 
-	# XXXX Test failing cases
+	self.failUnlessRaises(MixError, BuildMessage.decodePayloadImpl, b)
+
+	# Check fragments (not yet supported)
+	pldFrag = chr(ord(pld[0])|0x80)+pld[1:]
+	self.failUnlessRaises(MixError, BuildMessage.decodePayloadImpl,pldFrag)
+
+	# Check impossibly long messages
+	pldSize = "\x7f\xff"+pld[2:] #sha1(pld[22:])+pld[22:]
+	self.failUnlessRaises(ParseError,
+			      BuildMessage.decodePayloadImpl,pldSize)
 
     def test_buildheader_1hop(self):
         bhead = BuildMessage._buildHeader
@@ -1090,13 +1138,15 @@
                         payload, decoder=None):
         """Decrypts the layers of a message one by one, checking them for
            correctness.
-	   XXXX Document decoder
                       msg: the message to examine
                       header_info_1: a tuple of (pks,secrets,rtypes,rinfo)
                             as used by do_header_test for the first header.
                       header_info_2: a tuple of (pks,secrets,rtypes,rinfo)
                             as used by do_header_test for the second header.
                       payload: The beginning of the expected decrypted payload.
+		      decoder: A function to call on the exit payload before
+		            comparing it to 'payload'.  Takes payload,tag;
+			    returns string.
            """
         # Check header 1, and get secrets
         sec = self.do_header_test(msg[:2048], *header_info_1)
@@ -1125,7 +1175,7 @@
     def test_build_fwd_message(self):
         bfm = BuildMessage.buildForwardMessage
         befm = BuildMessage.buildEncryptedForwardMessage
-        payload = "Hello"
+        payload = "Hello!!!!"
 
         m = bfm(payload, 500, "Goodbye",
                 [self.server1, self.server2],
@@ -1140,12 +1190,18 @@
                                (FWD_TYPE, 500),
                                (self.server2.getRoutingInfo().pack(),
                                 "Goodbye") ),
-                             "Hello")
+                             "Hello!!!!")
 
         m = bfm(payload, 500, "Goodbye",
                 [self.server1,],
                 [self.server3,])
 
+	messages = {}
+
+	def decoder0(p,t,messages=messages):
+	    messages['fwd'] = (p,t)
+	    return BuildMessage.decodeForwardPayload(p)
+
         self.do_message_test(m,
                              ( (self.pk1,), None,
                                (SWAP_FWD_TYPE,),
@@ -1153,16 +1209,19 @@
                              ( (self.pk3,), None,
                                (500,),
                                ("Goodbye",) ),
-                             "Hello")
+                             "Hello!!!!",
+			     decoder=decoder0)
 
 	# Encrypted forward message
-	rsa1, rsa2 = pk_generate(1024), pk_generate(2048)
+	rsa1, rsa2 = self.pk1, self.pk512
+	payload = "<<<<Hello>>>>" * 100
 	for rsakey in rsa1,rsa2:
-	    m = befm(payload, 500, "Phello", 
+	    m = befm(payload, 500, "Phello",
 		     [self.server1, self.server2],
 		     [self.server3, self.server2],
 		     rsakey)
-	    def decoder(p,t,key=rsakey):
+	    def decoder(p,t,key=rsakey,messages=messages):
+		messages['efwd'+str(key.get_modulus_bytes())] = (p,t)
 		return BuildMessage.decodeEncryptedForwardPayload(p,t,key)
 
 	    self.do_message_test(m,
@@ -1177,18 +1236,42 @@
 				 payload,
 				 decoder=decoder)
 
+	# Now do more tests on final messages: is the format as expected?
+	p,t = messages['fwd']
+	self.assertEquals(20, len(t))
+	self.assertEquals(28*1024,len(p))
+	self.assertEquals(0, ord(t[0]) & 0x80)
+	comp = BuildMessage.compressData("Hello!!!!")
+	self.assertEquals(len(comp), ord(p[0])*256 +ord(p[1]))
+	self.assert_(p[22:].startswith(comp))
+	self.assertEquals(sha1(p[22:]), p[2:22])
+
+	for rsakey in (rsa1, rsa2):
+	    n = rsakey.get_modulus_bytes()
+	    p,t = messages['efwd'+str(n)]
+	    mrsa, mrest = t+p[:n-20], p[n-20:]
+	    mrsa = pk_decrypt(mrsa, rsakey)
+	    sessionkey, rsa_rest = mrsa[:16], mrsa[16:]
+	    ks = Keyset(sessionkey)
+	    msg = rsa_rest + lioness_decrypt(mrest,
+			      ks.getLionessKeys("End-to-end encrypt"))
+	    comp = BuildMessage.compressData(payload)
+	    self.assert_(len(comp), ord(msg[0])*256 + ord(msg[1]))
+	    self.assertEquals(sha1(msg[22:]), msg[2:22])
+	    self.assert_(msg[22:].startswith(comp))
+
     def test_buildreply(self):
         brb = BuildMessage.buildReplyBlock
         bsrb = BuildMessage.buildStatelessReplyBlock
         brm = BuildMessage.buildReplyMessage
 
         ## Stateful reply blocks.
-        reply, secrets, tag = \
+        reply, secrets_1, tag_1 = \
              brb([self.server3, self.server1, self.server2,
                   self.server1, self.server3],
                  SMTP_TYPE,
 		 "no-such-user@invalid", tag=("+"*20))
-	hsecrets = secrets[:-1]
+	hsecrets = secrets_1[:-1]
 	hsecrets.reverse()
 
         pks_1 = (self.pk3, self.pk1, self.pk2, self.pk1, self.pk3)
@@ -1199,11 +1282,13 @@
 
         self.assert_(reply.routingInfo == self.server3.getRoutingInfo().pack())
 
-        m = brm("Information?",
+        m = brm("Information???",
                 [self.server3, self.server1],
                 reply)
 
-	def decoder(p,t,secrets=secrets):
+	messages = {}
+	def decoder(p,t,secrets=secrets_1,messages=messages):
+	    messages['repl'] = p,t
 	    return BuildMessage.decodeReplyPayload(p,secrets)
 
         self.do_message_test(m,
@@ -1214,12 +1299,12 @@
                              (pks_1, hsecrets,
                               (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,SMTP_TYPE),
                               infos+("no-such-user@invalid",)),
-                             "Information?",
+                             "Information???",
 			     decoder=decoder)
         ## Stateless replies
         reply = bsrb([self.server3, self.server1, self.server2,
                       self.server1, self.server3], MBOX_TYPE,
-                     "fred", "Galaxy Far Away", 0)
+                     "fred", "Tyrone Slothrop", 0)
 
         sec,(loc,) = self.do_header_test(reply.header, pks_1, None,
                             (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
@@ -1228,17 +1313,25 @@
 	self.assertEquals(loc[20:], "fred")
 
 	seed = loc[:20]
-	prng = AESCounterPRNG(sha1(seed+"Galaxy Far AwayGenerate")[:16])
+	prng = AESCounterPRNG(sha1(seed+"Tyrone SlothropGenerate")[:16])
 	sec.reverse()
 	self.assertEquals(sec, [ prng.getBytes(16) for _ in range(len(sec)) ])
 
-        m = brm("Information?  What's the world come to?",
+	# _Gravity's Rainbow_, page 258.
+	payload = '''
+              "...Is it any wonder the world's gone insane, with information
+	    come to the be the only medium of exchange?"
+	      "I thought it was cigarettes."
+	      "You dream."
+	          -- Gravity's Rainbow, p.258 ''' # " <- for emacs python-mode
+        m = brm(payload,
                 [self.server3, self.server1],
                 reply)
 
-	def decoder2(p,t):
+	def decoder2(p,t,messages=messages):
+	    messages['srepl'] = p,t
 	    return BuildMessage.decodeStatelessReplyPayload(p,t,
-							    "Galaxy Far Away")
+							 "Tyrone Slothrop")
         self.do_message_test(m,
                              ((self.pk3, self.pk1), None,
                               (FWD_TYPE,SWAP_FWD_TYPE),
@@ -1247,8 +1340,101 @@
                              (pks_1, None,
                               (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
                               infos+("fred",)),
-                             "Information?  What's the world come to?",
+			     payload,
 			     decoder=decoder2)
+
+	# Now test format of generated messages.
+	p,t = messages['repl']
+	self.assertEquals(t, tag_1)
+	for s in secrets_1:
+	    ks = Keyset(s)
+	    p = lioness_encrypt(p, ks.getLionessKeys(
+ 	 	               mixminion.Crypto.PAYLOAD_ENCRYPT_MODE))
+	comp = BuildMessage.compressData('Information???')
+	self.assertEquals(len(comp), ord(p[0])*256 +ord(p[1]))
+	self.assert_(p[22:].startswith(comp))
+	self.assertEquals(sha1(p[22:]), p[2:22])
+
+	p,t = messages['srepl']
+	self.assertEquals('\000', sha1(t+"Tyrone SlothropValidate")[-1])
+	prng = AESCounterPRNG(sha1(t+"Tyrone SlothropGenerate")[:16])
+	for _ in xrange(6): # 5 hops plus end-to-end
+	    s = prng.getBytes(16)
+	    ks = Keyset(s)
+	    p = lioness_encrypt(p, ks.getLionessKeys(
+		                      mixminion.Crypto.PAYLOAD_ENCRYPT_MODE))
+	comp = BuildMessage.compressData(payload)
+	self.assertEquals(len(comp), ord(p[0])*256 +ord(p[1]))
+	self.assert_(p[22:].startswith(comp))
+	self.assertEquals(sha1(p[22:]), p[2:22])
+
+    def test_decoding(self):
+	# Now we create a bunch of fake payloads and try to decode them.
+
+	# Successful messages:
+	payload = "All dreamers and sleepwalkers must pay the price, and "+\
+	  "even the invisible victim is responsible for the fate of all.\n"+\
+	  "   -- _Invisible Man_"
+
+	comp = BuildMessage.compressData(payload)
+	self.assertEquals(len(comp), 109)
+	encoded1 = (comp+ "RWE/HGW"*4096)[:28*1024-22]
+	encoded1 = '\x00\x6D'+sha1(encoded1)+encoded1
+	# Forward message.
+	self.assertEquals(payload, BuildMessage.decodeForwardPayload(encoded1))
+
+	# Encoded forward message
+	efwd = (comp+"RWE/HGW"*4096)[:28*1024-22-38]
+	efwd = '\x00\x6D'+sha1(efwd)+efwd
+	rsa1 = self.pk1
+	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)
+	efwd_t = efwd_rsa[:20]
+	efwd_p = efwd_rsa[20:]+efwd_lioness
+	self.assertEquals(payload,
+	     BuildMessage.decodeEncryptedForwardPayload(efwd_p,efwd_t,rsa1))
+
+	# Stateful reply
+	secrets = [ "Is that you, Des","troyer?Rinehart?" ]
+	sdict = { 'tag1'*5 : secrets }	
+	ks = Keyset(secrets[1])
+	m = lioness_decrypt(encoded1, ks.getLionessKeys(PAYLOAD_ENCRYPT_MODE))
+	ks = Keyset(secrets[0])
+	m = lioness_decrypt(m, ks.getLionessKeys(PAYLOAD_ENCRYPT_MODE))
+	self.assertEquals(payload, BuildMessage.decodeReplyPayload(m,secrets))
+	repl1 = m
+	
+	# Stateless reply
+	tag = "To light my way out\xBE"
+	passwd = "out I would have to burn every paper in the briefcase"
+	self.assertEquals('\000', sha1(tag+passwd+"Validate")[-1])
+	prng = AESCounterPRNG(sha1(tag+passwd+"Generate")[:16])
+	secrets2 = [ prng.getBytes(16) for _ in xrange(5) ]
+	m = encoded1
+	s = secrets2[:]
+	s.reverse()
+	for k in s:
+	    key = Keyset(k).getLionessKeys(PAYLOAD_ENCRYPT_MODE)
+	    m = lioness_decrypt(m,key)
+	self.assertEquals(payload, BuildMessage.decodeStatelessReplyPayload(m,tag,passwd))
+	repl2 = m
+	
+	# Okay, now let's try out 'decodePayload'.
+	decodePayload = BuildMessage.decodePayload
+	self.assertEquals(payload, 
+	  decodePayload(encoded1, "zzzz"*5, self.pk1, sdict, passwd))
+	self.assertEquals(payload, 
+	  decodePayload(efwd_p, efwd_t, self.pk1, sdict, passwd))
+	self.assert_(sdict)
+	self.assertEquals(payload, 
+	  decodePayload(repl1, "tag1"*5, self.pk1, sdict, passwd))
+	self.assert_(not sdict)
+	self.assertEquals(payload, 
+	  decodePayload(repl2, tag, self.pk1, sdict, passwd))
+	
+	# And now the failing cases
+	# XXXX TEST THESE
 	
 
 #----------------------------------------------------------------------
@@ -1260,9 +1446,9 @@
 class PacketHandlerTests(unittest.TestCase):
     def setUp(self):
         from mixminion.PacketHandler import PacketHandler
-        self.pk1 = BMTSupport.pk1
-        self.pk2 = BMTSupport.pk2
-        self.pk3 = BMTSupport.pk3
+        self.pk1 = getRSAKey(0,1024)
+        self.pk2 = getRSAKey(1,1024)
+        self.pk3 = getRSAKey(2,1024)
         self.tmpfile = mix_mktemp(".db")
         h = self.hlog = HashLog(self.tmpfile, "Z"*20)
 
@@ -1278,11 +1464,10 @@
         self.hlog.close()
 
     def do_test_chain(self, m, sps, routingtypes, routinginfo, payload,
-                      appkey=None, decoder=None):
+		      appkey=None):
         """Routes a message through a series of servers, making sure that
            each one decrypts it properly and routes it correctly to the
            next.
-	   XXXX DOC DECODER
                     m: the message to test
                     sps: sequence of PacketHandler objects for m's path
                     routingtypes: sequence of expected routingtype
@@ -1304,10 +1489,7 @@
                     self.assertEquals(appkey, res[1][2])
 		
 		p = res[1][3]
-		if decoder is None:
-		    p = BuildMessage.decodeForwardPayload(p)
-		else:
-		    p = decoder(p, res[1][1][:20])
+		p = BuildMessage.decodeForwardPayload(p)
                 self.assert_(p.startswith(payload))
                 break
 
@@ -1475,6 +1657,7 @@
         q, (a, m_x) = self.sp2.processMessage(m_x)
         self.failUnlessRaises(CryptoError, self.sp3.processMessage, m_x)
 
+
 #----------------------------------------------------------------------
 # QUEUE
 
@@ -1494,7 +1677,7 @@
         self.d1 = mix_mktemp("q1")
         self.d2 = mix_mktemp("q2")
         self.d3 = mix_mktemp("q3")
-        
+
     def testCreateQueue(self):
         # Nonexistent dir.
         self.failUnlessRaises(MixFatalError, Queue, self.d1)
@@ -1533,7 +1716,7 @@
         queue2 = Queue(self.d3, create=1)
 
         # Put 100 messages in queue1
-        handles = [queue1.queueMessage("Sample message %s" % i) 
+        handles = [queue1.queueMessage("Sample message %s" % i)
                    for i in range(100)]
         hdict = {}
         for i in range(100): hdict[handles[i]] = i
@@ -1578,7 +1761,7 @@
         for h in handles[30:60]:
             queue1.removeMessage(h)
         self.assertEquals(40, queue1.count())
-        
+
         # Make sure that smaller pickRandoms work.
         L1 = queue1.pickRandom(10)
         L2 = queue1.pickRandom(10)
@@ -1592,7 +1775,7 @@
         f.close()
         self.assertEquals(s, "Sample message 60")
 
-        # test successful 'openNewMessage' 
+        # test successful 'openNewMessage'
         f, h = queue1.openNewMessage()
         f.write("z"*100)
         self.failUnlessRaises(IOError, queue1.messageContents, h)
@@ -1620,9 +1803,9 @@
         # Scrub both queues.
         queue1.removeAll()
         queue2.removeAll()
-        queue1.cleanQueue()    
+        queue1.cleanQueue()
         queue2.cleanQueue()
-    
+
     def testDeliveryQueues(self):
 	d_d = mix_mktemp("qd")
 
@@ -1708,6 +1891,7 @@
 	bcmq.removeAll()
 	bcmq.cleanQueue()
 
+
 #---------------------------------------------------------------------
 # LOGGING
 class LogTests(unittest.TestCase):
@@ -1728,7 +1912,7 @@
         log.error("All your anonymity are belong to us")
         self.failUnless(buf.getvalue().endswith(
             "[ERROR] All your anonymity are belong to us\n"))
-        
+
 	buf.truncate(0)
 	try:
 	    raise MixError()
@@ -1756,6 +1940,7 @@
         self.assertEquals(open(t).read().count("\n") , 1)
         self.assertEquals(open(t1).read().count("\n"), 3)
 
+
 #----------------------------------------------------------------------
 # File paranoia
 from mixminion.Common import createPrivateDir, checkPrivateDir
@@ -1810,7 +1995,7 @@
 	    self.failUnlessRaises(MixFatalError, checkPrivateDir, subdir)
 	    os.chown(noia, 0, os.getgid())
 	else:
-	    # We're not root.  We can't reliably find or make a directory 
+	    # We're not root.  We can't reliably find or make a directory
 	    # that's non-root and non-us.  Let's just make sure we don't
 	    # own temp.
 	    if os.path.exists("/tmp"):
@@ -1866,6 +2051,7 @@
 # SIGHANDLERS
 # FFFF Write tests here
 
+
 #----------------------------------------------------------------------
 # MMTP
 # FFFF Write more tests
@@ -1888,23 +2074,25 @@
             pkfile = f+"_pk"
             certfile = f+"_cert"
             dh_fname = os.environ.get("MM_TEST_DHPARAMS", None)
-            if dh_fname:
+            if dh_fname and not USE_SLOW_MODE:
                 dhfile = dh_fname
                 if not os.path.exists(dh_fname):
 		    print "[Generating DH parameters...",
 		    sys.stdout.flush()
 		    _ml.generate_dh_parameters(dhfile, 0)
-		    print "done.]"
+		    print "done.]",
+		    sys.stdout.flush()
             else:
 		print "[Generating DH parameters (not caching)...",
 		sys.stdout.flush()
                 _ml.generate_dh_parameters(dhfile, 0)
-		print "done.]"
-            pk = _ml.rsa_generate(1024, 65537)
+		print "done.]",
+		sys.stdout.flush()
+            pk = getRSAKey(3,1024)
             pk.PEM_write_key(open(pkfile, 'w'), 0)
             _ml.generate_cert(certfile, pk, "Testing certificate",
                               time.time(), time.time()+365*24*60*60)
-            
+
 	pk = _ml.rsa_PEM_read_key(open(pkfile, 'r'), 0)
         return _ml.TLSContext_new(certfile, pk, dhfile)
     else:
@@ -1926,7 +2114,7 @@
         listener.register(server)
         pk = _ml.rsa_PEM_read_key(open(pkfile, 'r'), public=0)
         keyid = sha1(pk.encode_key(1))
-        
+
         return server, listener, messagesIn, keyid
 
 class MMTPTests(unittest.TestCase):
@@ -1950,12 +2138,12 @@
 
     def testNonblockingTransmission(self):
         self.doTest(self._testNonblockingTransmission)
-    
+
     def _testBlockingTransmission(self):
         server, listener, messagesIn, keyid = _getMMTPServer()
         self.listener = listener
         self.server = server
-        
+
         messages = ["helloxxx"*4096, "helloyyy"*4096]
 
         server.process(0.1)
@@ -1982,7 +2170,7 @@
         while t.isAlive():
             server.process(0.1)
         t.join()
-        
+
 
     def _testNonblockingTransmission(self):
         server, listener, messagesIn, keyid = _getMMTPServer()
@@ -1992,16 +2180,16 @@
         messages = ["helloxxx"*4096, "helloyyy"*4096]
         async = mixminion.MMTPServer.AsyncServer()
         clientcon = mixminion.MMTPServer.MMTPClientConnection(
-           _getTLSContext(0), "127.0.0.1", TEST_PORT, keyid, messages[:], 
+           _getTLSContext(0), "127.0.0.1", TEST_PORT, keyid, messages[:],
 	   [None, None], None)
         clientcon.register(async)
         def clientThread(clientcon=clientcon, async=async):
             while not clientcon.isShutdown():
                 async.process(2)
-            
+
         server.process(0.1)
         t = threading.Thread(None, clientThread)
-        
+
         t.start()
         while len(messagesIn) < 2:
             server.process(0.1)
@@ -2020,7 +2208,7 @@
         def clientThread(clientcon=clientcon, async=async):
             while not clientcon.isShutdown():
                 async.process(2)
-            
+
         severity = getLog().getMinSeverity()
         try:
 	    suspendLog() # suppress warning
@@ -2033,7 +2221,8 @@
             t.join()
         finally:
             resumeLog()  #unsuppress warning
-                    
+
+
 #----------------------------------------------------------------------
 # Config files
 
@@ -2077,13 +2266,13 @@
 Bar bar
 Baz:
   baz
-  and more baz 
-  and more baz  
+  and more baz
+  and more baz
 [Sec2]
 
 # Comment
 Bap +
-Quz 99 99  
+Quz 99 99
 
 
 Fob=1
@@ -2098,7 +2287,7 @@
 IntRS=5
 
   """
-        
+
         f = TCF(string=longerString)
         self.assertEquals(f['Sec1']['Foo'], 'abcde f')
         self.assertEquals(f['Sec1']['Bar'], 'bar')
@@ -2119,7 +2308,7 @@
             "IntAMD: 10\nIntRS: 5\n\n"))
         # Test file input
         fn = mix_mktemp()
-        
+
         file = open(fn, 'w')
         file.write(longerString)
         file.close()
@@ -2144,7 +2333,7 @@
         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
         file = open(fn, 'w')
         file.write(shorterString)
@@ -2227,7 +2416,7 @@
 	self.assertEquals(time.gmtime(tm)[:6], (2000,1,1,0,0,0))
 	tm = C._parseTime("25/12/2001 06:15:10")
 	self.assertEquals(time.gmtime(tm)[:6], (2001,12,25,6,15,10))
-        
+
         def fails(fn, val, self=self):
             self.failUnlessRaises(ConfigError, fn, val)
 
@@ -2316,12 +2505,8 @@
 Nickname: fred-the-bunny
 """
 
-_IDENTITY_KEY = None
 def _getIdentityKey():
-    global _IDENTITY_KEY
-    if _IDENTITY_KEY is None:
-	_IDENTITY_KEY = mixminion.Crypto.pk_generate(2048)
-    return _IDENTITY_KEY
+    return getRSAKey(0,2048)
 
 import mixminion.Config
 import mixminion.ServerInfo
@@ -2355,7 +2540,7 @@
         eq(info['Server']['Contact'], "a@b.c")
         eq(info['Server']['Comments'],
            "This is a test of the emergency broadcast system")
-        
+
         eq(info['Incoming/MMTP']['Version'], "0.1")
         eq(info['Incoming/MMTP']['Port'], 48099)
         eq(info['Incoming/MMTP']['Protocols'], "0.1")
@@ -2403,7 +2588,7 @@
 							     d,
 							     "key2",
 							     d)
-        
+
         # Now with a bad signature
         sig2 = mixminion.Crypto.pk_sign(sha1("Hello"), identity)
         sig2 = base64.encodestring(sig2).replace("\n", "")
@@ -2421,7 +2606,8 @@
         self.failUnlessRaises(ConfigError,
                               mixminion.ServerInfo.ServerInfo,
                               None, badSig)
-        
+
+
 #----------------------------------------------------------------------
 # Modules annd ModuleManager
 from mixminion.Modules import *
@@ -2438,7 +2624,7 @@
     def getName(self):
 	return "TestModule"
     def getConfigSyntax(self):
-	return { "Example" : { "Foo" : ("REQUIRE", 
+	return { "Example" : { "Foo" : ("REQUIRE",
 					mixminion.Config._parseInt, None) } }
     def validateConfig(self, cfg, entries, lines, contents):
 	if cfg['Example'] is not None:
@@ -2519,10 +2705,10 @@
 	manager.queueMessage("Hello 1", 1234, "fail!")
 	manager.queueMessage("Hello 2", 1234, "fail?")
 	manager.queueMessage("Hello 3", 1234, "good")
-	manager.queueMessage("Drop very much", 
+	manager.queueMessage("Drop very much",
 			     mixminion.Modules.DROP_TYPE,  "")
 	queue = manager.queues['TestModule']
-	self.failUnless(isinstance(queue, 
+	self.failUnless(isinstance(queue,
 			   mixminion.Modules.SimpleModuleDeliveryQueue))
 	self.assertEquals(3, queue.count())
 	self.assertEquals(exampleMod.processedMessages, [])
@@ -2549,9 +2735,9 @@
 	finally:
             getLog().setMinSeverity(severity) #unsuppress warning
 
-	# 
+	#
 	# Try again, this time with the test module disabled.
-	# 
+	#
 	cfg_test = SERVER_CONFIG_SHORT + """
 Homedir = %s
 ModulePath = %s
@@ -2584,10 +2770,11 @@
 	
 	# FFFF Add tests for catching exceptions from buggy modules
 
+
 #----------------------------------------------------------------------
 import mixminion.ServerMain
 
-#XXXX DOC
+# Sample server configuration to test ServerMain.
 SERVERCFG = """
 [Server]
 Homedir: %(home)s
@@ -2612,26 +2799,19 @@
 	resumeLog()
     return mixminion.ServerMain.ServerKeyring(conf)
 
-_IDENTITY_KEY = None
-def _getIdentityKey():
-    global _IDENTITY_KEY
-    if _IDENTITY_KEY is None:
-	_IDENTITY_KEY = _getKeyring().getIdentityKey()
-    return _IDENTITY_KEY
-
 class ServerMainTests(unittest.TestCase):
     def testServerKeyring(self):
 	keyring = _getKeyring()
 	home = _FAKE_HOME
 
 	# Test creating identity key
-	identity = _getIdentityKey()
+	identity = keyring.getIdentityKey()
 	fn = os.path.join(home, "keys", "identity.key")
 	identity2 = mixminion.Crypto.pk_PEM_load(fn)
 	self.assertEquals(mixminion.Crypto.pk_get_modulus(identity),
 			  mixminion.Crypto.pk_get_modulus(identity2))
 	# (Make sure warning case can occur.)
-	pk = _ml.rsa_generate(128, 65537)
+	pk = getRSAKey(0,128)
 	mixminion.Crypto.pk_PEM_save(pk, fn)
 	suspendLog()
 	keyring.getIdentityKey()
@@ -2672,15 +2852,14 @@
 	self.assertEquals(4, len(keyring.keyIntervals))
 	keyring.removeDeadKeys()
 	self.assertEquals(3, len(keyring.keyIntervals))
-	getLog().info("foo")
 	
-	if 0:
+	if USE_SLOW_MODE:
 	    # These are slow, since they regenerate the DH params.
 	    # Test getDHFile
 	    f = keyring.getDHFile()
 	    f2 = keyring.getDHFile()
 	    self.assertEquals(f, f2)
-	    
+	
 	    # Test getTLSContext
 	    keyring.getTLSContext()
 
@@ -2707,7 +2886,6 @@
 	dc = mixminion.ClientMain.DirectoryCache(dirname)
 	dc.load()
 	
-
 #----------------------------------------------------------------------
 def testSuite():
     suite = unittest.TestSuite()
@@ -2718,8 +2896,6 @@
 	suite.addTest(tc(BuildMessageTests))
 	return suite
 
-    suite.addTest(tc(ClientMainTests))
-    suite.addTest(tc(ServerMainTests))
     suite.addTest(tc(MiscTests))
     suite.addTest(tc(MinionlibCryptoTests))
     suite.addTest(tc(CryptoTests))
@@ -2732,10 +2908,14 @@
     suite.addTest(tc(PacketHandlerTests))
     suite.addTest(tc(QueueTests))
 
+    suite.addTest(tc(ClientMainTests))
+    suite.addTest(tc(ServerMainTests))
+
     # These tests are slowest, so we do them last.
     suite.addTest(tc(ModuleManagerTests))
     suite.addTest(tc(ServerInfoTests))
     suite.addTest(tc(MMTPTests))
+
     return suite
 
 def testAll(name, args):