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

[minion-cvs] Base implementation of server descriptor blocks.



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

Modified Files:
	BuildMessage.py Config.py Crypto.py ServerInfo.py test.py 
Log Message:
Base implementation of server descriptor blocks.

BuildMessage.py: use getPublicKey instead of getModulus.

Config.py:
	- Add validation functions for more types: base64, hex, publicKey
	- Add 'restricted-format' mode
	- Pass file contents to validate function

Crypto.py:
	- Expose more functions from _minionlib.
	- Fix portability bug; run on Python 2.0 again.

ServerInfo.py:
	- Untested ServerInfo implementation
	- Add beginnings of ServerInfo generation/keygen functionality

test.py:
	- Use FakeServerInfo for testing
	- Tests for newly exposed Crypto functionality
	- Be more verbose when we hang for DH parameter generation
	- Tests for new validation functions (but not _parsePublicKey yet)



Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -d -r1.9 -r1.10
--- BuildMessage.py	1 Jul 2002 18:03:05 -0000	1.9
+++ BuildMessage.py	26 Jul 2002 20:52:17 -0000	1.10
@@ -242,7 +242,7 @@
         extHeaders = "".join(subhead.getExtraBlocks())
         rest = Crypto.ctr_crypt(extHeaders+header, headerKeys[i])
         subhead.digest = Crypto.sha1(rest+junkSeen[i])
-        pubkey = Crypto.pk_from_modulus(path[i].getModulus())
+        pubkey = path[i].getPacketKey()
         esh = Crypto.pk_encrypt(subhead.pack(), pubkey)
         header = esh + rest
 

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- Config.py	26 Jul 2002 15:47:20 -0000	1.4
+++ Config.py	26 Jul 2002 20:52:17 -0000	1.5
@@ -38,11 +38,14 @@
 
 import os
 import re
+import binascii
+import time
 from cStringIO import StringIO
 
 import mixminion.Common
 from mixminion.Common import MixError, getLog
 import mixminion.Packet
+import mixminion.Crypto
 
 #----------------------------------------------------------------------
 
@@ -187,13 +190,78 @@
             
         raise ConfigError("No match found for command %r" %cmd)
 
+_allChars = "".join(map(chr, range(256)))
+def _parseBase64(s,_hexmode=0):
+    """Validation function.  Converts a base-64 encoded config value into
+       its original. Raises ConfigError on failure."""  
+    s = s.translate(_allChars, " \t\v\n")
+    try:
+	if _hexmode:
+	    return binascii.a2b_hex(s)
+	else:
+	    return binascii.a2b_base64(s)
+    except (TypeError, binascii.Error, binascii.Incomplete), e:
+	raise ConfigError("Invalid Base64 data")
+
+def _parseHex(s):
+    """Validation function.  Converts a hex-64 encoded config value into
+       its original. Raises ConfigError on failure."""  
+    return _parseBase64(s,1)
+
+def _parsePublicKey(s):
+    """Validate function.  Converts a Base-64 encoding of an ASN.1
+       represented RSA public key with modulus 65535 into an RSA
+       object."""
+    asn1 = _parseBase64(s)
+    if len(asn1) > 550:
+	raise ConfigError("Overlong public key")
+    try:
+	key = mixminion.Crypto.pk_decode_public_key(asn1)
+    except mixminion.Crypto.CryptoError:
+	raise ConfigError("Invalid public key")
+    if key.get_public_key()[1] != 65535:
+	raise ConfigError("Invalid exponent on public key")
+    return key
+
+_date_re = re.compile(r"(\d\d)/(\d\d)/(\d\d\d\d)")
+_time_re = re.compile(r"(\d\d)/(\d\d)/(\d\d\d\d) (\d\d):(\d\d):(\d\d)")
+def _parseDate(s,_timeMode=0):
+    """Validation function.  Converts from DD/MM/YYYY format to a (long)
+       time value for midnight on that date."""
+    s = s.strip()
+    r = (_date_re, _time_re)[_timeMode]
+    m = r.match(s)
+    if not m:
+	raise ConfigError("Invalid %s %r" % (("date", "time")[_timeMode],s))
+    if _timeMode:
+	dd, MM, yyyy, hh, mm, ss = map(int, m.groups())
+    else:
+	dd, MM, yyyy = map(int, m.groups())
+	hh, mm, ss = 0, 0, 0	
+
+    if not ((1 <= dd <= 31) and (1 <= MM <= 12) and
+	    (1970 <= yyyy)  and (0 <= hh < 24) and
+	    (0 <= mm < 60)  and (0 <= ss <= 61)):
+	raise ConfigError("Invalid %s %r" % (("date","time")[_timeMode],s))
+
+    
+    # we set the DST flag to zero so that subtracting time.timezone always
+    # gives us gmt.
+    return time.mktime((yyyy,MM,dd,hh,mm,ss,0,0,0))-time.timezone
+
+def _parseTime(s):
+    """Validation function.  Converts from DD/MM/YYYY HH:MM:SS format
+       to a (float) time value for GMT."""
+    return _parseDate(s,1)
+
 #----------------------------------------------------------------------
 
 # Regular expression to match a section header.
 _section_re = re.compile(r'\[([^\]]+)\]')
 # Regular expression to match the first line of an entry
 _entry_re = re.compile(r'([^:= \t]+)(?:\s*[:=]|[ \t])\s*(.*)')
