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

[minion-cvs] Implement and test server descriptors.



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

Modified Files:
	Config.py Crypto.py ServerInfo.py test.py 
Log Message:
Implement and test server descriptors.

Config: 
	- Add functionality for allow/deny rules. 
	- Add 'restricted' format for descriptors
	- Add fast path for assumed-valid files
	- Make 'Host' sections of config optional
	- Add more key-management and descriptor-generation fields to 
	  server config.

Crypto:
	- Add wrappers for PEM

ServerInfo:
	- Implement and debug server descriptors

test:
	- Tests for above functionality
	- Tests for logs

crypt.c:
	- Change generate_cert to take a time range instead of a number
	  of days.

tls.c:
	- Remove stale XXXX comment.


Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- Config.py	26 Jul 2002 20:52:17 -0000	1.5
+++ Config.py	28 Jul 2002 22:42:33 -0000	1.6
@@ -32,6 +32,19 @@
    [Section2]
    Key5 value5
       value5 value5 value5
+
+   We also specify a 'restricted' format in which blank lines,
+   comments,  line continuations, and entry formats other than 'key: value'
+   are forbidden.  Example:
+
+   [Section1]
+   Key1: Value1
+   Key2: Value2
+   Key3: Value3
+   [Section2]
+   Key4: Value4
+
+   The restricted format is used for server descriptors.   
    """
 
 __all__ = [ 'getConfig', 'loadConfig', 'addHook' ]
@@ -84,7 +97,7 @@
     # This isn't a method of _Config, since we want to be able to call
     # it before we read the configuration file.
     _CONFIG_HOOKS.append(hook)
-    
+
 #----------------------------------------------------------------------
 
 class ConfigError(MixError):
@@ -104,7 +117,7 @@
 
 def _parseSeverity(severity):
     """Validation function.  Converts a config value to a log severity.
-       Raises ConfigError on failure."""  
+       Raises ConfigError on failure."""
     s = severity.strip().upper()
     if not mixminion.Common._SEVERITIES.has_key(s):
         raise ConfigError("Invalid log level %r" % (severity))
@@ -112,7 +125,7 @@
 
 def _parseServerMode(mode):
     """Validation function.  Converts a config value to a server mode
-       (one of 'relay' or 'local'). Raises ConfigError on failure."""  
+       (one of 'relay' or 'local'). Raises ConfigError on failure."""
     s = mode.strip().lower()
     if s not in ('relay', 'local'):
         raise ConfigError("Server mode must be 'Relay' or 'Local'")
@@ -131,14 +144,14 @@
     'day':    60*60*24,
     'week':   60*60*24*7,
     'mon':    60*60*24*30,
-    'month':  60*60*24*30,    # These last two aren't quite right, but we 
+    'month':  60*60*24*30,    # These last two aren't quite right, but we
     'year':   60*60*24*365,   # don't need exactness.
     }
 _abbrev_units = { 'sec' : 'second', 'min': 'minute', 'mon': 'month' }
 def _parseInterval(interval):
     """Validation function.  Converts a config value to an interval of time,
        in the format (number of units, name of unit, total number of seconds).
-       Raises ConfigError on failure."""  
+       Raises ConfigError on failure."""
     inter = interval.strip().lower()
     m = _interval_re.match(inter)
     if not m:
@@ -150,7 +163,7 @@
 
 def _parseInt(integer):
     """Validation function.  Converts a config value to an int.
-       Raises ConfigError on failure."""  
+       Raises ConfigError on failure."""
     i = integer.strip().lower()
     try:
         return int(i)
@@ -159,7 +172,7 @@
 
 def _parseIP(ip):
     """Validation function.  Converts a config value to an IP address.
-       Raises ConfigError on failure."""  
+       Raises ConfigError on failure."""
     i = ip.strip().lower()
     try:
         f = mixminion.Packet._packIP(i)
@@ -168,9 +181,52 @@
 
     return i
 
+_address_set_re = re.compile(r'''(\d+\.\d+\.\d+\.\d+|\*)
+                                 \s*
+                                 (?:/\s*(\d+\.\d+\.\d+\.\d+))?\s*
+                                 (?:(\d+)\s*
+                                           (?:-\s*(\d+))?
+                                        )?''',re.X)
+def _parseAddressSet_allow(s, allowMode=1):
+    """Validation function.  Converts an address set string of the form
+       IP/mask port-port into a tuple of (IP, Mask, Portmin, Portmax).
+       Raises ConfigError on failure."""
+    s = s.strip()
+    m = _address_set_re.match(s)
+    if not m:
+        raise ConfigError("Misformatted address rule %r", s)
+    ip, mask, port, porthi = m.groups()
+    if ip == '*':
+        if mask != None:
+            raise ConfigError("Misformatted address rule %r", s)
+        ip,mask = '0.0.0.0','0.0.0.0'
+    else:
+        ip = _parseIP(ip)
+    if mask:
+        mask = _parseIP(mask)
+    else:
+        mask = "255.255.255.255"
+    if port:
+        port = _parseInt(port)
+        if porthi:
+            porthi = _parseInt(porthi)
+        else:
+            porthi = port
+        if not 1 <= port <= porthi <= 65535:
+            raise ConfigError("Invalid port range %s-%s" %(port,porthi))
+    elif allowMode:
+        port = porthi = 48099
+    else:
+        port, porthi = 0, 65535
+
+    return (ip, mask, port, porthi)
+
+def _parseAddressSet_deny(s):
+    return _parseAddressSet_allow(s,0)
+
 def _parseCommand(command):
     """Validation function.  Converts a config value to a shell command of
-       the form (fname, optionslist). Raises ConfigError on failure."""  
+       the form (fname, optionslist). Raises ConfigError on failure."""
     c = command.strip().lower().split()
     if not c:
         raise ConfigError("Invalid command %r" %command)
@@ -187,13 +243,13 @@
             c = os.path.join(p, cmd)
             if os.path.exists(c):
                 return c, opts
-            
+
         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."""  
