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

[minion-cvs] Last round (I hope) of code tweaks before 0.0.1. Only ...



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

Modified Files:
	HashLog.py MMTPServer.py Modules.py PacketHandler.py Queue.py 
	ServerMain.py 
Added Files:
	ServerConfig.py ServerKeys.py 
Log Message:
Last round (I hope) of code tweaks before 0.0.1.  Only system tests and docs remain!

Refactoring:
	- Move _base64 from ServerInfo to formatBase64 in Common
	- Finish separating server code.
		- Move ServerConfig to new file
		- Move Server key mgt to new file
	- Don't warn on missing overwrite; we now override.
	- Minimize calls to true RNG

Everywhere:
	- Remove extraneous whitespace
	- Wrap overlong lines
	- Remove extraneous import statements
	- Give all modules sane __all__ fields
	- Make Pychecker pleased.



--- NEW FILE: ServerConfig.py ---
# Copyright 2002 Nick Mathewson.  See LICENSE for licensing information.
# $Id: ServerConfig.py,v 1.1 2002/12/12 19:56:47 nickm Exp $

"""Configuration format for server configuration files.

   See Config.py for information about the generic configuration facility."""

__all__ = [ "ServerConfig" ]

import mixminion.Config
import mixminion.server.Modules
from mixminion.Config import ConfigError
from mixminion.Common import LOG

class ServerConfig(mixminion.Config._ConfigFile):
    ##
    # Fields:
    #   moduleManager
    #
    _restrictFormat = 0
    def __init__(self, fname=None, string=None, moduleManager=None):
	# We use a copy of SERVER_SYNTAX, because the ModuleManager will
	# mess it up.
        self._syntax = SERVER_SYNTAX.copy()

	if moduleManager is None:
	    self.moduleManager = mixminion.server.Modules.ModuleManager()
	else:
	    self.moduleManager = moduleManager
        self._addCallback("Server", self.__loadModules)

        mixminion.Config._ConfigFile.__init__(self, fname, string)

    def validate(self, sections, entries, lines, contents):
	mixminion.Config._validateHostSection(sections.get('Host', {}))
	# Server section
	server = sections['Server']
	bits = server['IdentityKeyBits']
	if not (2048 <= bits <= 4096):
	    raise ConfigError("IdentityKeyBits must be between 2048 and 4096")
	if server['EncryptIdentityKey']:
	    LOG.warn("Identity key encryption not yet implemented")
	if server['EncryptPrivateKey']:
	    LOG.warn("Encrypted private keys not yet implemented")
	if server['PublicKeyLifetime'][2] < 24*60*60:
	    raise ConfigError("PublicKeyLifetime must be at least 1 day.")
	if server['PublicKeySloppiness'][2] > 20*60:
	    raise ConfigError("PublicKeySloppiness must be <= 20 minutes.")
	if [e for e in entries['Server'] if e[0]=='Mode']:
	    LOG.warn("Mode specification is not yet supported.")

	if not sections['Incoming/MMTP'].get('Enabled'):
	    LOG.warn("Disabling incoming MMTP is not yet supported.")
	if [e for e in entries['Incoming/MMTP'] if e[0] in ('Allow', 'Deny')]:
	    LOG.warn("Allow/deny are not yet supported")

	if not sections['Outgoing/MMTP'].get('Enabled'):
	    LOG.warn("Disabling incoming MMTP is not yet supported.")
	if [e for e in entries['Outgoing/MMTP'] if e[0] in ('Allow', 'Deny')]:
	    LOG.warn("Allow/deny are not yet supported")

        self.moduleManager.validate(sections, entries, lines, contents)

    def __loadModules(self, section, sectionEntries):
	"""Callback from the [Server] section of a config file.  Parses
	   the module options, and adds new sections to the syntax
	   accordingly."""
        self.moduleManager.setPath(section.get('ModulePath', None))
        for mod in section.get('Module', []):
	    LOG.info("Loading module %s", mod)
            self.moduleManager.loadExtModule(mod)

        self._syntax.update(self.moduleManager.getConfigSyntax())

    def getModuleManager(self):
	"Return the module manager initialized by this server."
	return self.moduleManager

# alias to make the syntax more terse.
C = mixminion.Config

SERVER_SYNTAX =  {
        'Host' : C.ClientConfig._syntax['Host'],
        'Server' : { '__SECTION__' : ('REQUIRE', None, None),
                     'Homedir' : ('ALLOW', None, "/var/spool/minion"),
                     'LogFile' : ('ALLOW', None, None),
                     'LogLevel' : ('ALLOW', C._parseSeverity, "WARN"),
                     'EchoMessages' : ('ALLOW', C._parseBoolean, "no"),
                     'EncryptIdentityKey' : ('REQUIRE', C._parseBoolean, "yes"),
		     'IdentityKeyBits': ('ALLOW', C._parseInt, "2048"),
                     'PublicKeyLifetime' : ('ALLOW', C._parseInterval,
                                            "30 days"),
                     'PublicKeySloppiness': ('ALLOW', C._parseInterval,
                                             "5 minutes"),
                     'EncryptPrivateKey' : ('REQUIRE', C._parseBoolean, "no"),
                     'Mode' : ('REQUIRE', C._parseServerMode, "local"),
                     'Nickname': ('ALLOW', None, None),
                     'Contact-Email': ('ALLOW', None, None),
                     'Comments': ('ALLOW', None, None),
                     'ModulePath': ('ALLOW', None, None),
                     'Module': ('ALLOW*', None, None),
                     },
        'DirectoryServers' : { 'ServerURL' : ('ALLOW*', None, None),
                               'Publish' : ('ALLOW', C._parseBoolean, "no"),
                               'MaxSkew' : ('ALLOW', C._parseInterval,
                                            "10 minutes",) },
	# FFFF Generic multi-port listen/publish options.
        'Incoming/MMTP' : { 'Enabled' : ('REQUIRE', C._parseBoolean, "no"),
			    'IP' : ('ALLOW', C._parseIP, "0.0.0.0"),
                            'Port' : ('ALLOW', C._parseInt, "48099"),
                          'Allow' : ('ALLOW*', C._parseAddressSet_allow, None),
                          'Deny' : ('ALLOW*', C._parseAddressSet_deny, None) },
        'Outgoing/MMTP' : { 'Enabled' : ('REQUIRE', C._parseBoolean, "no"),
                          'Allow' : ('ALLOW*', C._parseAddressSet_allow, None),
                          'Deny' : ('ALLOW*', C._parseAddressSet_deny, None) },
	# FFFF Missing: Queue-Size / Queue config options
	# FFFF         timeout options
	# FFFF         listen timeout??
	# FFFF         Retry options
	# FFFF         pool options
        }

--- NEW FILE: ServerKeys.py ---
# Copyright 2002 Nick Mathewson.  See LICENSE for licensing information.
# $Id: ServerKeys.py,v 1.1 2002/12/12 19:56:47 nickm Exp $

"""mixminion.ServerKeys

   Classes for servers to generate and store keys and server descriptors.
   """
#FFFF We need support for encrypting private keys.

__all__ = [ "ServerKeyring", "generateServerDescriptorAndKeys" ]

import bisect
import os
import socket
import sys
import time

import mixminion._minionlib
import mixminion.Crypto
import mixminion.server.HashLog
import mixminion.server.PacketHandler

from mixminion.ServerInfo import ServerInfo, PACKET_KEY_BYTES, signServerInfo
from mixminion.Common import LOG, MixError, MixFatalError, createPrivateDir, \
     formatBase64, formatDate, formatTime, previousMidnight, secureDelete