-def _readConfigLine(line):
+_restricted_entry_re = re.compile(r'([^:= \t]+): (.*)')
+def _readConfigLine(line, restrict=0):
     """Helper function.  Given a line of a configuration file, return
        a (TYPE, VALUE) pair, where TYPE is one of the following:
 
@@ -220,14 +288,16 @@
     elif space:
         return "MORE", line
     else:
-        m = _entry_re.match(line)
+	if restrict:
+	    m = _restricted_entry_re.match(line)
+	else:
+	    m = _entry_re.match(line)
         if not m:
             return "ERR", "Bad entry"
         return "ENT", (m.group(1), m.group(2))
 
-def _readConfigFile(file):
-    """Helper function. Given an open file object for a configuration
-       file, parse it into sections.
+def _readConfigFile(contents, restrict=0):
+    """Helper function. Given the string contents of a configuration
 
        Returns a list of (SECTION-NAME, SECTION) tuples, where each
        SECTION is a list of (KEY, VALUE, LINENO) tuples.
@@ -238,9 +308,14 @@
     curSection = None
     lineno = 0
     lastKey = None
-    for line in file.readlines():
+    
+    fileLines = contents.split("\n")
+    if fileLines[-1] == '':
+	del fileLines[-1]
+
+    for line in fileLines:
         lineno += 1
-        type, val = _readConfigLine(line)
+        type, val = _readConfigLine(line, restrict)
         if type == 'ERR':
             raise ConfigError("%s at line %s" % (val, lineno))
         elif type == 'SEC':
@@ -253,9 +328,15 @@
             curSection.append( [key, val, lineno] )
             lastKey = key
         elif type == 'MORE':
+	    if restrict:
+		raise ConfigError("Continuation not allowed at line %s"%lineno)
             if not lastKey:
                 raise ConfigError("Unexpected indentation at line %s" %lineno)
             curSection[-1][1] = "%s %s" % (curSection[-1][1], val)
+	else:
+	    assert type is None
+	    if restrict:
+		raise ConfigError("Empty line not allowed at line %s"%lineno)
     return sections
 
 def _formatEntry(key,val,w=79,ind=4):
@@ -292,6 +373,8 @@
     #                               (ALLOW/REQUIRE/ALLOW*/REQUIRE*,
     #                                 parseFn,
     #                                 default, ) }
+    #     _restrictFormat is 1/0: do we allow full RFC822ness, or do
+    #         we insist on a tight data format?
     
     ## Validation rules:
     # A key without a corresponding entry in _syntax gives an error.
@@ -307,7 +390,7 @@
 
     def __init__(self, fname=None, string=None):
         """Create a new _ConfigFile.  If fname is set, read from
-           fname.  If string is set, parse string."""
+           fname.  If string is set, parse string. """
         assert fname is None or string is None
         self.fname = fname
         if fname:
@@ -343,7 +426,8 @@
 
     def __reload(self, file):
         """As in .reload(), but takes an open file object."""