+       its original. Raises ConfigError on failure."""
     s = s.translate(_allChars, " \t\v\n")
     try:
 	if _hexmode:
@@ -205,7 +261,7 @@
 
 def _parseHex(s):
     """Validation function.  Converts a hex-64 encoded config value into
-       its original. Raises ConfigError on failure."""  
+       its original. Raises ConfigError on failure."""
     return _parseBase64(s,1)
 
 def _parsePublicKey(s):
@@ -244,7 +300,7 @@
 	    (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
@@ -272,7 +328,7 @@
          'MORE': The line is a continuation line of an entry. VALUE is the
                  contents of the line.
     """
-    
+
     if line == '':
         return None, None
 
@@ -298,9 +354,8 @@
 
 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.
+       file, returns a list of (SECTION-NAME, SECTION) tuples, where
+       each SECTION is a list of (KEY, VALUE, LINENO) tuples.
 
        Throws ConfigError if the file is malformatted.
     """
@@ -308,7 +363,7 @@
     curSection = None
     lineno = 0
     lastKey = None
-    
+
     fileLines = contents.split("\n")
     if fileLines[-1] == '':
 	del fileLines[-1]
@@ -358,7 +413,7 @@
             lines.append(ind+v)
     lines.append("") # so the last line ends with \n
     return "\n".join(lines)
-    
+
 class _ConfigFile:
     """Base class to parse, validate, and represent configuration files.
     """
@@ -368,14 +423,14 @@
     #  _sectionEntries: A  map from secname->[ (key, value) ] inorder.
     #  _sectionNames: An inorder list of secnames.
     #
-    # Set by a subclass:
+    # Fields to be set by a subclass:
     #     _syntax is map from sec->{key:
     #                               (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.
     # A section without a corresponding entry is ignored.
@@ -388,12 +443,17 @@
     #   the entry's value will be set to default.  Otherwise, the value
     #   will be set to None.
 
-    def __init__(self, fname=None, string=None):
-        """Create a new _ConfigFile.  If fname is set, read from
-           fname.  If string is set, parse string. """
-        assert fname is None or string is None
-        self.fname = fname
-        if fname:
+    def __init__(self, filename=None, string=None, assumeValid=0):
+        """Create a new _ConfigFile.  If <filename> is set, read from
+           a corresponding file.  If <string> is set, parse its contents.
+
+           If <assumeValid> is true, skip all unnecessary validation
+           steps.  (Use this to load a file that's already been checked as
+           valid.)"""
+        assert filename is None or string is None
+        self.assumeValid = assumeValid
+        self.fname = filename
+        if filename:
             self.reload()
         elif string:
             cs = StringIO(string)
@@ -401,7 +461,7 @@
                 self.__reload(cs)
             finally:
                 cs.close()
-        else:    
+        else:
             self.clear()
 
     def clear(self):
@@ -409,7 +469,7 @@
         self._sections = {}
         self._sectionEntries = {}
         self._sectionNames = []
-        
+
     def reload(self):
         """Reload this _ConfigFile object from disk.  If the object is no
            longer present and correctly formatted, raise an error, but leave
@@ -441,10 +501,10 @@
 
             if self_sections.has_key(secName):
                 raise ConfigError("Duplicate section [%s]" %secName)
-            
+
             section = {}
             sectionEntries = []
-            entryLines = [] 
+            entryLines = []
             self_sections[secName] = section
             self_sectionEntries[secName] = sectionEntries
             sectionEntryLines[secName] = entryLines
@@ -508,7 +568,8 @@
                         assert rule == 'ALLOW*'
                         section[k] = map(parseFn,default)
 
-        # Check for missing required sections.
+        # Check for missing required sections, setting any missing
+        # allowed sections to {}.
         for secName, secConfig in self._syntax.items():
             secRule = secConfig.get('__SECTION__', ('ALLOW',None,None))
             if (secRule[0] == 'REQUIRE'
@@ -517,15 +578,18 @@
             elif not self_sections.has_key(secName):
                 self_sections[secName] = {}
                 self_sectionEntries[secName] = {}
-
-        # Make sure that sectionEntries is correct (sanity check)
-        for s in self_sectionNames:
-            for k,v in self_sectionEntries[s]:
-                assert v == self_sections[s][k] or v in self_sections[s][k]
-
-        # Call our validation hook.
-        self.validate(self_sections, self_sectionEntries, sectionEntryLines,
-		      fileContents)
+                
+        if not self.assumeValid:
+            # Make sure that sectionEntries is correct (sanity check)
+            #XXXX remove this
+            for s in self_sectionNames:
+                for k,v in self_sectionEntries[s]:
+                    assert (v==self_sections[s][k] or
+                            v in self_sections[s][k])
+                    
+            # Call our validation hook.
+            self.validate(self_sections, self_sectionEntries, 
+                          sectionEntryLines, fileContents)
 
         self._sections = self_sections
         self._sectionEntries = self_sectionEntries
@@ -562,13 +626,13 @@
             for k,v in self._sectionEntries[s]:
                 lines.append(_formatEntry(k,v))
             lines.append("\n")
-            
+
         return "".join(lines)
 
 class ClientConfig(_ConfigFile):
     _restrictFormat = 0
     _syntax = {
-        'Host' : { '__SECTION__' : ('REQUIRE', None, None),
+        'Host' : { '__SECTION__' : ('ALLOW', None, None),
                    'ShredCommand': ('ALLOW', _parseCommand, None),
                    'EntropySource': ('ALLOW', None, "/dev/urandom"),
                    },
@@ -591,7 +655,7 @@
 class ServerConfig(_ConfigFile):
     _restrictFormat = 0
     _syntax = {
-        'Host' : ClientConfig._syntax['Host'], 
+        'Host' : ClientConfig._syntax['Host'],
         'Server' : { '__SECTION__' : ('REQUIRE', None, None),
                      'Homedir' : ('ALLOW', None, "/var/spool/minion"),
                      'LogFile' : ('ALLOW', None, None),
@@ -600,23 +664,28 @@
                      'EncryptIdentityKey' : ('REQUIRE', _parseBoolean, "yes"),
                      'PublicKeyLifetime' : ('REQUIRE', _parseInterval,
                                             "30 days"),
+                     'PublicKeySloppiness': ('ALLOW', _parseInterval,
+                                             "5 minutes"),
                      'EncryptPublicKey' : ('REQUIRE', _parseBoolean, "no"),
                      'Mode' : ('REQUIRE', _parseServerMode, "local"),
+                     'Nickname': ('ALLOW', None, None),
+                     'Contact-Email': ('ALLOW', None, None),
+                     'Comments': ('ALLOW', None, None),
                      },
         'DirectoryServers' : { 'ServerURL' : ('ALLOW*', None, None),
                                'Publish' : ('ALLOW', _parseBoolean, "no"),
                                'MaxSkew' : ('ALLOW', _parseInterval,
-                                            "10 minutes",) }, 
+                                            "10 minutes",) },
         'Incoming/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
 			    'IP' : ('ALLOW', _parseIP, None),
                             'Port' : ('ALLOW', _parseInt, "48099"),
-                            'Allow' : ('ALLOW*', None, None),
-                            'Deny' : ('ALLOW*', None, None) },
+                            'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
+                            'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },
         'Outgoing/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
-                            'Allow' : ('ALLOW*', None, None),
-                            'Deny' : ('ALLOW', None, None) },
-        'Delivery/MBox' : { 'Enabled' : ('REQUIRE',  _parseBoolean, "no"),
-                            'AddressFile' : ('REQUIRE', None, None),
+                            'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
+                            'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },
+        'Delivery/MBOX' : { 'Enabled' : ('REQUIRE',  _parseBoolean, "no"),
+                            'AddressFile' : ('ALLOW', None, None),
                             'Command' : ('ALLOW', _parseCommand, "sendmail") },
         }
     # XXXX Missing: Queue-Size / Queue config options
@@ -628,3 +697,10 @@
         #XXXX write this.
         pass
     
+##         if sections['Server']['PublicKeyLifeTime'][2] < 24*60*60:
+##             raise ConfigError("PublicKeyLifetime must be at least 1 day.")
+##         elif sections['Server']['PublicKeyLifeTime'][2] % (24*60*60) > 30:
+##             getLog().warn("PublicKeyLifetime rounded to the nearest day")
+##             nDays = sections[60*60*24]
+
+

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- Crypto.py	26 Jul 2002 20:52:17 -0000	1.10
+++ Crypto.py	28 Jul 2002 22:42:33 -0000	1.11
@@ -29,7 +29,6 @@
 
 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
@@ -188,6 +187,27 @@
     """Reads an ASN1 representation of a public key from external storage."""
     return _ml.rsa_decode_key(s,1)
 
+def pk_PEM_save(rsa, filename, password=None):
+    """Save a PEM-encoded private key to a file.  If <password> is provided,
+       encrypt the key using the password."""
+    f = open(filename, 'w')
+    if password:
+        rsa.PEM_write_key(f, 0, password)
+    else:
+        rsa.PEM_write_key(f, 0)
+    f.close()
+
+def pk_PEM_load(filename, password=None):
+    """Load a PEM-encoded private key from a file.  If <password> is provided,
+       decrypt the key using the password."""
+    f = open(filename, 'r')
+    if password:
+        rsa = _ml.rsa_PEM_read_key(f, 0, password)
+    else:
+        rsa = _ml.rsa_PEM_read_key(f, 0)
+    f.close()
+    return rsa
+    
 #----------------------------------------------------------------------
 # OAEP Functionality
 #

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -d -r1.6 -r1.7
--- ServerInfo.py	26 Jul 2002 20:52:17 -0000	1.6
+++ ServerInfo.py	28 Jul 2002 22:42:33 -0000	1.7
@@ -3,14 +3,16 @@
 
 """mixminion.ServerInfo
 
-   Data structures to represent a server's information, and functions to
-   martial and unmarshal it.
-
+   Implementation of server descriptors (as described in the mixminion
+   spec).  Includes logic to parse, validate, and generate server
+   descriptors.
    """
 
 __all__ = [ 'ServerInfo' ]
 
 import time
+import os
+import binascii
 
 from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
 from mixminion.Packet import IPV4Info
@@ -19,20 +21,27 @@
 
 ConfigError = mixminion.Config.ConfigError
 
-# tmp variable to make this easier to spell.
-C = mixminion.Config
-
+# Longest allowed Nickname
 MAX_NICKNAME = 128
+# Longest allowed Contact email
 MAX_CONTACT = 256
-MAX_COMMENT = 1024
+# Longest allowed Comments field
+MAX_COMMENTS = 1024
+# Shortest permissible identity key
 MIN_IDENTITY_BYTES = 2048 >> 3
+# Longest permissible identity key
 MAX_IDENTITY_BYTES = 4096 >> 3
+# Length of packet key
 PACKET_KEY_BYTES = 1024 >> 3
 
+# tmp alias to make this easier to spell.
+C = mixminion.Config
 class ServerInfo(mixminion.Config._ConfigFile):
+    """A ServerInfo object holds a parsed server descriptor."""
     _restrictFormat = 1
     _syntax = {
 	"Server" : { "__SECTION__": ("REQUIRE", None, None),
+                     "Descriptor-Version": ("REQUIRE", None, None),
 		     "IP": ("REQUIRE", C._parseIP, None),
 		     "Nickname": ("REQUIRE", None, None),
 		     "Identity": ("REQUIRE", C._parsePublicKey, None),
@@ -50,10 +59,14 @@
 		     "Port": ("REQUIRE", C._parseInt, None),
 		     "Key-Digest": ("REQUIRE", C._parseBase64, None),
 		     "Protocols": ("REQUIRE", None, None),
+                     "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
+                     "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
 		     },
 	"Modules/MMTP" : {
  	             "Version": ("REQUIRE", None, None),
 		     "Protocols": ("REQUIRE", None, None),
+                     "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
+                     "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
 		     },
 	"Modules/MBOX" : {
    	             "Version": ("REQUIRE", None, None),
@@ -63,27 +76,26 @@
 		     }
 	}
 
-    def __init__(self, fname, string):
-	mixminion.Config._ConfigFile.__init__(self, fname, string)
+    def __init__(self, fname=None, string=None, assumeValid=0):
+	mixminion.Config._ConfigFile.__init__(self, fname, string, assumeValid)
 
     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']
+	identityKey = server['Identity']
 	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:
+	if server['Contact'] and len(server['Contact']) > MAX_CONTACT:
 	    raise ConfigError("Contact too long")
-	if len(sever['Comments']) > MAX_COMMENTS:
+        if server['Comments'] and len(server['Comments']) > MAX_COMMENTS:
 	    raise ConfigError("Comments too long")
 	packetKeyBytes = server['Packet-Key'].get_modulus_bytes()
 	if packetKeyBytes != PACKET_KEY_BYTES:
@@ -95,7 +107,6 @@
 	if digest != server['Digest']:
 	    raise ConfigError("Invalid digest")
 	
-	signature = server['']
 	if digest != mixminion.Crypto.pk_check_signature(server['Signature'],
 							 identityKey):
 	    raise ConfigError("Invalid signature")
@@ -104,10 +115,10 @@
 
     def getAddr(self):
 	return self['Server']['IP']
-    
+
     def getPort(self):
 	return self['Incoming/MMTP']['Port']
-    
+
     def getPacketKey(self):
 	return self['Server']['Packet-Key']
 
@@ -122,86 +133,141 @@
 #----------------------------------------------------------------------
 # This should go in a different file.
 class ServerKeys:
-    "XXXX"
+    """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 certificate.
+
+       Whether we publish or not, we always generate a server descriptor
+       to store the keys' lifetimes.
+
+       When we create a new ServerKeys object, the associated keys are not
+       read from disk unil the object's load method is called."""
     def __init__(self, keyroot, keyname, hashroot):
-	self.keydir = os.path.join(keyroot, "key_"+keyname)
+	keydir = 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")
-
+        self.descFile = os.path.join(keydir, "ServerDesc")
+        if not os.path.exists(keydir):
+            os.mkdir(keydir, 0700)
+        
     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)