#----------------------------------------------------------------------
class ServerKeyring:
    """A ServerKeyring remembers current and future keys, descriptors, and
       hash logs for a mixminion server.

       FFFF We need a way to generate keys as needed, not just a month's
       FFFF worth of keys up front.
       """
    ## Fields:
    # homeDir: server home directory
    # keyDir: server key directory
    # keySloppiness: fudge-factor: how forgiving are we about key liveness?
    # keyIntervals: list of (start, end, keyset Name)
    # liveKey: list of (start, end, keyset name for current key.)
    # nextRotation: time_t when this key expires.
    # keyRange: tuple of (firstKey, lastKey) to represent which key names
    #      have keys on disk.

    ## Directory layout:
    #    MINION_HOME/work/queues/incoming/ [Queue of received,unprocessed pkts]
    #                             mix/ [Mix pool]
    #                             outgoing/ [Messages for mmtp delivery]
    #                             deliver/mbox/ []
    #                      tls/dhparam [Diffie-Hellman parameters]
    #                      hashlogs/hash_1*  [HashLogs of packet hashes
    #                               hash_2*    corresponding to key sets]
    #                                ...
    #                 log [Messages from the server]
    #                 keys/identity.key [Long-lived identity PK]
    #                      key_1/ServerDesc [Server descriptor]
    #                            mix.key [packet key]
    #                            mmtp.key [mmtp key]
    #                            mmtp.cert [mmmtp key x509 cert]
    #                      key_2/...
    #                 conf/miniond.conf [configuration file]
    #                       ....

    # FFFF Support to put keys/queues in separate directories.

    def __init__(self, config):
	"Create a ServerKeyring from a config object"
	self.configure(config)

    def configure(self, config):
	"Set up a SeverKeyring from a config object"
	self.config = config
	self.homeDir = config['Server']['Homedir']
	self.keyDir = os.path.join(self.homeDir, 'keys')
	self.hashDir = os.path.join(self.homeDir, 'work', 'hashlogs')
	self.keySloppiness = config['Server']['PublicKeySloppiness'][2]
	self.checkKeys()

    def checkKeys(self):
	"""Internal method: read information about all this server's
	   currently-prepared keys from disk."""
        self.keyIntervals = []
	firstKey = sys.maxint
	lastKey = 0

	LOG.debug("Scanning server keystore at %s", self.keyDir)

	if not os.path.exists(self.keyDir):
	    LOG.info("Creating server keystore at %s", self.keyDir)
	    createPrivateDir(self.keyDir)

	# Iterate over the entires in HOME/keys
        for dirname in os.listdir(self.keyDir):
	    # Skip any that aren't directories named "key_INT"
	    if not os.path.isdir(os.path.join(self.keyDir,dirname)):
		continue
            if not dirname.startswith('key_'):
		LOG.warn("Unexpected directory %s under %s",
			      dirname, self.keyDir)
                continue
            keysetname = dirname[4:]
	    try:
		setNum = int(keysetname)
		# keep trace of the first and last used key number
		if setNum < firstKey: firstKey = setNum
		if setNum > lastKey: lastKey = setNum
	    except ValueError:
		LOG.warn("Unexpected directory %s under %s",
			      dirname, self.keyDir)
		continue

	    # Find the server descriptor...
            d = os.path.join(self.keyDir, dirname)
            si = os.path.join(d, "ServerDesc")
            if os.path.exists(si):
                inf = ServerInfo(fname=si, assumeValid=1)
		# And find out when it's valid.
                t1 = inf['Server']['Valid-After']
                t2 = inf['Server']['Valid-Until']
                self.keyIntervals.append( (t1, t2, keysetname) )
		LOG.debug("Found key %s (valid from %s to %s)",
			       dirname, formatDate(t1), formatDate(t2))
	    else:
		LOG.warn("No server descriptor found for key %s"%dirname)

	# Now, sort the key intervals by starting time.
        self.keyIntervals.sort()
	self.keyRange = (firstKey, lastKey)

	# Now we try to see whether we have more or less than 1 key in effect
	# for a given time.
	for idx in xrange(len(self.keyIntervals)-1):
	    end = self.keyIntervals[idx][1]
	    start = self.keyIntervals[idx+1][0]
	    if start < end:
		LOG.warn("Multiple keys for %s.  That's unsupported.",
			      formatDate(end))
	    elif start > end:
		LOG.warn("Gap in key schedule: no key from %s to %s",
			      formatDate(end), formatDate(start))

	self.nextKeyRotation = 0 # Make sure that now > nextKeyRotation before
	                         # we call _getLiveKey()
	self._getLiveKey()       # Set up liveKey, nextKeyRotation.

    def getIdentityKey(self):
	"""Return this server's identity key.  Generate one if it doesn't
	   exist."""
	password = None # FFFF Use this, somehow.
	fn = os.path.join(self.keyDir, "identity.key")
	bits = self.config['Server']['IdentityKeyBits']
	if os.path.exists(fn):
	    key = mixminion.Crypto.pk_PEM_load(fn, password)
	    keylen = key.get_modulus_bytes()*8
	    if keylen != bits:
		LOG.warn(
		    "Stored identity key has %s bits, but you asked for %s.",
		    keylen, bits)
	else:
	    LOG.info("Generating identity key. (This may take a while.)")
	    key = mixminion.Crypto.pk_generate(bits)
	    mixminion.Crypto.pk_PEM_save(key, fn, password)
	    LOG.info("Generated %s-bit identity key.", bits)

	return key

    def removeIdentityKey(self):
        """Remove this server's identity key."""
        fn = os.path.join(self.keyDir, "identity.key")
        if not os.path.exists(fn):
            LOG.info("No identity key to remove.")
        else:
            LOG.warn("Removing identity key in 10 seconds")
            time.sleep(10)
            LOG.warn("Removing identity key")
            secureDelete([fn], blocking=1)

	dhfile = os.path.join(self.homeDir, 'work', 'tls', 'dhparam')
        if os.path.exists('dhfile'):
            LOG.info("Removing diffie-helman parameters file")
            secureDelete([dhfile], blocking=1)

    def createKeys(self, num=1, startAt=None):
	"""Generate 'num' public keys for this server. If startAt is provided,
           make the first key become valid at'startAt'.  Otherwise, make the
	   first key become valid right after the last key we currently have
	   expires.  If we have no keys now, make the first key start now."""
        # FFFF Use this.
	#password = None

	if startAt is None:
	    if self.keyIntervals:
		startAt = self.keyIntervals[-1][1]+60
	    else:
		startAt = time.time()+60

	startAt = previousMidnight(startAt)

	firstKey, lastKey = self.keyRange

	for _ in xrange(num):
	    if firstKey == sys.maxint:
		keynum = firstKey = lastKey = 1
	    elif firstKey > 1:
		firstKey -= 1
		keynum = firstKey
	    else:
		lastKey += 1
		keynum = lastKey

	    keyname = "%04d" % keynum

	    nextStart = startAt + self.config['Server']['PublicKeyLifetime'][2]

	    LOG.info("Generating key %s to run from %s through %s (GMT)",
		     keyname, formatDate(startAt),
		     formatDate(nextStart-3600))
 	    generateServerDescriptorAndKeys(config=self.config,
					    identityKey=self.getIdentityKey(),
					    keyname=keyname,
					    keydir=self.keyDir,
					    hashdir=self.hashDir,
					    validAt=startAt)
	    startAt = nextStart

        self.checkKeys()

    def removeDeadKeys(self, now=None):
	"""Remove all keys that have expired"""
        self.checkKeys()

        if now is None:
            now = time.time()
            expiryStr = " expired"
        else:
            expiryStr = ""

        cutoff = now - self.keySloppiness
	dirs = [ os.path.join(self.keyDir,"key_"+name)
                  for va, vu, name in self.keyIntervals if vu < cutoff ]

	for dirname, (va, vu, name) in zip(dirs, self.keyIntervals):
            LOG.info("Removing%s key %s (valid from %s through %s)",
                        expiryStr, name, formatDate(va), formatDate(vu-3600))
	    files = [ os.path.join(dirname,f)
                                 for f in os.listdir(dirname) ]
	    secureDelete(files, blocking=1)
	    os.rmdir(dirname)

	self.checkKeys()

    def _getLiveKey(self, when=None):
	"""Find the first key that is now valid.  Return (Valid-after,
	   valid-util, name)."""
        if not self.keyIntervals:
	    self.liveKey = None
	    self.nextKeyRotation = 0
	    return None

	w = when
	if when is None:
	    when = time.time()
	    if when < self.nextKeyRotation:
		return self.liveKey

	idx = bisect.bisect(self.keyIntervals, (when, None, None))-1
	k = self.keyIntervals[idx]
	if w is None:
	    self.liveKey = k
	    self.nextKeyRotation = k[1]

	return k

    def getNextKeyRotation(self):
	"""Return the expiration time of the current key"""
        return self.nextKeyRotation

    def getServerKeyset(self):
	"""Return a ServerKeyset object for the currently live key."""
	# FFFF Support passwords on keys
	_, _, name = self._getLiveKey()
	keyset = ServerKeyset(self.keyDir, name, self.hashDir)
	keyset.load()
	return keyset

    def getDHFile(self):
	"""Return the filename for the diffie-helman parameters for the
	   server.  Creates the file if it doesn't yet exist."""
	dhdir = os.path.join(self.homeDir, 'work', 'tls')
	createPrivateDir(dhdir)
	dhfile = os.path.join(dhdir, 'dhparam')
        if not os.path.exists(dhfile):
            LOG.info("Generating Diffie-Helman parameters for TLS...")
            mixminion._minionlib.generate_dh_parameters(dhfile, verbose=0)
            LOG.info("...done")
	else:
	    LOG.debug("Using existing Diffie-Helman parameter from %s",
			   dhfile)

        return dhfile

    def getTLSContext(self):
	"""Create and return a TLS context from the currently live key."""
        keys = self.getServerKeyset()
        return mixminion._minionlib.TLSContext_new(keys.getCertFileName(),
						   keys.getMMTPKey(),
						   self.getDHFile())

    def getPacketHandler(self):
	"""Create and return a PacketHandler from the currently live key."""
        keys = self.getServerKeyset()
	packetKey = keys.getPacketKey()
	hashlog = mixminion.server.HashLog.HashLog(keys.getHashLogFileName(),
						 keys.getMMTPKeyID())
        return mixminion.server.PacketHandler.PacketHandler(packetKey,
						     hashlog)