-        sections = _readConfigFile(file)
+	fileContents = file.read()
+        sections = _readConfigFile(fileContents, self._restrictFormat)
 
         # These will become self.(_sections,_sectionEntries,_sectionNames)
         # if we are successful.
@@ -352,7 +436,7 @@
         self_sectionNames = []
         sectionEntryLines = {}
 
-        for secName, secEntries in  sections:
+        for secName, secEntries in sections:
             self_sectionNames.append(secName)
 
             if self_sections.has_key(secName):
@@ -440,13 +524,15 @@
                 assert v == self_sections[s][k] or v in self_sections[s][k]
 
         # Call our validation hook.
-        self.validate(self_sections, self_sectionEntries, sectionEntryLines)
+        self.validate(self_sections, self_sectionEntries, sectionEntryLines,
+		      fileContents)
 
         self._sections = self_sections
         self._sectionEntries = self_sectionEntries
         self._sectionNames = self_sectionNames
 
-    def validate(self, sections, sectionEntries, entryLines):
+    def validate(self, sections, sectionEntries, entryLines,
+		 fileContents):
         """Check additional semantic properties of a set of configuration
            data before overwriting old data.  Subclasses should override."""
         pass
@@ -480,6 +566,7 @@
         return "".join(lines)
 
 class ClientConfig(_ConfigFile):
+    _restrictFormat = 0
     _syntax = {
         'Host' : { '__SECTION__' : ('REQUIRE', None, None),
                    'ShredCommand': ('ALLOW', _parseCommand, None),
@@ -497,11 +584,12 @@
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self, fname, string)
 
-    def validate(self, sections, entries, lines):
+    def validate(self, sections, entries, lines, contents):
         #XXXX Write this
         pass
 
 class ServerConfig(_ConfigFile):
+    _restrictFormat = 0
     _syntax = {
         'Host' : ClientConfig._syntax['Host'], 
         'Server' : { '__SECTION__' : ('REQUIRE', None, None),
@@ -520,7 +608,7 @@
                                'MaxSkew' : ('ALLOW', _parseInterval,
                                             "10 minutes",) }, 
         'Incoming/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
-                            'IP' : ('ALLOW', _parseIP, None),
+			    'IP' : ('ALLOW', _parseIP, None),
                             'Port' : ('ALLOW', _parseInt, "48099"),
                             'Allow' : ('ALLOW*', None, None),
                             'Deny' : ('ALLOW*', None, None) },
@@ -536,26 +624,7 @@
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self, fname, string)
 
-    def validate(self, sections, entries, lines):
+    def validate(self, sections, entries, lines, contents):
         #XXXX write this.
         pass
     
-## _serverDescriptorSyntax = {
-##     'Server' : { 'Descriptor-Version' : 'REQUIRE',
-##                  'IP' : 'REQUIRE',
-##                  'Nickname' : 'ALLOW',
-##                  'Identity' : 'REQUIRE',
-##                  'Digest' : 'REQUIRE',
-##                  'Signature' : 'REQUIRE',
-##                  'Valid-After' : 'REQUIRE',
-##                  'Valid-Until' : 'REQUIRE',
-##                  'Contact' : 'ALLOW',
-##                  'Comments' : 'ALLOW',
-##                  'Packet-Key' : 'REQUIRE',  },
-##     'Incoming/MMTP' : { 'MMTP-Descriptor-Version' : 'REQUIRE',
-##                         'Port' :  'REQUIRE',
-##                         'Key-Digest' : 'REQUIRE', },
-##     'Modules/MMTP' : { 'MMTP-Descriptor-Version' : 'REQUIRE',
-##                        'Allow' : 'ALLOW*',
-##                        'Deny' : 'ALLOW*' }
-##     }

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -d -r1.9 -r1.10
--- Crypto.py	26 Jul 2002 15:47:20 -0000	1.9
+++ Crypto.py	26 Jul 2002 20:52:17 -0000	1.10
@@ -18,7 +18,8 @@
 
 __all__ = [ 'CryptoError', 'init_crypto', 'sha1', 'ctr_crypt', 'prng',
             'strxor', 'lioness_encrypt', 'lioness_decrypt', 'trng',
-            'pk_encrypt', 'pk_decrypt', 'pk_generate', 'openssl_seed',
+            'pk_encrypt', 'pk_decrypt', 'pk_sign', 'pk_check_signature',
+	    'pk_generate', 'openssl_seed',
             'pk_get_modulus', 'pk_from_modulus',
             'pk_encode_private_key', 'pk_decode_private_key',
             'Keyset', 'AESCounterPRNG', 'HEADER_SECRET_MODE',
@@ -27,6 +28,8 @@
             'HIDE_HEADER_MODE' ]
 
 CryptoError = _ml.CryptoError