-
+        "Read this set of keys from disk."
+        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 sha1(self.mmtpKey.encode_key(1))
+    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))
 
 def _base64(s):
+    "Helper function: returns a one-line base64 encoding of a given string."
     return binascii.b2a_base64(s).replace("\n","")
 
 def _time(t):
+    """Helper function: turns a time (in seconds) into the format used by
+       Server descriptors"""
     gmt = time.gmtime(t)
-    return "%02d/%02s/%04d %02d:%02d:%02d" % (
+    return "%02d/%02d/%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])
+    """Helper function: turns a time (in seconds) into a date in the format
+       used by server descriptors"""
+    gmt = time.gmtime(t+1) # Add 1 to make sure we round down.
+    return "%02d/%02d/%04d" % (gmt[2],gmt[1],gmt[0])
+
+def _rule(allow, (ip, mask, portmin, portmax)):
+    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)
+
+# 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.
+
+          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
+          validAt -- The starting time (in seconds) for this key's lifetime."""
 
-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 = ServerKeys(keydir, keyname, hashdir)
     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
-    
+    allowIncoming = config['Incoming/MMTP'].get('Enabled', 0)
+
+    nickname = config['Server']['Nickname']
+    if not nickname:
+        nickname = config['Incoming/MMTP'].get('IP', "<Unnamed server>")
+    contact = config['Server']['Contact-Email']
+    comments = config['Server']['Comments']
+    if not validAt:
+        validAt = time.time()
+
+    validUntil = validAt + config['Server']['PublicKeyLifetime'][2]
+    certStarts = validAt - CERTIFICATE_EXPIRY_SLOPPINESS
+    certEnds = validUntil + CERTIFICATE_EXPIRY_SLOPPINESS + \
+               config['Server']['PublicKeySloppiness'][2]
+
     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