#----------------------------------------------------------------------
class ServerKeyset:
    """A set of expirable keys for use by a server.

       A server has one long-lived identity key, and two short-lived
       temporary keys: one for subheader encryption and one for MMTP.  The
       subheader (or 'packet') key has an associated hashlog, and the
       MMTP key has an associated self-signed X509 certificate.

       Whether we publish or not, we always generate a server descriptor
       to store the keys' lifetimes.

       When we create a new ServerKeyset object, the associated keys are not
       read from disk unil the object's load method is called."""
    ## Fields:
    # hashlogFile: filename of this keyset's hashlog.
    # packetKeyFile, mmtpKeyFile: filename of this keyset's short-term keys
    # certFile: filename of this keyset's X509 certificate
    # descFile: filename of this keyset's server descriptor.
    #
    # packetKey, mmtpKey: This server's actual short-term keys.
    def __init__(self, keyroot, keyname, hashroot):
	"""Load a set of keys named "keyname" on a server where all keys
	   are stored under the directory "keyroot" and hashlogs are stored
	   under "hashroot". """
	keydir  = os.path.join(keyroot, "key_"+keyname)
	self.hashlogFile = os.path.join(hashroot, "hash_"+keyname)
	self.packetKeyFile = os.path.join(keydir, "mix.key")
	self.mmtpKeyFile = os.path.join(keydir, "mmtp.key")
	self.certFile = os.path.join(keydir, "mmtp.cert")
        self.descFile = os.path.join(keydir, "ServerDesc")
        if not os.path.exists(keydir):
	    createPrivateDir(keydir)

    def load(self, password=None):
        """Read the short-term keys from disk.  Must be called before
	   getPacketKey or getMMTPKey."""
        self.packetKey = mixminion.Crypto.pk_PEM_load(self.packetKeyFile,
                                                      password)
        self.mmtpKey = mixminion.Crypto.pk_PEM_load(self.mmtpKeyFile,
                                                    password)
    def save(self, password=None):
        """Save this set of keys to disk."""
        mixminion.Crypto.pk_PEM_save(self.packetKey, self.packetKeyFile,
                                     password)
        mixminion.Crypto.pk_PEM_save(self.mmtpKey, self.mmtpKeyFile,
                                     password)
    def getCertFileName(self): return self.certFile
    def getHashLogFileName(self): return self.hashlogFile
    def getDescriptorFileName(self): return self.descFile
    def getPacketKey(self): return self.packetKey
    def getMMTPKey(self): return self.mmtpKey
    def getMMTPKeyID(self):
        "Return the sha1 hash of the asn1 encoding of the MMTP public key"
	return mixminion.Crypto.sha1(self.mmtpKey.encode_key(1))

#----------------------------------------------------------------------
# Functionality to generate keys and server descriptors

# We have our X509 certificate set to expire a bit after public key does,
# so that slightly-skewed clients don't incorrectly give up while trying to
# connect to us.
CERTIFICATE_EXPIRY_SLOPPINESS = 5*60

def generateServerDescriptorAndKeys(config, identityKey, keydir, keyname,
                                    hashdir, validAt=None):
    """Generate and sign a new server descriptor, and generate all the keys to
       go with it.

          config -- Our ServerConfig object.
          identityKey -- This server's private identity key
          keydir -- The root directory for storing key sets.
          keyname -- The name of this new key set within keydir
	  hashdir -- The root directory for storing hash logs.
          validAt -- The starting time (in seconds) for this key's lifetime."""

    # First, we generate both of our short-term keys...
    packetKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
    mmtpKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)

    # ...and save them to disk, setting up our directory structure while
    # we're at it.
    serverKeys = ServerKeyset(keydir, keyname, hashdir)
    serverKeys.packetKey = packetKey
    serverKeys.mmtpKey = mmtpKey
    serverKeys.save()

    # FFFF unused
    # allowIncoming = config['Incoming/MMTP'].get('Enabled', 0)

    # Now, we pull all the information we need from our configuration.
    nickname = config['Server']['Nickname']
    if not nickname:
        nickname = socket.gethostname()
        if not nickname or nickname.lower().startswith("localhost"):
            nickname = config['Incoming/MMTP'].get('IP', "<Unknown host>")
	LOG.warn("No nickname given: defaulting to %r", nickname)
    contact = config['Server']['Contact-Email']
    comments = config['Server']['Comments']
    if not validAt:
        validAt = time.time()

    # Calculate descriptor and X509 certificate lifetimes.
    # (Round validAt to previous mignight.)
    validAt = mixminion.Common.previousMidnight(validAt+30)
    validUntil = validAt + config['Server']['PublicKeyLifetime'][2]
    certStarts = validAt - CERTIFICATE_EXPIRY_SLOPPINESS
    certEnds = validUntil + CERTIFICATE_EXPIRY_SLOPPINESS + \
               config['Server']['PublicKeySloppiness'][2]

    # Create the X509 certificate.
    mixminion.Crypto.generate_cert(serverKeys.getCertFileName(),
				   mmtpKey,
				   "MMTP certificate for %s" %nickname,
                                   certStarts, certEnds)

    fields = {
	"IP": config['Incoming/MMTP'].get('IP', "0.0.0.0"),
	"Port": config['Incoming/MMTP'].get('Port', 0),
	"Nickname": nickname,
	"Identity":
	   formatBase64(mixminion.Crypto.pk_encode_public_key(identityKey)),
	"Published": formatTime(time.time()),
	"ValidAfter": formatDate(validAt),
	"ValidUntil": formatDate(validUntil),
	"PacketKey":
  	   formatBase64(mixminion.Crypto.pk_encode_public_key(packetKey)),
	"KeyID":
	   formatBase64(serverKeys.getMMTPKeyID()),
	}

    # If we don't know our IP address, try to guess
    if fields['IP'] == '0.0.0.0':
	try:
	    fields['IP'] = _guessLocalIP()
	    LOG.warn("No IP configured; guessing %s",fields['IP'])
	except IPGuessError, e:
	    LOG.error("Can't guess IP: %s", str(e))
	    raise MixError("Can't guess IP: %s" % str(e))

    # Fill in a stock server descriptor.  Note the empty Digest: and
    # Signature: lines.
    info = """\
        [Server]
	Descriptor-Version: 0.1
        IP: %(IP)s
        Nickname: %(Nickname)s
	Identity: %(Identity)s
	Digest:
        Signature:
        Published: %(Published)s
        Valid-After: %(ValidAfter)s
	Valid-Until: %(ValidUntil)s
	Packet-Key: %(PacketKey)s
        """ % fields
    if contact:
	info += "Contact: %s\n"%contact
    if comments:
	info += "Comments: %s\n"%comments

    # Only advertise incoming MMTP if we support it.
    if config["Incoming/MMTP"].get("Enabled", 0):
	info += """\
            [Incoming/MMTP]
            Version: 0.1
            Port: %(Port)s
	    Key-Digest: %(KeyID)s
	    Protocols: 0.1
            """ % fields
        for k,v in config.getSectionItems("Incoming/MMTP"):
            if k not in ("Allow", "Deny"):
                continue
            info += "%s: %s" % (k, _rule(k=='Allow',v))

    # Only advertise outgoing MMTP if we support it.
    if config["Outgoing/MMTP"].get("Enabled", 0):
	info += """\
            [Outgoing/MMTP]
	    Version: 0.1
            Protocols: 0.1
            """
        for k,v in config.getSectionItems("Outgoing/MMTP"):
            if k not in ("Allow", "Deny"):
                continue
            info += "%s: %s" % (k, _rule(k=='Allow',v))

    # Ask our modules for their configuration information.
    info += "".join(config.moduleManager.getServerInfoBlocks())

    # Remove extra (leading or trailing) whitespace from the lines.
    lines = [ line.strip() for line in info.split("\n") ]
    # Remove empty lines
    lines = filter(None, lines)
    # Force a newline at the end of the file, rejoin, and sign.
    lines.append("")
    info = "\n".join(lines)
    info = signServerInfo(info, identityKey)

    # Write the desciptor
    f = open(serverKeys.getDescriptorFileName(), 'w')
    try:
        f.write(info)
    finally:
        f.close()

    # This is for debugging: we try to parse and validate the descriptor
    #   we just made.
    # FFFF Remove this once we're more confident.
    ServerInfo(string=info)

    return info