+generate_cert = _ml.generate_cert
+PEM_read_key = _ml.rsa_PEM_read_key
 
 # Number of bytes in an AES key.
 AES_KEY_LEN = 128 >> 3
@@ -133,6 +136,12 @@
     # public key encrypt
     return key.crypt(data, 1, 1)
 
+def pk_sign(data, key):
+    """XXXX"""
+    bytes = key.get_modulus_bytes()
+    data = add_oaep(data,OAEP_PARAMETER,bytes)
+    return key.crypt(data, 0, 1)
+
 def pk_decrypt(data,key):
     """Returns the unpadded RSA decryption of data, using the private key in\n
        key
@@ -142,6 +151,13 @@
     data = key.crypt(data, 0, 0)
     return check_oaep(data,OAEP_PARAMETER,bytes)
 
+def pk_check_signature(data, key):
+    """XXXX"""
+    bytes = key.get_modulus_bytes()
+    # private key decrypt
+    data = key.crypt(data, 1, 0)
+    return check_oaep(data,OAEP_PARAMETER,bytes)
+
 def pk_generate(bits=1024,e=65535):
     """Generate a new RSA keypair with 'bits' bits and exponent 'e'.  It is
        safe to use the default value of 'e'.
@@ -164,6 +180,14 @@
     """Reads an ASN1 representation of a keypair from external storage."""
     return _ml.rsa_decode_key(s,0)
 
+def pk_encode_public_key(key):
+    """Creates an ASN1 representation of a public key for external storage."""
+    return key.encode_key(1)
+
+def pk_decode_public_key(s):
+    """Reads an ASN1 representation of a public key from external storage."""
+    return _ml.rsa_decode_key(s,1)
+
 #----------------------------------------------------------------------
 # OAEP Functionality
 #
@@ -423,7 +447,7 @@
         file = None
     else:
         st = os.stat(file)
-        if not (st.st_mode & stat.S_IFCHR):
+        if not (st[stat.ST_MODE] & stat.S_IFCHR):
             getLog().error("Entropy source %s isn't a character device", file)
             file = None
 

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- ServerInfo.py	24 Jun 2002 20:28:19 -0000	1.5
+++ ServerInfo.py	26 Jul 2002 20:52:17 -0000	1.6
@@ -6,33 +6,296 @@
    Data structures to represent a server's information, and functions to
    martial and unmarshal it.
 
-   ???? Since we don't have an interchange format yet, we only have
-   an object with the minimal info."""
+   """
 
 __all__ = [ 'ServerInfo' ]
 
+import time
+
 from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
 from mixminion.Packet import IPV4Info
+import mixminion.Config
+import mixminion.Crypto
 
-#
-# Stub class till we have the real thing
-#
-class ServerInfo:
-    """Represents a Mixminion server, and the information needed to send
-       messages to it."""
-    def __init__(self, addr, port, modulus, keyid):
-        self.addr = addr
-        self.port = port
-        self.modulus = modulus
-        self.keyid = keyid
+ConfigError = mixminion.Config.ConfigError
 