+				   "MMTP certificate for %s" %nickname,
+                                   certStarts, certEnds)
 
     fields = {
-	"IP": config['Incoming/MMTP']['IP'],
-	"Port": config['Incoming/MMTP']['Port'],
+	"IP": config['Incoming/MMTP'].get('IP', "0.0.0.0"),
+	"Port": config['Incoming/MMTP'].get('Port', 0),
 	"Nickname": nickname,
-	"Identity": 
+	"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)),
+  	   _base64(mixminion.Crypto.pk_encode_public_key(packetKey)),
 	"KeyID":
 	   _base64(serverKeys.getMMTPKeyID()),
 	}
@@ -210,7 +276,7 @@
         [Server]
 	Descriptor-Version: 1.0
         IP: %(IP)s
-        Port: %(Port)s
+        Nickname: %(Nickname)s
 	Identity: %(Identity)s
 	Digest:
         Signature:
@@ -218,69 +284,93 @@
         Valid-After: %(ValidAfter)s
 	Valid-Until: %(ValidUntil)s
 	Packet-Key: %(PacketKey)s
-        """ %(fields)
+        """ % fields
     if contact:
-	info += "Contact %s\n"%contact
-    if comment:
-	info += "Contact %s\n"%comment
-            
-    if ALLOW_INCOMING_MMTP: #XXXX
+	info += "Contact: %s\n"%contact
+    if comments:
+	info += "Comments: %s\n"%comments
+
+    if config["Incoming/MMTP"].get("Enabled", 0):
 	info += """\
             [Incoming/MMTP]
             Version: 1.0
             Port: %(Port)s
 	    Key-Digest: %(KeyID)s
 	    Protocols: 1.0
-            """
-    if ALLOW_OUTGOING_MMTP: #XXXX
+            """ % fields
+        for k,v in config.getSectionItems("Incoming/MMTP"):
+            if k not in ("Allow", "Deny"):
+                continue
+            info += "%s: %s" % (k, _rule(k=='Allow',v))
+
+    if config["Outgoing/MMTP"].get("Enabled", 0):
 	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
+            if k not in ("Allow", "Deny"):
+                continue
+            info += "%s: %s" % (k, _rule(k=='Allow',v))
+
+    if config["Delivery/MBOX"].get("Enabled", 0):
 	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)
+    # Force a newline at the end of the file, rejoin, and sign.
+    lines.append("")
     info = "\n".join(lines)
     info = signServerInfo(info, identityKey)
-    
-    # debug XXXX
+
+    f = open(serverKeys.getDescriptorFileName(), 'w')
+    try:
+        f.write(info)
+    finally:
+        f.close()
+
+    # for debugging XXXX ### Remove this once we're more confident.
     ServerInfo(string=info)
 
     return info
-    
 
-#----------------------------------------------------------------------
+#-----------------------b-----------------------------------------------
 def getServerInfoDigest(info):
-    return _getServerInfoDiggestImpl(info, None)
+    """Calculate the digest of a server descriptor"""
+    return _getServerInfoDigestImpl(info, None)
 
 def signServerInfo(info, rsa):
-    return _getServerInfoDiggestImpl(info, rsa)
+    """Sign a server descriptor.  <info> should be a well-formed server
+       descriptor, with Digest: and Signature: lines present but with
+       no values."""       
+    return _getServerInfoDigestImpl(info, rsa)
 
 def _getServerInfoDigestImpl(info, rsa=None):
+    """Helper method.  Calculates the correct digest of a server descriptor
+       (as provided in a string).  If rsa is provided, signs the digest and
+       creates a new descriptor.  Otherwise just returns the digest."""
+
+    # The algorithm's pretty easy.  We just find the Digest and Signature
+    # lines, replace each with an 'Empty' version, and calculate the digest.
     infoLines = info.split("\n")
     if not infoLines[0] == "[Server]":
-	raise ConfigError("Must begin with server section")
+        raise ConfigError("Must begin with server section")
     digestLine = None
     signatureLine = None
     infoLines = info.split("\n")
-    for lineno in range(len(infoLines)):
+    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:'
@@ -288,10 +378,11 @@
     info = "\n".join(infoLines)
 
     digest = mixminion.Crypto.sha1(info)
-    if pk is None:
+
+    if rsa is None:
 	return digest
+    # If we got an RSA key, we need to add the digest and signature.
 
-    #### Signature case.
     signature = mixminion.Crypto.pk_sign(digest,rsa)
     digest = _base64(digest)
     signature = binascii.b2a_base64(signature).replace("\n","")

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- test.py	26 Jul 2002 20:52:17 -0000	1.14
+++ test.py	28 Jul 2002 22:42:33 -0000	1.15
@@ -20,6 +20,8 @@
 import atexit
 import tempfile
 import types
+import re
+import binascii
 
 from mixminion.Common import MixError, MixFatalError, MixProtocolError, getLog
 
@@ -45,7 +47,11 @@
         fnames = [fnames]
     for fname in fnames:
         try:
-            os.unlink(fname)
+            if os.path.isdir(fname):
+                try_unlink([os.path.join(fname,f) for f in os.listdir(fname)])
+                os.rmdir(fname)
+            else:
+                os.unlink(fname)
         except OSError:
             pass
 
@@ -1342,6 +1348,41 @@
         queue2.cleanQueue()
 
 #----------------------------------------------------------------------
+# LOGGING
+class LogTests(unittest.TestCase):
+    def testLogging(self):
+        import cStringIO
+        from mixminion.Common import Log, FileLogTarget, ConsoleLogTarget
+        log = Log("INFO")
+        self.assertEquals(log.getMinSeverity(), "INFO")
+        log.log("WARN", "This message should not appear")
+        buf = cStringIO.StringIO()
+        log.addHandler(ConsoleLogTarget(buf))
+        log.trace("Foo")
+        self.assertEquals(buf.getvalue(), "")
+        log.log("WARN", "Hello%sworld", ", ")
+        self.failUnless(buf.getvalue().endswith(
+            "[WARN] Hello, world\n"))
+        self.failUnless(buf.getvalue().index('\n') == len(buf.getvalue())-1)
+        log.error("All your anonymity are belong to us")
+        self.failUnless(buf.getvalue().endswith(
+            "[ERROR] All your anonymity are belong to us\n"))
+        
+        t = tempfile.mktemp("log")
+        t1 = t+"1"
+        unlink_on_exit(t, t1)
+        log.addHandler(FileLogTarget(t))
+        log.info("Abc")
+        log.info("Def")
+        os.rename(t,t1)
+        log.info("Ghi")
+        log.reset()
+        log.info("Klm")
+        log.close()
+        self.assertEquals(open(t).read().count("\n") , 1)
+        self.assertEquals(open(t1).read().count("\n"), 3)
+        
+#----------------------------------------------------------------------
 # SIGHANDLERS
 # XXXX
 
@@ -1382,7 +1423,8 @@
 		print "done.]"
             pk = _ml.rsa_generate(1024, 65535)
             pk.PEM_write_key(open(pkfile, 'w'), 0)
-            _ml.generate_cert(certfile, pk, 365, "Testing certificate")
+            _ml.generate_cert(certfile, pk, "Testing certificate",
+                              time.time(), time.time()+365*24*60*60)
             unlink_on_exit(certfile, pkfile)
             
 	pk = _ml.rsa_PEM_read_key(open(pkfile, 'r'), 0)
@@ -1521,7 +1563,6 @@
 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"),
@@ -1538,7 +1579,8 @@
                           'IntAMD2': ('ALLOW*', _parseInt, ["5", "2"]),
                           'IntRS': ('REQUIRE', _parseInt, None) }
                 }
-    def __init__(self, fname=None, string=None):
+    def __init__(self, fname=None, string=None, restrict=0):
+        self._restrictFormat = restrict
         _ConfigFile.__init__(self,fname,string)
 
 class ConfigFileTests(unittest.TestCase):
@@ -1627,7 +1669,6 @@
         self.assertEquals(f['Sec1']['Bar'], 'bar')
         self.assertEquals(f['Sec2']['Quz'], ['99 99', '88 88'])
         
-
         # Test 'reload' operation
         file = open(fn, 'w')
         file.write(shorterString)
@@ -1637,6 +1678,11 @@
         self.assertEquals(f['Sec1']['Bar'], "default")
         self.assertEquals(f['Sec2'], {})
 
+        # Test restricted mode
+        s = "[Sec1]\nFoo: Bar\nBaz: Quux\n[Sec3]\nIntRS: 9\n"
+        f = TCF(string=s, restrict=1)
+        self.assertEquals(f['Sec1']['Foo'], "Bar")
+        self.assertEquals(f['Sec3']['IntRS'], 9)
 
     def testBadFiles(self):
         TCF = TestConfigFile
@@ -1654,6 +1700,15 @@
         fails("[Sec1]\nFoo 1\n[Sec2]\nBap = 9\n") # Missing require*
         fails("[Sec1]\nFoo: Bar\n[Sec3]\nIntRS=Z\n") # Failed validation
 
+        # now test the restricted format
+        def fails(string, self=self):
+            self.failUnlessRaises(ConfigError, TestConfigFile, None, string, 1)
+        fails("[Sec1]\nFoo=Bar\n")
+        fails("[Sec1]\nFoo Bar\n")
+        fails("[Sec1]\n\nFoo: Bar\n")
+        fails("\n[Sec1]\nFoo: Bar\n")
+        fails("\n[Sec1]\nFoo: Bar\n\n")
+
     def testValidationFns(self):
         import mixminion.Config as C
 
@@ -1669,6 +1724,19 @@
         self.assertEquals(C._parseInterval("2 houRS"), (2,"hour",7200))
         self.assertEquals(C._parseInt("99"), 99)
         self.assertEquals(C._parseIP("192.168.0.1"), "192.168.0.1")
+        pa = C._parseAddressSet_allow
+        self.assertEquals(pa("*"), ("0.0.0.0", "0.0.0.0", 48099, 48099))
+        self.assertEquals(pa("192.168.0.1/255.255.0.0"),
+                          ("192.168.0.1", "255.255.0.0", 48099, 48099))
+        self.assertEquals(pa("192.168.0.1 /  255.255.0.0  23-99"),
+                          ("192.168.0.1", "255.255.0.0", 23, 99))
+        self.assertEquals(pa("192.168.0.1 /  255.255.0.0  23"),
+                          ("192.168.0.1", "255.255.0.0", 23, 23))
+        self.assertEquals(pa("192.168.0.1"),
+                          ("192.168.0.1", "255.255.255.255", 48099, 48099))
+        self.assertEquals(pa("192.168.0.1",0),
+                          ("192.168.0.1", "255.255.255.255", 0, 65535))
+
         # XXXX Won't work on Windows.
         self.assertEquals(C._parseCommand("ls -l"), ("/bin/ls", ['-l']))
         self.assertEquals(C._parseCommand("rm"), ("/bin/rm", []))
@@ -1700,6 +1768,9 @@
         fails(C._parseIP, "192.0.0")
         fails(C._parseIP, "192.0.0.0.0")
         fails(C._parseIP, "A.0.0.0")
+        fails(pa, "1/1")
+        fails(pa, "192.168.0.1 50-40")
+        fails(pa, "192.168.0.1 50-9999999")
 	fails(C._parseBase64, "Y")
 	fails(C._parseHex, "Z")
 	fails(C._parseHex, "A")
@@ -1728,7 +1799,138 @@
         except ConfigError, e:
             # This is what we expect
             pass
-  
+
+
+#----------------------------------------------------------------------
+# Server descriptors
+SERVER_CONFIG = """
+[Server]
+EncryptIdentityKey: no
+PublicKeyLifetime: 10 days
+EncryptPublicKey: no
+Mode: relay
+Nickname: The Server
+Contact-Email: a@b.c
+Comments: This is a test of the emergency
+   broadcast system
+
+[Incoming/MMTP]
+Enabled = yes
+IP: 192.168.0.1
+Allow: 192.168.0.16 1-1024
+Deny: 192.168.0.16
+Allow: *
+
+[Outgoing/MMTP]
+Enabled = yes
+Allow: *
+
+[Delivery/MBOX]
+Enabled: yes
+"""
+
+SERVER_CONFIG_SHORT = """
+[Server]
+EncryptIdentityKey: no
+PublicKeyLifetime: 10 days
+EncryptPublicKey: no
+Mode: relay
+"""
+
+
+import mixminion.Config
+import mixminion.ServerInfo
+class ServerInfoTests(unittest.TestCase):
+    def testServerInfoGen(self):
+        d = tempfile.mktemp()
+        conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG)
+        identity = mixminion.Crypto.pk_generate(2048)
+        if not os.path.exists(d):
+            os.mkdir(d, 0700)
+        unlink_on_exit(d)
+        inf = mixminion.ServerInfo.generateServerDescriptorAndKeys(conf,
+                                                                   identity,
+                                                                   d,
+                                                                   "key1",
+                                                                   d)
+        info = mixminion.ServerInfo.ServerInfo(string=inf)
+        eq = self.assertEquals
+        eq(info['Server']['Descriptor-Version'], "1.0")
+        eq(info['Server']['IP'], "192.168.0.1")
+        eq(info['Server']['Nickname'], "The Server")
+        self.failUnless(0 <= time.time()-info['Server']['Published'] <= 120)
+        self.failUnless(0 <= time.time()-info['Server']['Valid-After']
+                          <= 24*60*60)
+        eq(info['Server']['Valid-Until']-info['Server']['Valid-After'],
+           10*24*60*60)
+        eq(info['Server']['Contact'], "a@b.c")
+        eq(info['Server']['Comments'],
+           "This is a test of the emergency broadcast system")
+        
+        eq(info['Incoming/MMTP']['Version'], "1.0")
+        eq(info['Incoming/MMTP']['Port'], 48099)
+        eq(info['Incoming/MMTP']['Protocols'], "1.0")
+        eq(info['Modules/MMTP']['Version'], "1.0")
+        eq(info['Modules/MMTP']['Protocols'], "1.0")
+        eq(info['Incoming/MMTP']['Allow'], [("192.168.0.16", "255.255.255.255",
+                                            1,1024),
+                                           ("0.0.0.0", "0.0.0.0",
+                                            48099, 48099)] )
+        eq(info['Incoming/MMTP']['Deny'], [("192.168.0.16", "255.255.255.255",
+                                            0,65535),
+                                           ])
+        eq(info['Modules/MBOX']['Version'], "1.0")
+
+        # Now make sure everything was saved properly
+        keydir = os.path.join(d, "key_key1")
+        eq(inf, open(os.path.join(keydir, "ServerDesc")).read())
+        keys = mixminion.ServerInfo.ServerKeys(d, "key1", d)
+        packetKey = mixminion.Crypto.pk_PEM_load(
+            os.path.join(keydir, "mix.key"))
+        eq(packetKey.get_public_key(),
+           info['Server']['Packet-Key'].get_public_key())
+        mmtpKey = mixminion.Crypto.pk_PEM_load(
+            os.path.join(keydir, "mmtp.key"))
+        eq(mixminion.Crypto.sha1(mmtpKey.encode_key(1)),
+           info['Incoming/MMTP']['Key-Digest'])
+
+        # Now check the digest and signature
+        identityPK = info['Server']['Identity']
+        pat = re.compile(r'^(Digest:|Signature:).*$', re.M)
+        x = sha1(pat.sub(r'\1', inf))
+
+        eq(info['Server']['Digest'], x)
+        eq(x, mixminion.Crypto.pk_check_signature(info['Server']['Signature'],
+                                                  identityPK))
+
+        # Now with a shorter configuration
+        conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG_SHORT)
+        inf2 = mixminion.ServerInfo.generateServerDescriptorAndKeys(conf,
+                                                                    identity,
+                                                                    d,
+                                                                    "key2",
+                                                                    d)
+        
+        # Now with a bad signature
+        sig2 = mixminion.Crypto.pk_sign(sha1("Hello"), identity)
+        sig2 = binascii.b2a_base64(sig2).replace("\n", "")
+        sigpat = re.compile('^Signature:.*$', re.M)
+        badSig = sigpat.sub("Signature: %s" % sig2, inf)
+        self.failUnlessRaises(ConfigError,
+                              mixminion.ServerInfo.ServerInfo,
+                              None, badSig)
+
+        # But make sure we don't check the sig on assumeValid
+        mixminion.ServerInfo.ServerInfo(None, badSig, assumeValid=1)
+
+        # Now with a bad digest
+        badDig = inf.replace("a@b.c", "---")
+        self.failUnlessRaises(ConfigError,
+                              mixminion.ServerInfo.ServerInfo,
+                              None, badSig)
+        
+        
+        
 def testSuite():
     suite = unittest.TestSuite()
     loader = unittest.TestLoader()
@@ -1736,7 +1938,9 @@
     suite.addTest(tc(MinionlibCryptoTests))
     suite.addTest(tc(CryptoTests))
     suite.addTest(tc(FormatTests))
+    suite.addTest(tc(LogTests))
     suite.addTest(tc(ConfigFileTests))
+    suite.addTest(tc(ServerInfoTests))
     suite.addTest(tc(HashLogTests))
     suite.addTest(tc(BuildMessageTests))
     suite.addTest(tc(PacketHandlerTests))