def _rule(allow, (ip, mask, portmin, portmax)):
    """Return an external representation of an IP allow/deny rule."""
    if mask == '0.0.0.0':
        ip="*"
        mask=""
    elif mask == "255.255.255.255":
        mask = ""
    else:
        mask = "/%s" % mask

    if portmin==portmax==48099 and allow:
        ports = ""
    elif portmin == 0 and portmax == 65535 and not allow:
        ports = ""
    elif portmin == portmax:
        ports = " %s" % portmin
    else:
        ports = " %s-%s" % (portmin, portmax)

    return "%s%s%s\n" % (ip,mask,ports)

#----------------------------------------------------------------------
# Helpers to guess a reasonable local IP when none is provided.

class IPGuessError(MixError):
    """Exception: raised when we can't guess a single best IP."""
    pass

# Cached guessed IP address
_GUESSED_IP = None

def _guessLocalIP():
    "Try to find a reasonable IP for this host."
    global _GUESSED_IP
    if _GUESSED_IP is not None:
	return _GUESSED_IP

    # First, let's see what our name resolving subsystem says our
    # name is.
    ip_set = {}
    try:
	ip_set[ socket.gethostbyname(socket.gethostname()) ] = 1
    except socket.error:
	try:
	    ip_set[ socket.gethostbyname(socket.getfqdn()) ] = 1
	except socket.error:
	    pass

    # And in case that doesn't work, let's see what other addresses we might
    # think we have by using 'getsockname'.
    for target_addr in ('18.0.0.1', '10.0.0.1', '192.168.0.1',
			'172.16.0.1')+tuple(ip_set.keys()):
	# open a datagram socket so that we don't actually send any packets
	# by connecting.
	try:
	    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	    s.connect((target_addr, 9)) #discard port
	    ip_set[ s.getsockname()[0] ] = 1
	except socket.error:
	    pass

    for ip in ip_set.keys():
	if ip.startswith("127.") or ip.startswith("0."):
	    del ip_set[ip]

    # FFFF reject 192.168, 10., 176.16.x

    if len(ip_set) == 0:
	raise IPGuessError("No address found")

    if len(ip_set) > 1:
	raise IPGuessError("Multiple addresses found: %s" % (
	            ", ".join(ip_set.keys())))

    return ip_set.keys()[0]

Index: HashLog.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/HashLog.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- HashLog.py	11 Dec 2002 06:58:55 -0000	1.1
+++ HashLog.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -78,7 +78,7 @@
 		self.journal[j[i:i+DIGEST_LEN]] = 1
 	    f.close()
 