-    def getAddr(self): return self.addr
-    def getPort(self): return self.port
-    def getModulus(self): return self.modulus
-    def getKeyID(self): return self.keyid
+# tmp variable to make this easier to spell.
+C = mixminion.Config
+
+MAX_NICKNAME = 128
+MAX_CONTACT = 256
+MAX_COMMENT = 1024
+MIN_IDENTITY_BYTES = 2048 >> 3
+MAX_IDENTITY_BYTES = 4096 >> 3
+PACKET_KEY_BYTES = 1024 >> 3
+
+class ServerInfo(mixminion.Config._ConfigFile):
+    _restrictFormat = 1
+    _syntax = {
+	"Server" : { "__SECTION__": ("REQUIRE", None, None),
+		     "IP": ("REQUIRE", C._parseIP, None),
+		     "Nickname": ("REQUIRE", None, None),
+		     "Identity": ("REQUIRE", C._parsePublicKey, None),
+		     "Digest": ("REQUIRE", C._parseBase64, None),
+		     "Signature": ("REQUIRE", C._parseBase64, None),
+		     "Published": ("REQUIRE", C._parseTime, None),
+		     "Valid-After": ("REQUIRE", C._parseDate, None),
+		     "Valid-Until": ("REQUIRE", C._parseDate, None),
+		     "Contact": ("ALLOW", None, None),
+		     "Comments": ("ALLOW", None, None),
+		     "Packet-Key": ("REQUIRE", C._parsePublicKey, None),
+		     },
+	"Incoming/MMTP" : {
+ 	             "Version": ("REQUIRE", None, None),
+		     "Port": ("REQUIRE", C._parseInt, None),
+		     "Key-Digest": ("REQUIRE", C._parseBase64, None),
+		     "Protocols": ("REQUIRE", None, None),
+		     },
+	"Modules/MMTP" : {
+ 	             "Version": ("REQUIRE", None, None),
+		     "Protocols": ("REQUIRE", None, None),
+		     },
+	"Modules/MBOX" : {
+   	             "Version": ("REQUIRE", None, None),
+		     },
+	"Modules/SMTP" : {
+           	     "Version": ("REQUIRE", None, None),
+		     }
+	}
+
+    def __init__(self, fname, string):
+	mixminion.Config._ConfigFile.__init__(self, fname, string)
+
+    def validate(self, sections, entries, lines, contents):
+	####
+	# Check 'Server' section.
+
+	server = sections['Server']
+	if server['Descriptor-Version'] != '1.0':
+	    raise ConfigError("Unrecognized descriptor version")
+	if len(server['Nickname']) > MAX_NICKNAME:
+	    raise ConfigError("Nickname too long")
+	identityKey = server['Identity-Key']
+	identityBytes = identityKey.get_modulus_bytes()
+	if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):
+	    raise ConfigError("Invalid length on identity key")
+	if server['Valid-Until'] <= server['Valid-After']:
+	    raise ConfigError("Server is never valid")
+	if len(server['Contact']) > MAX_CONTACT:
+	    raise ConfigError("Contact too long")
+	if len(sever['Comments']) > MAX_COMMENTS:
+	    raise ConfigError("Comments too long")
+	packetKeyBytes = server['Packet-Key'].get_modulus_bytes()
+	if packetKeyBytes != PACKET_KEY_BYTES:
+	    raise ConfigError("Invalid length on packet key")
+
+	####
+	# Check Digest of file
+	digest = getServerInfoDigest(contents)
+	if digest != server['Digest']:
+	    raise ConfigError("Invalid digest")
+	
+	signature = server['']
+	if digest != mixminion.Crypto.pk_check_signature(server['Signature'],
+							 identityKey):
+	    raise ConfigError("Invalid signature")
+
+	#### XXXX CHECK OTHER SECTIONS
+
+    def getAddr(self):
+	return self['Server']['IP']
     
+    def getPort(self):
+	return self['Incoming/MMTP']['Port']
+    
+    def getPacketKey(self):
+	return self['Server']['Packet-Key']
+
+    def getKeyID(self):
+	return self['Incoming/MMTP']['Key-Digest']
+
     def getRoutingInfo(self):
         """Returns a mixminion.Packet.IPV4Info object for routing messages
            to this server."""
-        return IPV4Info(self.addr, self.port, self.keyid)
+        return IPV4Info(self.getAddr(), self.getPort(), self.getKeyID())
 