-	self.journalFile = os.open(self.journalFileName, 
+	self.journalFile = os.open(self.journalFileName,
 		    _JOURNAL_OPEN_FLAGS|os.O_APPEND, 0600)
 
     def seenHash(self, hash):
@@ -113,5 +113,5 @@
         self.sync()
         self.log.close()
 	os.close(self.journalFile)
-	
+
 

Index: MMTPServer.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/MMTPServer.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- MMTPServer.py	11 Dec 2002 06:58:55 -0000	1.1
+++ MMTPServer.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -72,7 +72,7 @@
 ## 						    readers,
 ## 						    len(self.writers),
 ## 						    writers))
-        
+
         readfds = self.readers.keys()
         writefds = self.writers.keys()
         try:
@@ -125,7 +125,7 @@
         """Register a connection as a reader and a writer.  The
            connection's 'handleRead' and 'handleWrite' methods will be
            called as appropriate.
-        """ 
+        """
         fd = connection.fileno()
         self.readers[fd] = self.writers[fd] = connection
 
@@ -140,7 +140,7 @@
 class Connection:
     "A connection is an abstract superclass for asynchronous channels"
     def handleRead(self):
-        """Invoked when there is data to read.""" 
+        """Invoked when there is data to read."""
         pass
     def handleWrite(self):
         """Invoked when there is data to write."""
@@ -222,7 +222,7 @@
     #           throw _ml.TLSWantRead or _ml.TLSWantWrite.  See __acceptFn,
     #           __connectFn, __shutdownFn, __readFn, __writeFn.
     #    __server: an AsyncServer.
-    #    __inbuf: A list of strings that we've read since the last expectRead. 
+    #    __inbuf: A list of strings that we've read since the last expectRead.
     #    __inbuflen: The total length of all the strings in __inbuf
     #    __expectReadLen: None, or the number of bytes to read before
     #           the current read succeeds.
@@ -257,7 +257,7 @@
         else:
             assert self.__state == self.__connectFn
             server.registerWriter(self)
-        
+
     def expectRead(self, bytes=None, terminator=None):
         """Begin reading from the underlying TLS connection.
 
@@ -278,9 +278,9 @@
         self.__server.registerReader(self)
 
     def beginWrite(self, str):
-        """Begin writing a string to the underlying connection.  When the  
+        """Begin writing a string to the underlying connection.  When the
            string is completely written, this object's "finished" method
-           will be called. 
+           will be called.
         """
         self.__outbuf = str
         self.__state = self.__writeFn
@@ -318,7 +318,7 @@
 	# side may be hostile, confused, or deadlocking.
 	trace("Got a 0 on shutdown (fd %s)", self.fd)
 	# ???? Is 'wantread' always correct?
-	# ???? Rather than waiting for a read, should we use a timer or 
+	# ???? Rather than waiting for a read, should we use a timer or
 	# ????       something?
 	raise _ml.TLSWantRead()
 
@@ -346,8 +346,8 @@
 		 self.address)
             self.shutdown(err=1, retriable=0)
             return
-         
-        if self.__terminator and stringContains(self.__inbuf[0], 
+
+        if self.__terminator and stringContains(self.__inbuf[0],
 						self.__terminator):
             trace("read found terminator (fd %s)", self.fd)
             self.__server.unregister(self)
@@ -370,7 +370,7 @@
         self.__outbuf = out
         if len(out) == 0:
             self.finished()
-        
+
     def handleRead(self):
         self.__handleAll()
 
@@ -384,7 +384,7 @@
            None.
         """
         self.lastActivity = time.time()
-        
+
         try:
             # We have a while loop here so that, upon entering a new
             # state, we immediately see if we can go anywhere with it
@@ -399,10 +399,10 @@
             warn("Unexpectedly closed connection to %s", self.address)
 	    self.handleFail(retriable=1)
             self.__sock.close()
-            self.__server.unregister(self) 
+            self.__server.unregister(self)
         except _ml.TLSError:
             if self.__state != self.__shutdownFn:
-                warn("Unexpected error: closing connection to %s", 
+                warn("Unexpected error: closing connection to %s",
 		     self.address)
                 self.shutdown(err=1, retriable=1)
             else:
@@ -412,7 +412,7 @@
         else:
             # We are in no state at all.
             self.__server.unregister(self)
-              
+
     def finished(self):
         """Called whenever a connect, accept, read, or write is finished."""
         pass
@@ -426,7 +426,7 @@
 	if err:
 	    self.handleFail(retriable)
         self.__state = self.__shutdownFn
-        
+
     def fileno(self):
         return self.fd
 
@@ -440,7 +440,7 @@
     def handleFail(self, retriable=0):
 	"""Called when we shutdown with an error."""
 	pass
-    
+
 #----------------------------------------------------------------------
 # Implementation for MMTP.
 
@@ -468,7 +468,7 @@
     #     SimpleTLSConnection.
     def __init__(self, sock, tls, consumer):
 	"""Create an MMTP connection to receive messages sent along a given
-	   socket.  When valid packets are received, pass them to the 
+	   socket.  When valid packets are received, pass them to the
 	   function 'consumer'."""
         SimpleTLSConnection.__init__(self, sock, tls, 1)
         self.messageConsumer = consumer
@@ -555,11 +555,11 @@
     """Asynchronious implementation of the sending ("client") side of a
        mixminion connection."""
     ## Fields:
-    # ip, port, keyID, messageList, handleList, sendCallback, failCallback: 
+    # ip, port, keyID, messageList, handleList, sendCallback, failCallback:
     #   As described in the docstring for __init__ below.
     def __init__(self, context, ip, port, keyID, messageList, handleList,
                  sentCallback=None, failCallback=None):
-	"""Create a connection to send messages to an MMTP server.  
+	"""Create a connection to send messages to an MMTP server.
 	   ip -- The IP of the destination server.
 	   port -- The port to connect to.
 	   keyID -- None, or the expected SHA1 hash of the server's public key
@@ -570,7 +570,7 @@
               whenever a message is successfully sent.
            failCallback -- None, or a function of (msg, handle, retriable)
               to be called when messages can't be sent."""
-	
+
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sock.setblocking(0)
         self.keyID = keyID
@@ -608,7 +608,7 @@
         self.beginWrite(PROTOCOL_STRING)
         self.finished = self.__sentProtocol
 
-    def __sentProtocol(self):    
+    def __sentProtocol(self):
         """Called when we're done sending the protocol string.  Begins
            reading the server's response.
         """
@@ -638,7 +638,7 @@
         self.expectedDigest = sha1(msg+"RECEIVED")
         msg = SEND_CONTROL+msg+sha1(msg+"SEND")
 	assert len(msg) == SEND_RECORD_LEN
-	
+
         self.beginWrite(msg)
         self.finished = self.__sentMessage
 
@@ -675,7 +675,7 @@
        del self.handleList[0]
        if self.sentCallback is not None:
            self.sentCallback(justSent, justSentHandle)
-	   
+
        self.beginNextMessage()
 
     def handleFail(self, retriable):
@@ -711,7 +711,7 @@
         con = MMTPServerConnection(sock, tls, self.onMessageReceived)
         con.register(self)
 	return con
-    
+
     def stopListening(self):
 	self.listener.shutdown()
 
@@ -721,10 +721,10 @@
 	for m,h in zip(messages, handles):
 	    assert len(m) == MESSAGE_LEN
 	    assert len(h) < 32
-	    
+
         con = MMTPClientConnection(self.context,
 				   ip, port, keyID, messages, handles,
-                                   self.onMessageSent, 
+                                   self.onMessageSent,
 				   self.onMessageUndeliverable)
         con.register(self)
 

Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/Modules.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- Modules.py	11 Dec 2002 06:58:55 -0000	1.1
+++ Modules.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -20,15 +20,14 @@
 import socket
 import base64
 
+import mixminion.BuildMessage
 import mixminion.Config
 import mixminion.Packet
-import mixminion.BuildMessage
+import mixminion.server.Queue
 from mixminion.Config import ConfigError, _parseBoolean, _parseCommand
 from mixminion.Common import LOG, createPrivateDir, MixError, isSMTPMailbox, \
      isPrintingAscii
 
-import mixminion.server.Queue
-
 # Return values for processMessage
 DELIVER_OK = 1
 DELIVER_FAIL_RETRY = 2
@@ -117,7 +116,7 @@
 
     def queueDeliveryMessage(self, (exitType, address, tag), message):
 	"""Instead of queueing our message, pass it directly to the underlying
-	   DeliveryModule."""	
+	   DeliveryModule."""
 	try:
 	    res = self.module.processMessage(message, tag, exitType, address)
 	    if res == DELIVER_OK:
@@ -137,14 +136,14 @@
 class SimpleModuleDeliveryQueue(mixminion.server.Queue.DeliveryQueue):
     """Helper class used as a default delivery queue for modules that
        don't care about batching messages to like addresses."""
-    ## Fields: 
+    ## Fields:
     # module: the underlying module.
     def __init__(self, module, directory):
 	mixminion.server.Queue.DeliveryQueue.__init__(self, directory)
 	self.module = module
 
     def _deliverMessages(self, msgList):
-	for handle, addr, message, n_retries in msgList:	
+	for handle, addr, message, n_retries in msgList:
 	    try:
 		exitType, address, tag = addr
 		result = self.module.processMessage(message,tag,exitType,address)
@@ -258,7 +257,7 @@
 	    self.registerModule(pyClass())
 	except Exception, e:
 	    raise MixError("Error initializing module %s" %className)
-	
+
     def validate(self, sections, entries, lines, contents):
 	# (As in ServerConfig)
         for m in self.modules:
@@ -293,7 +292,7 @@
 	"""Remove trash messages from all internal queues."""
 	for queue in self.queues.values():
 	    queue.cleanQueue()
-	
+
     def disableModule(self, module):
 	"""Unmaps all the types for a module object."""
         LOG.info("Disabling module %s", module.getName())
@@ -363,7 +362,7 @@
           addr: smtpaddr
           addr: smtpaddr
            ...
-       
+
        When we receive a message send to 'addr', we deliver it to smtpaddr.
        """
     ##
@@ -405,7 +404,7 @@
 	    if not isSMTPMailbox(sec[field]):
 		LOG.warn("Value of %s (%s) doesn't look like an email address",
 			 field, sec[field])
-	    
+
 
     def configure(self, config, moduleManager):
 	if not config['Delivery/MBOX'].get("Enabled", 0):
@@ -491,8 +490,8 @@
 Subject: Anonymous Mixminion message
 
 THIS IS AN ANONYMOUS MESSAGE.  The mixminion server '%(nickname)s' at
-%(addr)s has been configured to deliver messages to your address.  
-If you do not want to receive messages in the future, contact %(contact)s 
+%(addr)s has been configured to deliver messages to your address.
+If you do not want to receive messages in the future, contact %(contact)s
 and you will be removed.
 
 %(msg)s""" % fields
@@ -502,7 +501,7 @@
 
 #----------------------------------------------------------------------
 class SMTPModule(DeliveryModule):
-    """Placeholder for real exit node implementation. 
+    """Placeholder for real exit node implementation.
        For now, use MixmasterSMTPModule"""
     def __init__(self):
         DeliveryModule.__init__(self)
@@ -519,7 +518,7 @@
        test mixminion by usingg Mixmaster nodes as exits."""
     # FFFF Mixmaster has tons of options.  Maybe we should use 'em...
     # FFFF ... or maybe we should deliberately ignore them, since
-    # FFFF this is only a temporary workaround until enough people 
+    # FFFF this is only a temporary workaround until enough people
     # FFFF are running SMTP exit nodes
     ## Fields:
     # server: The path (usually a single server) to use for outgoing messages.
@@ -529,7 +528,7 @@
     # options: Options to pass to the Mixmaster binary when queueing messages
     # tmpQueue: An auxiliary Queue used to hold files so we can pass them to
     #    Mixmaster.  (This should go away; we should use stdin instead.)
-    
+
     def __init__(self):
         SMTPModule.__init__(self)
 
@@ -542,7 +541,7 @@
                                     'Type-III Anonymous Message'),
                    }
                  }
-                   
+
     def validateConfig(self, sections, entries, lines, contents):
 	# Currently, we accept any configuration options that the config allows
         pass
@@ -560,8 +559,8 @@
 					"-s", self.subject)
         manager.enableModule(self)
 
-    def getName(self): 
-        return "SMTP_MIX2" 
+    def getName(self):
+        return "SMTP_MIX2"
 
     def createDeliveryQueue(self, queueDir):
 	# We create a temporary queue so we can hold files there for a little
@@ -586,7 +585,7 @@
         LOG.debug("Queued Mixmaster message: exit code %s", code)
         self.tmpQueue.removeMessage(handle)
         return DELIVER_OK
-                         
+
     def flushMixmasterPool(self):
 	"""Send all pending messages from the Mixmaster queue.  This
 	   should be called after invocations of processMessage."""
@@ -601,7 +600,7 @@
     def _deliverMessages(self, msgList):
         SimpleModuleDeliveryQueue._deliverMessages(self, msgList)
         self.module.flushMixmasterPool()
-        
+
 #----------------------------------------------------------------------
 
 def sendSMTPMessage(server, toList, fromAddr, message):
@@ -664,7 +663,7 @@
        whether the message is a text plaintext message (code='TXT'), a
        binary plaintext message (code 'BIN'), or an encrypted message/reply
        (code='ENC').  If requested, non-TXT messages are base-64 encoded.
- 
+
        Returns: (code, message, tag (for ENC) or None (for BIN, TXT).
        Returns None if the message is invalid.
 

Index: PacketHandler.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/PacketHandler.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- PacketHandler.py	11 Dec 2002 06:58:55 -0000	1.1
+++ PacketHandler.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -35,7 +35,7 @@
             # Check whether we have a key or a sequence of keys.
             _ = privatekey[0]
             assert len(hashlog) == len(privatekey)
-            
+
             self.privatekey = privatekey
             self.hashlog = hashlog
         except TypeError:
@@ -124,7 +124,7 @@
         # If the subheader says that we have extra blocks of routing info,
         # decrypt and parse them now.
         if subh.isExtended():
-            nExtra = subh.getNExtraBlocks() 
+            nExtra = subh.getNExtraBlocks()
             if (rt < Packet.MIN_EXIT_TYPE) or (nExtra > 15):
                 # None of the native methods allow multiple blocks; no
                 # size can be longer than the number of bytes in the rest
@@ -168,7 +168,7 @@
         header2 = Crypto.lioness_decrypt(msg.header2,
                            keys.getLionessKeys(Crypto.HEADER_ENCRYPT_MODE))
 
-        # If we're the swap node, (1) decrypt the payload with a hash of 
+        # If we're the swap node, (1) decrypt the payload with a hash of
 	# header2... (2) decrypt header2 with a hash of the payload...
 	# (3) and swap the headers.
         if rt == Packet.SWAP_FWD_TYPE:

Index: Queue.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/Queue.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- Queue.py	11 Dec 2002 06:58:55 -0000	1.1
+++ Queue.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -3,7 +3,7 @@
 
 """mixminion.server.Queue
 
-   Facility for fairly secure, directory-based, unordered queues. 
+   Facility for fairly secure, directory-based, unordered queues.
    """
 
 import os
@@ -14,7 +14,7 @@
 
 from mixminion.Common import MixError, MixFatalError, secureDelete, LOG, \
      createPrivateDir
-from mixminion.Crypto import AESCounterPRNG
+from mixminion.Crypto import getCommonPRNG
 
 __all__ = [ 'Queue', 'DeliveryQueue', 'TimedMixQueue', 'CottrellMixQueue',
 	    'BinomialCottrellMixQueue' ]
@@ -68,7 +68,7 @@
 
         secureDelete([]) # Make sure secureDelete is configured. HACK!
 
-        self.rng = AESCounterPRNG()
+        self.rng = getCommonPRNG()
         self.dir = location
 
         if not os.path.isabs(location):
@@ -228,7 +228,7 @@
 		    else:
 			return 1
 		except OSError:
-		    # If the 'stat' or 'unlink' calls above fail, then 
+		    # If the 'stat' or 'unlink' calls above fail, then
 		    # .cleaning must not exist, or must not be readable
 		    # by us.
 		    if os.path.exists(cleanFile):
@@ -319,7 +319,7 @@
 	"""Returns a (n_retries, addr, msg) payload for a given
 	   message handle."""
         return self.getObject(handle)
-	
+
     def sendReadyMessages(self):
 	"""Sends all messages which are not already being sent."""
 
@@ -429,7 +429,7 @@
 	# says that Cottrell says is the real thing.
 
 	TimedMixQueue.__init__(self, location, interval)
-	self.minPool = minPool	
+	self.minPool = minPool
 	self.minSend = minSend
 	self.sendRate = sendRate
 
@@ -462,9 +462,9 @@
 				    if self.rng.getFloat() < msgProbability ])
 
 def _secureDelete_bg(files, cleanFile):
-    """Helper method: delete files in another thread, removing 'cleanFile' 
+    """Helper method: delete files in another thread, removing 'cleanFile'
        once we're done."""
-      
+
     pid = os.fork()
     if pid != 0:
         return pid