+#----------------------------------------------------------------------
+# This should go in a different file.
+class ServerKeys:
+    "XXXX"
+    def __init__(self, keyroot, keyname, hashroot):
+	self.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")
+
+    def load(self, password=None):
+	r = mixminion.Crypto.PEM_read_key
+	if password:
+	    self.packetKey = r(self.packetKeyFile,0,password)
+	    self.mmtpKey = r(self.mmtpKeyFile,0,password)
+	else:
+	    self.packetKey = r(self.packetKeyFile,0)
+	    self.mmtpKey = r(self.mmtpKeyFile,0)
+
+    def save(self, pasword=None):
+	if password:
+	    self.packetKey.PEM_write_key(self.packetKeyFile,0,password)
+	    self.mmtpKey.PEM_write_key(self.mmtpKeyFile,0,password)
+	else:
+	    self.packetKey.PEM_write_key(self.packetKeyFile,0)
+	    self.mmtpKey.PEM_write_key(self.mmtpKeyFile,0)
+
+    def getCertFileName(self): return self.certFile
+    def getHashLogFileName(self): return self.hashlogFile
+    def getPacketKey(self): return self.packetKey
+    def getMMTPKey(self): return self.mmtpKey
+    def getMMTPKeyID(self): 
+	return sha1(self.mmtpKey.encode_key(1))
+
+def _base64(s):
+    return binascii.b2a_base64(s).replace("\n","")
+
+def _time(t):
+    gmt = time.gmtime(t)
+    return "%02d/%02s/%04d %02d:%02d:%02d" % (
+	gmt[2],gmt[1],gmt[0],  gmt[3],gmt[4],gmt[5])
+
+def _date(t):
+    gmt = time.gmtime(t+1)
+    return "%02d/%02s/%04d" % (gmt[2],gmt[1],gmt[0])
+
+def generateNewServerInfoAndKeys(config, identityKey, keydir, keyname):
+    packetKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
+    mmtpKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
+    
+    serverKeys = ServerKeys(keydir, keyname)
+    serverKeys.packetKey = packetKey
+    serverKeys.mmtpKey = mmtpKey
+    serverKeys.save()
+
+    nickname = "XXXX" #XXXX"
+    contact = "XXXX"
+    comment = "XXXX"
+    validAt = time.time() #XXXX
+    validUntil = time.time()+365*24*60*60 #XXXX
+    lifespan = ceilDiv(validUntil-validAt , 24*60*60)#XXXX
+    
+    mixminion.Crypto.generate_cert(serverKeys.getCertFileName(),
+				   mmtpKey,
+				   lifespan, 
+				   "MMTP certificate for %s" %nickname)
+    
+    if not config['Server']['Incoming/MMTP']:
+	# Don't generate a serverInfo if we don't allow connections in.
+	return
+
+    fields = {
+	"IP": config['Incoming/MMTP']['IP'],
+	"Port": config['Incoming/MMTP']['Port'],
+	"Nickname": nickname,
+	"Identity": 
+	   _base64(mixminion.Crypto.pk_encode_public_key(identityKey)),
+	"Published": _time(time.time()),
+	"ValidAfter": _date(validAt),
+	"ValidUntil": _date(validUntil),
+	"PacketKey":
+  	   _base64(mixminion.Crypto.pk_encode_public_key(publicKey)),
+	"KeyID":
+	   _base64(serverKeys.getMMTPKeyID()),
+	}
+	
+    info = """\
+        [Server]
+	Descriptor-Version: 1.0
+        IP: %(IP)s
+        Port: %(Port)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 comment:
+	info += "Contact %s\n"%comment
+            
+    if ALLOW_INCOMING_MMTP: #XXXX
+	info += """\
+            [Incoming/MMTP]
+            Version: 1.0
+            Port: %(Port)s
+	    Key-Digest: %(KeyID)s
+	    Protocols: 1.0
+            """
+    if ALLOW_OUTGOING_MMTP: #XXXX
+	info += """\
+            [Modules/MMTP]
+	    Version: 1.0
+            Protocols: 1.0
+            """
+        for k,v in config.getSectionItems("Outgoing/MMTP"):
+	    # XXXX write the rule
+	    pass
+    if ALLOW_DELIVERY_MBOX: #XXXX
+	info += """\
+            [Modules/MBOX]
+            Version: 1.0
+            """
+	    
+    # Remove extra (leading) whitespace.
+    lines = [ line.strip() for line in info.split("\n") ]
+    # Remove empty lines
+    lines = filter(None, lines)
+    info = "\n".join(lines)
+    info = signServerInfo(info, identityKey)
+    
+    # debug XXXX
+    ServerInfo(string=info)
+
+    return info
+    
+
+#----------------------------------------------------------------------
+def getServerInfoDigest(info):
+    return _getServerInfoDiggestImpl(info, None)
+
+def signServerInfo(info, rsa):
+    return _getServerInfoDiggestImpl(info, rsa)
+
+def _getServerInfoDigestImpl(info, rsa=None):
+    infoLines = info.split("\n")
+    if not infoLines[0] == "[Server]":
+	raise ConfigError("Must begin with server section")
+    digestLine = None
+    signatureLine = None
+    infoLines = info.split("\n")
+    for lineno in range(len(infoLines)):
+	line = infoLines[lineNo]
+	if line.startswith("Digest:") and digestLine is None:
+	    digestLine = lineNo
+	elif line.startswith("Signature:") and signatureLine is None:
+	    signatureLine = lineNo
+    
+    assert digestLine is not None and signatureLine is not None
+
+    infoLines[digestLine] = 'Digest:'
+    infoLines[signatureLine] = 'Signature:'
+    info = "\n".join(infoLines)
+
+    digest = mixminion.Crypto.sha1(info)
+    if pk is None:
+	return digest
+
+    #### Signature case.
+    signature = mixminion.Crypto.pk_sign(digest,rsa)
+    digest = _base64(digest)
+    signature = binascii.b2a_base64(signature).replace("\n","")
+    infoLines[digestLine] = 'Digest: '+digest
+    infoLines[signatureLine] = 'Signature: '+signature
+
+    return "\n".join(infoLines)

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -u -d -r1.13 -r1.14
--- test.py	26 Jul 2002 15:47:20 -0000	1.13
+++ test.py	26 Jul 2002 20:52:17 -0000	1.14
@@ -322,6 +322,16 @@
                     k512.crypt(pk_encrypt(msg,k512), 0, 0),
                     mixminion.Crypto.OAEP_PARAMETER, 64))
 
+	# test signing
+	eq(pk_check_signature(pk_sign(msg, k1024),pub1024), msg)
+	eq(pk_check_signature(pk_sign(msg, k1024),k1024), msg)
+	self.failUnlessRaises(TypeError,
+			      pk_sign, msg, pub1024)
+	self.failUnlessRaises(CryptoError,
+			      pk_check_signature,
+			      pk_sign(msg, k1024)+"X",
+			      pub1024)
+
         # Make sure we can still encrypt after we've encoded/decoded a
         # key.
         encoded = pk_encode_private_key(k512)
@@ -616,19 +626,34 @@
     pk2 = pk_generate()
     pk3 = pk_generate()
 
-from mixminion.ServerInfo import ServerInfo
+    
+class FakeServerInfo:
+    """Represents a Mixminion server, and the information needed to send
+       messages to it."""
+    def __init__(self, addr, port, key, keyid):
+        self.addr = addr
+        self.port = port
+        self.key = key
+        self.keyid = keyid
+
+    def getAddr(self): return self.addr
+    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."""
+        return IPV4Info(self.addr, self.port, self.keyid)
 
 class BuildMessageTests(unittest.TestCase):
     def setUp(self):
         self.pk1 = BMTSupport.pk1
         self.pk2 = BMTSupport.pk2
         self.pk3 = BMTSupport.pk3