Index: ServerMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerMain.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- ServerMain.py	11 Dec 2002 06:58:55 -0000	1.1
+++ ServerMain.py	12 Dec 2002 19:56:47 -0000	1.2
@@ -7,317 +7,25 @@
 
    See the "MixminionServer" class for more information about how it
    all works. """
-#FFFF We need support for encrypting private keys.
 
-import os
+__all__ = [ 'MixminonServer' ]
+
 import getopt
+import os
 import sys
 import time
-import bisect
 
-import mixminion._minionlib
+import mixminion.Config
 import mixminion.Crypto
-from mixminion.ServerInfo import ServerKeyset, ServerInfo, \
-     generateServerDescriptorAndKeys
-from mixminion.Common import LOG, MixFatalError, MixError, secureDelete, \
-     createPrivateDir, previousMidnight, ceilDiv, formatDate, formatTime
-
-import mixminion.server.Queue
 import mixminion.server.MMTPServer
 import mixminion.server.Modules
-import mixminion.server.HashLog
 import mixminion.server.PacketHandler
+import mixminion.server.Queue
+import mixminion.server.ServerConfig
+import mixminion.server.ServerKeys
 
-
-class ServerKeyring:
-    """A ServerKeyring remembers current and future keys, descriptors, and
-       hash logs for a mixminion server.
-
-       FFFF We need a way to generate keys as needed, not just a month's
-       FFFF worth of keys up front. 
-       """
-    ## Fields:
-    # homeDir: server home directory
-    # keyDir: server key directory
-    # keySloppiness: fudge-factor: how forgiving are we about key liveness?
-    # keyIntervals: list of (start, end, keyset Name)
-    # liveKey: list of (start, end, keyset name for current key.)
-    # nextRotation: time_t when this key expires.
-    # keyRange: tuple of (firstKey, lastKey) to represent which key names
-    #      have keys on disk.
-
-    ## Directory layout:
-    #    MINION_HOME/work/queues/incoming/ [Queue of received,unprocessed pkts]
-    #                             mix/ [Mix pool]
-    #                             outgoing/ [Messages for mmtp delivery]
-    #                             deliver/mbox/ []
-    #                      tls/dhparam [Diffie-Hellman parameters]
-    #                      hashlogs/hash_1*  [HashLogs of packet hashes 
-    #                               hash_2*    corresponding to key sets]
-    #                                ...  
-    #                 log [Messages from the server]
-    #                 keys/identity.key [Long-lived identity PK]
-    #                      key_1/ServerDesc [Server descriptor]
-    #                            mix.key [packet key]
-    #                            mmtp.key [mmtp key]
-    #                            mmtp.cert [mmmtp key x509 cert]
-    #                      key_2/...
-    #                 conf/miniond.conf [configuration file]
-    #                       ....
-
-    # FFFF Support to put keys/queues in separate directories.
-
-    def __init__(self, config):
-	"Create a ServerKeyring from a config object"
-	self.configure(config)
-
-    def configure(self, config):
-	"Set up a SeverKeyring from a config object"
-	self.config = config
-	self.homeDir = config['Server']['Homedir']
-	self.keyDir = os.path.join(self.homeDir, 'keys')
-	self.hashDir = os.path.join(self.homeDir, 'work', 'hashlogs')
-	self.keySloppiness = config['Server']['PublicKeySloppiness'][2]
-	self.checkKeys()
-
-    def checkKeys(self):
-	"""Internal method: read information about all this server's
-	   currently-prepared keys from disk."""
-        self.keyIntervals = []
-	firstKey = sys.maxint
-	lastKey = 0
-
-	LOG.debug("Scanning server keystore at %s", self.keyDir)
-
-	if not os.path.exists(self.keyDir):
-	    LOG.info("Creating server keystore at %s", self.keyDir)
-	    createPrivateDir(self.keyDir)
-
-	# Iterate over the entires in HOME/keys
-        for dirname in os.listdir(self.keyDir):
-	    # Skip any that aren't directories named "key_INT"
-	    if not os.path.isdir(os.path.join(self.keyDir,dirname)):
-		continue
-            if not dirname.startswith('key_'):
-		LOG.warn("Unexpected directory %s under %s",
-			      dirname, self.keyDir)
-                continue
-            keysetname = dirname[4:]
-	    try:
-		setNum = int(keysetname)
-		# keep trace of the first and last used key number
-		if setNum < firstKey: firstKey = setNum
-		if setNum > lastKey: lastKey = setNum
-	    except ValueError:
-		LOG.warn("Unexpected directory %s under %s",
-			      dirname, self.keyDir)
-		continue
-
-	    # Find the server descriptor...
-            d = os.path.join(self.keyDir, dirname)
-            si = os.path.join(d, "ServerDesc")
-            if os.path.exists(si):
-                inf = ServerInfo(fname=si, assumeValid=1)
-		# And find out when it's valid.
-                t1 = inf['Server']['Valid-After']
-                t2 = inf['Server']['Valid-Until']
-                self.keyIntervals.append( (t1, t2, keysetname) )
-		LOG.debug("Found key %s (valid from %s to %s)",
-			       dirname, formatDate(t1), formatDate(t2))
-	    else:
-		LOG.warn("No server descriptor found for key %s"%dirname)
-
-	# Now, sort the key intervals by starting time.
-        self.keyIntervals.sort()
-	self.keyRange = (firstKey, lastKey)
-
-	# Now we try to see whether we have more or less than 1 key in effect
-	# for a given time.
-	for idx in xrange(len(self.keyIntervals)-1):
-	    end = self.keyIntervals[idx][1]
-	    start = self.keyIntervals[idx+1][0]
-	    if start < end:
-		LOG.warn("Multiple keys for %s.  That's unsupported.",
-			      formatDate(end))
-	    elif start > end:
-		LOG.warn("Gap in key schedule: no key from %s to %s",
-			      formatDate(end), formatDate(start))
-
-	self.nextKeyRotation = 0 # Make sure that now > nextKeyRotation before
-	                         # we call _getLiveKey()
-	self._getLiveKey()       # Set up liveKey, nextKeyRotation.
-
-    def getIdentityKey(self):
-	"""Return this server's identity key.  Generate one if it doesn't
-	   exist."""
-	password = None # FFFF Use this, somehow.
-	fn = os.path.join(self.keyDir, "identity.key")
-	bits = self.config['Server']['IdentityKeyBits']
-	if os.path.exists(fn):
-	    key = mixminion.Crypto.pk_PEM_load(fn, password)
-	    keylen = key.get_modulus_bytes()*8
-	    if keylen != bits:
-		LOG.warn(
-		    "Stored identity key has %s bits, but you asked for %s.",
-		    keylen, bits)
-	else:
-	    LOG.info("Generating identity key. (This may take a while.)")
-	    key = mixminion.Crypto.pk_generate(bits)
-	    mixminion.Crypto.pk_PEM_save(key, fn, password)
-	    LOG.info("Generated %s-bit identity key.", bits)
-
-	return key
-
-    def removeIdentityKey(self):
-        """Remove this server's identity key."""
-        fn = os.path.join(self.keyDir, "identity.key")
-        if not os.path.exists(fn):
-            LOG.info("No identity key to remove.")
-        else:
-            LOG.warn("Removing identity key in 10 seconds")
-            time.sleep(10)
-            LOG.warn("Removing identity key")
-            secureDelete([fn], blocking=1)
-
-	dhfile = os.path.join(self.homeDir, 'work', 'tls', 'dhparam')
-        if os.path.exists('dhfile'):
-            LOG.info("Removing diffie-helman parameters file")
-            secureDelete([dhfile], blocking=1)
-
-    def createKeys(self, num=1, startAt=None):
-	"""Generate 'num' public keys for this server. If startAt is provided,
-           make the first key become valid at'startAt'.  Otherwise, make the
-	   first key become valid right after the last key we currently have
-	   expires.  If we have no keys now, make the first key start now."""
-        # FFFF Use this.
-	#password = None
-
-	if startAt is None:
-	    if self.keyIntervals:
-		startAt = self.keyIntervals[-1][1]+60
-	    else:
-		startAt = time.time()+60
-
-	startAt = previousMidnight(startAt)
-
-	firstKey, lastKey = self.keyRange
-
-	for _ in xrange(num):
-	    if firstKey == sys.maxint:
-		keynum = firstKey = lastKey = 1
-	    elif firstKey > 1:
-		firstKey -= 1
-		keynum = firstKey
-	    else:
-		lastKey += 1
-		keynum = lastKey
-
-	    keyname = "%04d" % keynum
-
-	    nextStart = startAt + self.config['Server']['PublicKeyLifetime'][2]
-
-	    LOG.info("Generating key %s to run from %s through %s (GMT)",
-		     keyname, formatDate(startAt), 
-		     formatDate(nextStart-3600))
- 	    generateServerDescriptorAndKeys(config=self.config,
-					    identityKey=self.getIdentityKey(),
-					    keyname=keyname,
-					    keydir=self.keyDir,
-					    hashdir=self.hashDir,
-					    validAt=startAt)
-	    startAt = nextStart
-
-        self.checkKeys()
-
-    def removeDeadKeys(self, now=None):
-	"""Remove all keys that have expired"""
-        self.checkKeys()
-
-        if now is None:
-            now = time.time()
-            expiryStr = " expired"
-        else:
-            expiryStr = ""
-
-        cutoff = now - self.keySloppiness
-	dirs = [ os.path.join(self.keyDir,"key_"+name)
-                  for va, vu, name in self.keyIntervals if vu < cutoff ]
-
-	for dirname, (va, vu, name) in zip(dirs, self.keyIntervals):
-            LOG.info("Removing%s key %s (valid from %s through %s)",
-                        expiryStr, name, formatDate(va), formatDate(vu-3600))
-	    files = [ os.path.join(dirname,f)
-                                 for f in os.listdir(dirname) ]
-	    secureDelete(files, blocking=1)
-	    os.rmdir(dirname)
-
-	self.checkKeys()
-
-    def _getLiveKey(self, when=None):
-	"""Find the first key that is now valid.  Return (Valid-after,
-	   valid-util, name)."""
-        if not self.keyIntervals:
-	    self.liveKey = None
-	    self.nextKeyRotation = 0
-	    return None
-
-	w = when
-	if when is None:
-	    when = time.time()
-	    if when < self.nextKeyRotation:
-		return self.liveKey
-
-	idx = bisect.bisect(self.keyIntervals, (when, None, None))-1
-	k = self.keyIntervals[idx]
-	if w is None:
-	    self.liveKey = k
-	    self.nextKeyRotation = k[1]
-
-	return k
-
-    def getNextKeyRotation(self):
-	"""Return the expiration time of the current key"""
-        return self.nextKeyRotation
-
-    def getServerKeyset(self):
-	"""Return a ServerKeyset object for the currently live key."""
-	# FFFF Support passwords on keys
-	_, _, name = self._getLiveKey()
-	keyset = ServerKeyset(self.keyDir, name, self.hashDir)
-	keyset.load()
-	return keyset
-
-    def getDHFile(self):
-	"""Return the filename for the diffie-helman parameters for the
-	   server.  Creates the file if it doesn't yet exist."""
-	dhdir = os.path.join(self.homeDir, 'work', 'tls')
-	createPrivateDir(dhdir)
-	dhfile = os.path.join(dhdir, 'dhparam')
-        if not os.path.exists(dhfile):
-            LOG.info("Generating Diffie-Helman parameters for TLS...")
-            mixminion._minionlib.generate_dh_parameters(dhfile, verbose=0)
-            LOG.info("...done")
-	else:
-	    LOG.debug("Using existing Diffie-Helman parameter from %s",
-			   dhfile)
-
-        return dhfile
-
-    def getTLSContext(self):
-	"""Create and return a TLS context from the currently live key."""
-        keys = self.getServerKeyset()
-        return mixminion._minionlib.TLSContext_new(keys.getCertFileName(),
-						   keys.getMMTPKey(),
-						   self.getDHFile())
-
-    def getPacketHandler(self):
-	"""Create and return a PacketHandler from the currently live key."""
-        keys = self.getServerKeyset()
-	packetKey = keys.getPacketKey()
-	hashlog = mixminion.server.HashLog.HashLog(keys.getHashLogFileName(),
-						 keys.getMMTPKeyID())
-        return mixminion.server.PacketHandler.PacketHandler(packetKey,
-						     hashlog)
+from mixminion.Common import LOG, MixError, MixFatalError, ceilDiv, \
+     formatBase64, formatTime
 
 class IncomingQueue(mixminion.server.Queue.DeliveryQueue):
     """A DeliveryQueue to accept messages from incoming MMTP connections,
@@ -336,7 +44,8 @@
 
     def queueMessage(self, msg):
 	"""Add a message for delivery"""
-	LOG.trace("Inserted message %r into incoming queue", msg[:8])
+	LOG.trace("Inserted message %s into incoming queue",
+		  formatBase64(msg[:8]))
 	self.queueDeliveryMessage(None, msg)
 
     def _deliverMessages(self, msgList):
@@ -347,16 +56,15 @@
 		res = ph.processMessage(message)
 		if res is None:
 		    # Drop padding before it gets to the mix.
-		    LOG.debug("Padding message %r dropped", 
-				   message[:8])
+		    LOG.debug("Padding message %s dropped",
+			      formatBase64(message[:8]))
 		else:
-		    LOG.debug("Processed message %r; inserting into pool",
-				   message[:8])
+		    LOG.debug("Processed message %s; inserting into pool",
+			      formatBase64(message[:8]))
 		    self.mixPool.queueObject(res)
 		    self.deliverySucceeded(handle)
 	    except mixminion.Crypto.CryptoError, e:
-		LOG.warn("Invalid PK or misencrypted packet header: %s",
-			      e)
+		LOG.warn("Invalid PK or misencrypted packet header: %s", e)
 		self.deliveryFailed(handle)
 	    except mixminion.Packet.ParseError, e:
 		LOG.warn("Malformed message dropped: %s", e)
@@ -366,8 +74,8 @@
 		self.deliveryFailed(handle)
 
 class MixPool:
-    """Wraps a mixminion.server.Queue.*MixQueue to send messages to an exit queue
-       and a delivery queue."""
+    """Wraps a mixminion.server.Queue.*MixQueue to send messages to an exit
+       queue and a delivery queue."""
     def __init__(self, queue):
 	"""Create a new MixPool to wrap a given *MixQueue."""
 	self.queue = queue
@@ -392,20 +100,20 @@
 	"""Get a batch of messages, and queue them for delivery as
 	   appropriate."""
 	handles = self.queue.getBatch()
-	LOG.debug("Mixing %s messages out of %s", 
+	LOG.debug("Mixing %s messages out of %s",
 		       len(handles), self.queue.count())
 	for h in handles:
 	    tp, info = self.queue.getObject(h)
 	    if tp == 'EXIT':
 		rt, ri, app_key, tag, payload = info
-		LOG.debug("  (sending message %r to exit modules)", 
-			       payload[:8])
+		LOG.debug("  (sending message %s to exit modules)",
+			  formatBase64(payload[:8]))
 		self.moduleManager.queueMessage(payload, tag, rt, ri)
 	    else:
 		assert tp == 'QUEUE'
 		ipv4, msg = info
-		LOG.debug("  (sending message %r to MMTP server)", 
-			       msg[:8])
+		LOG.debug("  (sending message %s to MMTP server)",
+			  formatBase64(msg[:8]))
 		self.outgoingQueue.queueDeliveryMessage(ipv4, msg)
 	    self.queue.removeMessage(h)
 
@@ -457,17 +165,17 @@
        all timed events."""
     ## Fields:
     # config: The ServerConfig object for this server
-    # keyring: The ServerKeyring
+    # keyring: The mixminion.server.ServerKeys.ServerKeyring
     #
-    # mmtpServer: Instance of mixminion.ServerMain._MMTPServer.  Receives 
+    # mmtpServer: Instance of mixminion.ServerMain._MMTPServer.  Receives
     #    and transmits packets from the network.  Places the packets it
     #    receives in self.incomingQueue.
-    # incomingQueue: Instance of IncomingQueue.  Holds received packets 
+    # incomingQueue: Instance of IncomingQueue.  Holds received packets
     #    before they are decoded.  Decodes packets with PacketHandler,
     #    and places them in mixPool.
-    # packetHandler: Instance of PacketHandler.  Used by incomingQueue to 
+    # packetHandler: Instance of PacketHandler.  Used by incomingQueue to
     #    decrypt, check, and re-pad received packets.
-    # mixPool: Instance of MixPool.  Holds processed messages, and 
+    # mixPool: Instance of MixPool.  Holds processed messages, and
     #    periodically decides which ones to deliver, according to some
     #    batching algorithm.
     # moduleManager: Instance of ModuleManager.  Map routing types to
@@ -478,7 +186,7 @@
 	"""Create a new server from a ServerConfig."""
 	LOG.debug("Initializing server")
 	self.config = config
-	self.keyring = ServerKeyring(config)
+	self.keyring = mixminion.server.ServerKeys.ServerKeyring(config)
 	if self.keyring._getLiveKey() is None:
 	    LOG.info("Generating a month's worth of keys.")
 	    LOG.info("(Don't count on this feature in future versions.)")
@@ -506,7 +214,7 @@
 	incomingDir = os.path.join(queueDir, "incoming")
 	LOG.trace("Initializing incoming queue")
 	self.incomingQueue = IncomingQueue(incomingDir, self.packetHandler)
-	LOG.trace("Found %d pending messages in incoming queue", 
+	LOG.trace("Found %d pending messages in incoming queue",
 		       self.incomingQueue.count())
 
 	mixDir = os.path.join(queueDir, "mix")
@@ -600,7 +308,7 @@
 
 def readConfigFile(configFile):
     try:
-	return mixminion.Config.ServerConfig(fname=configFile)
+	return mixminion.server.ServerConfig.ServerConfig(fname=configFile)
     except (IOError, OSError), e:
 	print >>sys.stderr, "Error reading configuration file %r:"%configFile
 	print >>sys.stderr, "   ", str(e)
@@ -670,7 +378,7 @@
 
     LOG.setMinSeverity("INFO")
     mixminion.Crypto.init_crypto(config)
-    keyring = ServerKeyring(config)
+    keyring = mixminion.server.ServerKeys.ServerKeyring(config)
     print >>sys.stderr, "Creating %s keys..." % keys
     for i in xrange(keys):
 	keyring.createKeys(1)
@@ -704,7 +412,7 @@
     config = readConfigFile(configFile)
     mixminion.Common.configureShredCommand(config)
     LOG.setMinSeverity("INFO")
-    keyring = ServerKeyring(config)
+    keyring = mixminion.server.ServerKeys.ServerKeyring(config)
     keyring.checkKeys()
     # This is impossibly far in the future.
     keyring.removeDeadKeys(now=(1L << 36))