-        n_1 = pk_get_modulus(self.pk1)
-        n_2 = pk_get_modulus(self.pk2)
-        n_3 = pk_get_modulus(self.pk3)
-        self.server1 = ServerInfo("127.0.0.1", 1, n_1, "X"*20)
-        self.server2 = ServerInfo("127.0.0.2", 3, n_2, "Z"*20)
-        self.server3 = ServerInfo("127.0.0.3", 5, n_3, "Q"*20)
+        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)
 
     def test_buildheader_1hop(self):
         bhead = BuildMessage._buildHeader
@@ -747,7 +772,7 @@
         def getLongRoutingInfo(longStr2=longStr2):
             return LocalInfo("fred",longStr2)
 
-        server4 = ServerInfo("127.0.0.1", 1, pk_get_modulus(self.pk1), "X"*20)
+        server4 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         server4.getRoutingInfo = getLongRoutingInfo
 
         secrets.append("1"*16)
@@ -966,12 +991,10 @@
         self.tmpfile = mktemp(".db")
         unlink_db_on_exit(self.tmpfile)
         h = self.hlog = HashLog(self.tmpfile, "Z"*20)
-        n_1 = pk_get_modulus(self.pk1)
-        n_2 = pk_get_modulus(self.pk2)
-        n_3 = pk_get_modulus(self.pk3)
-        self.server1 = ServerInfo("127.0.0.1", 1, n_1, "X"*20)
-        self.server2 = ServerInfo("127.0.0.2", 3, n_2, "Z"*20)
-        self.server3 = ServerInfo("127.0.0.3", 5, n_3, "Q"*20)
+
+        self.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)
         self.sp1 = PacketHandler(self.pk1, h)
         self.sp2 = PacketHandler(self.pk2, h)
         self.sp3 = PacketHandler(self.pk3, h)
@@ -1065,7 +1088,7 @@
         from mixminion.PacketHandler import ContentError
 
         # A long intermediate header needs to fail.
-        server1X = ServerInfo("127.0.0.1", 1, pk_get_modulus(self.pk1), "X"*20)
+        server1X = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         class _packable:
             def pack(self): return "x"*200
         server1X.getRoutingInfo = lambda _packable=_packable: _packable()
@@ -1347,10 +1370,16 @@
             if dh_fname:
                 dhfile = dh_fname
                 if not os.path.exists(dh_fname):
-                    _ml.generate_dh_parameters(dhfile, 0)
+		    print "[Generating DH parameters...",
+		    sys.stdout.flush()
+		    _ml.generate_dh_parameters(dhfile, 0)
+		    print "done.]"
             else:
+		print "[Generating DH parameters (not caching)...",
+		sys.stdout.flush()
                 _ml.generate_dh_parameters(dhfile, 0)
                 unlink_on_exit(dhfile)
+		print "done.]"
             pk = _ml.rsa_generate(1024, 65535)
             pk.PEM_write_key(open(pkfile, 'w'), 0)
             _ml.generate_cert(certfile, pk, 365, "Testing certificate")
@@ -1492,6 +1521,7 @@
 from mixminion.Config import _ConfigFile, ConfigError, _parseInt
 
 class TestConfigFile(_ConfigFile):
+    _restrictFormat = 0
     _syntax = { 'Sec1' : {'__SECTION__': ('REQUIRE', None, None),
                           'Foo': ('REQUIRE', None, None),
                           'Bar': ('ALLOW', None, "default"),
@@ -1644,6 +1674,14 @@
         self.assertEquals(C._parseCommand("rm"), ("/bin/rm", []))
         self.assertEquals(C._parseCommand("/bin/ls"), ("/bin/ls", []))
         self.failUnless(C._parseCommand("python")[0] is not None)
+	self.assertEquals(C._parseBase64(" YW\nJj"), "abc")
+	self.assertEquals(C._parseHex(" C0D0"), "\xC0\xD0")
+	tm = C._parseDate("30/05/2002")
+	self.assertEquals(time.gmtime(tm)[:6], (2002,5,30,0,0,0))
+	tm = C._parseDate("01/01/2000")
+	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)
@@ -1662,6 +1700,15 @@
         fails(C._parseIP, "192.0.0")
         fails(C._parseIP, "192.0.0.0.0")
         fails(C._parseIP, "A.0.0.0")
+	fails(C._parseBase64, "Y")
+	fails(C._parseHex, "Z")
+	fails(C._parseHex, "A")
+	fails(C._parseDate, "1/1/2000")
+	fails(C._parseDate, "01/50/2000")
+	fails(C._parseDate, "01/50/2000 12:12:12")
+	fails(C._parseTime, "01/50/2000 12:12:12")
+	fails(C._parseTime, "01/50/2000 12:12:99")
+
         nonexistcmd = '/file/that/does/not/exist'
         if not os.path.exists(nonexistcmd):
             fails(C._parseCommand, nonexistcmd)