[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Directory work: towards automated keygen and automated ...



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

Modified Files:
	ServerKeys.py ServerMain.py 
Log Message:
Directory work: towards automated keygen and automated directory generation.

ServerList:
- Remember server identities longterm
- New code to receive and process an incoming server.
- Add an 'incoming' directory and functions to manipulate/check/add incoming 
  servers.
- Include a list of recommended servers with each directory.

ServerKeys, ServerMain:
- Generate keys automatically, in advance.
- Refactor
- Remember which keysets are published (currently, none).

DirMain:
- Always clean directory before generating a new one.

ServerInfo:
- Add 'Recommended-Servers' to directory
- Bump directory version to 0.2.

test.py:
- Make pychecker happy
- Adapt to new behaviors


Index: ServerKeys.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerKeys.py,v
retrieving revision 1.21
retrieving revision 1.22
diff -u -d -r1.21 -r1.22
--- ServerKeys.py	17 May 2003 00:08:45 -0000	1.21
+++ ServerKeys.py	23 May 2003 07:54:12 -0000	1.22
@@ -10,11 +10,11 @@
 __all__ = [ "ServerKeyring", "generateServerDescriptorAndKeys",
             "generateCertChain" ]
 
-import bisect
 import os
 import socket
 import sys
 import time
+import threading
 
 import mixminion._minionlib
 import mixminion.Crypto
@@ -32,6 +32,19 @@
      secureDelete
 
 #----------------------------------------------------------------------
+
+# Seconds before a key becomes live that we want to generate
+# and publish it.
+#
+#FFFF Make this configurable?  (Set to 3 days.)
+PUBLICATION_LATENCY = 3*24*60*60
+
+# Number of seconds worth of keys we want to generate in advance.
+#
+#FFFF Make this configurable?  (Set to 2 weeks).
+PREPUBLICATION_INTERVAL = 14*24*60*60
+
+#----------------------------------------------------------------------
 class ServerKeyring:
     """A ServerKeyring remembers current and future keys, descriptors, and
        hash logs for a mixminion server.
@@ -45,10 +58,12 @@
     # homeDir: server home directory
     # keyDir: server key directory
     # keyOverlap: How long after a new key begins do we accept the old one?
-    # keyIntervals: list of (start, end, keyset Name)
+    # keySets: sorted list of (start, end, keyset)
     # nextRotation: time_t when this key expires, DOCDOCDOC not so.
     # keyRange: tuple of (firstKey, lastKey) to represent which key names
     #      have keys on disk.
+    #
+    #DOCDOC currentKeys
 
     ## Directory layout:
     #    MINION_HOME/work/queues/incoming/ [Queue of received,unprocessed pkts]
@@ -73,10 +88,11 @@
 
     def __init__(self, config):
         "Create a ServerKeyring from a config object"
+        self._lock = threading.RLock()
         self.configure(config)
 
     def configure(self, config):
-        "Set up a SeverKeyring from a config object"
+        "Set up a ServerKeyring from a config object"
         self.config = config
         self.homeDir = config['Server']['Homedir']
         self.keyDir = os.path.join(self.homeDir, 'keys')
@@ -88,7 +104,7 @@
     def checkKeys(self):
         """Internal method: read information about all this server's
            currently-prepared keys from disk."""
-        self.keyIntervals = []
+        self.keySets = []
         firstKey = sys.maxint
         lastKey = 0
 
@@ -98,6 +114,8 @@
             LOG.info("Creating server keystore at %s", self.keyDir)
             createPrivateDir(self.keyDir)
 
+        unpublished = []
+        
         # Iterate over the entires in HOME/keys
         for dirname in os.listdir(self.keyDir):
             # Skip any that aren't directories named "key_INT"
@@ -119,28 +137,28 @@
                 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)
+            keyset = ServerKeyset(self.keyDir, keysetname, self.hashDir)
+            # XXXX004 catch bad/missing serverdescriptor!
+            t1, t2 = keyset.getLiveness()
+            self.keySets.append( (t1, t2, keyset) )
+                
+            LOG.debug("Found key %s (valid from %s to %s)",
+                      dirname, formatDate(t1), formatDate(t2))
+
+            if not keyset.isPublished():
+                unpublished.append(keysetname)
+
+        self.unpublished = unpublished
 
         # Now, sort the key intervals by starting time.
-        self.keyIntervals.sort()
+        self.keySets.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]
+        for idx in xrange(len(self.keySets)-1):
+            end = self.keySets[idx][1]
+            start = self.keySets[idx+1][0]
             if start < end:
                 LOG.warn("Multiple keys for %s.  That's unsupported.",
                               formatDate(end))
@@ -186,6 +204,20 @@
             LOG.info("Removing diffie-helman parameters file")
             secureDelete([dhfile], blocking=1)
 
+    def createKeysAsNeeded(self,now=None):
+        """DOCDOC"""
+        if now is None:
+            now = time.time()
+
+        if self.getNextKeygen() > now-10: # 10 seconds of leeway
+            return
+
+        lastExpiry = self.keySets[-1][1]
+        lifetime = self.config['Server']['PublicKeyLifetime'].getSeconds()
+        nKeys = ceilDiv(PREPUBLICATION_INTERVAL, lifetime)
+
+        self.createKeys(num=nKeys)
+
     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
@@ -195,8 +227,8 @@
         #password = None
 
         if startAt is None:
-            if self.keyIntervals:
-                startAt = self.keyIntervals[-1][1]+60
+            if self.keySets:
+                startAt = self.keySets[-1][1]+60
             else:
                 startAt = time.time()+60
 
@@ -231,6 +263,24 @@
 
         self.checkKeys()
 
+    def getNextKeygen(self):
+        """DOCDOC
+
+           -1 => Right now!
+        """
+        if not self.keySets:
+            return -1
+
+        # Our last current key expires at 'lastExpiry'.
+        lastExpiry = self.keySets[-1][1]
+        # We want to have keys in the directory valid for
+        # PREPUBLICATION_INTERVAL seconds after that, and we assume that
+        # a key takes up to PUBLICATION_LATENCY seconds to make it into the
+        # directory.
+        nextKeygen = lastExpiry - PUBLICATION_LATENCY - PREPUBLICATION_INTERVAL
+
+        return nextKeygen
+
     def removeDeadKeys(self, now=None):
         """Remove all keys that have expired"""
         self.checkKeys()
@@ -243,9 +293,10 @@
 
         cutoff = now - self.keyOverlap
 
-        for va, vu, name in self.keyIntervals:
+        for va, vu, keyset in self.keySets:
             if vu >= cutoff:
                 continue
+            name = keyset.keyname
             LOG.info("Removing%s key %s (valid from %s through %s)",
                      expiryStr, name, formatDate(va), formatDate(vu-3600))
             dirname = os.path.join(self.keyDir, "key_"+name)
@@ -261,21 +312,19 @@
 
     def _getLiveKeys(self, now=None):
         """Find all keys that are now valid.  Return list of (Valid-after,
-           valid-util, name)."""
-        if not self.keyIntervals:
+           valid-util, keyset)."""
+        if not self.keySets:
             return []
         if now is None:
             now = time.time()
-        if self.nextUpdate and now > self.nextUpdate:
-            self.nextUpdate = None
 
         cutoff = now-self.keyOverlap
         # A key is live if
         #     * it became valid before now, and
         #     * it did not become invalid until keyOverlap seconds ago
 
-        return [ k for k in self.keyIntervals
-                 if k[0] < now and k[1] > cutoff ]
+        return [ (va,vu,k) for (va,vu,k) in self.keySets
+                 if va < now and vu > cutoff ]
 
     def getServerKeysets(self):
         """Return a ServerKeyset object for the currently live key.
@@ -283,10 +332,7 @@
            DOCDOC"""
         # FFFF Support passwords on keys
         keysets = [ ]
-        for va, vu, name in self._getLiveKeys():
-            ks = ServerKeyset(self.keyDir, name, self.hashDir)
-            ks.validAfter = va
-            ks.validUntil = vu
+        for va, vu, ks in self._getLiveKeys():
             ks.load()
             keysets.append(ks)
 
@@ -323,7 +369,7 @@
     def updateKeys(self, packetHandler, mmtpServer, when=None):
         """DOCDOC: Return next rotation."""
         self.removeDeadKeys()
-        keys = self.getServerKeysets(when)
+        self.currentKeys = keys = self.getServerKeysets(when)
         LOG.info("Updating keys: %s currently valid", len(keys))
         if mmtpServer is not None:
             context = self._getTLSContext(keys[-1])
@@ -338,12 +384,17 @@
                     k.getHashLogFileName(), k.getPacketKeyID()))
             packetHandler.setKeys(packetkeys, hashLogs)
 
+        self.nextUpdate = None
         self.getNextKeyRotation(keys)
 
     def getNextKeyRotation(self, keys=None):
+        """DOCDOC"""
         if self.nextUpdate is None:
             if keys is None:
-                keys = self.getServerKeysets()
+                if self.currentKeys is None:
+                    keys = self.getServerKeysets()
+                else:
+                    keys = self.currentKeys
             addKeyEvents = []
             rmKeyEvents = []
             for k in keys:
@@ -361,7 +412,6 @@
                          formatTime(rm,1))
                 self.nextUpdate = rm
 
-
         return self.nextUpdate
 
     def getAddress(self):
@@ -372,6 +422,12 @@
                 desc['Incoming/MMTP']['Port'],
                 desc['Incoming/MMTP']['Key-Digest'])
 
+    def lock(self, blocking=1):
+        return self._lock.acquire(blocking)
+
+    def unlock(self):
+        self._lock.release()
+
 #----------------------------------------------------------------------
 class ServerKeyset:
     """A set of expirable keys for use by a server.
@@ -393,7 +449,7 @@
     # descFile: filename of this keyset's server descriptor.
     #
     # packetKey, mmtpKey: This server's actual short-term keys.
-    # DOCDOC serverinfo, validAfter, validUntil
+    # DOCDOC serverinfo, validAfter, validUntil,published(File)?
     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
@@ -408,9 +464,11 @@
         self.mmtpKeyFile = os.path.join(keydir, "mmtp.key")
         self.certFile = os.path.join(keydir, "mmtp.cert")
         self.descFile = os.path.join(keydir, "ServerDesc")
+        self.publishedFile = os.path.join(keydir, "published")
         self.serverinfo = None
         self.validAfter = None
         self.validUntil = None
+        self.published = os.path.exists(self.publishedFile)
         if not os.path.exists(keydir):
             createPrivateDir(keydir)
 
@@ -438,20 +496,38 @@
         "Return the sha1 hash of the asn1 encoding of the packet public key"
         return mixminion.Crypto.sha1(self.packetKey.encode_key(1))
     def getServerDescriptor(self):
+        """DOCDOC"""
         if self.serverinfo is None:
             self.serverinfo = ServerInfo(fname=self.descFile)
         return self.serverinfo
     def getLiveness(self):
+        """DOCDOC"""
         if self.validAfter is None or self.validUntil is None:
             info = self.getServerDescriptor()
             self.validAfter = info['Server']['Valid-After']
             self.validUntil = info['Server']['Valid-Until']
         return self.validAfter, self.validUntil
+    def isPublished(self):
+        """DOCDOC"""
+        return self.published
+    def markAsPublished(self):
+        """DOCDOC"""
+        f = open(self.published, 'w')
+        try:
+            f.write(formatTime(time.time(), 1))
+            f.write("\n")
+        finally:
+            f.close()
+        self.published = 1
     def regenerateServerDescriptor(self, config, identityKey, validAt=None):
         """DOCDOC"""
         self.load()
         if validAt is None:
             validAt = self.getLiveness()[0]
+        try:
+            os.unlink(self.publishedFile)
+        except OSError:
+            pass
         generateServerDescriptorAndKeys(config, identityKey,
                          self.keyroot, self.keyname, self.hashroot,
                          validAt=validAt, useServerKeys=1)

Index: ServerMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerMain.py,v
retrieving revision 1.52
retrieving revision 1.53
diff -u -d -r1.52 -r1.53
--- ServerMain.py	17 May 2003 00:08:45 -0000	1.52
+++ ServerMain.py	23 May 2003 07:54:12 -0000	1.53
@@ -538,13 +538,7 @@
         #XXXX004 Catch ConfigError for bad serverinfo.
         #XXXX004 Check whether config matches serverinfo
         self.keyring = mixminion.server.ServerKeys.ServerKeyring(config)
-        if not self.keyring.getLiveKeys():
-            LOG.info("Generating a month's worth of keys.")
-            LOG.info("(Don't count on this feature in future versions.)")
-            # We might not be able to do this, if we password-encrypt keys
-            keylife = config['Server']['PublicKeyLifetime'].getSeconds()
-            nKeys = ceilDiv(30*24*60*60, keylife)
-            self.keyring.createKeys(nKeys)
+        self.keyring.createKeysAsNeeded()
 
         LOG.debug("Initializing packet handler")
         self.packetHandler = mixminion.server.PacketHandler.PacketHandler()
@@ -603,7 +597,32 @@
 
     def updateKeys(self):
         """DOCDOC"""
-        self.keyring.updateKeys(self.packetHandler, self.mmtpServer)
+        # We don't dare to block here -- we could block the main thread for 
+        # as long as it takes to generate several new RSA keys, which would
+        # stomp responsiveness on slow computers.
+        # ???? Could there be a more elegant approach to this?
+        if not self.keyring.lock(1):
+            LOG.warn("generateKeys in progress:"
+                     " updateKeys delaying for 2 minutes")
+            # This will cause getNextKeyRotation to return 2 minutes later
+            # than now.
+            self.keyring.nextUpdate = time.time() += 120
+            return
+
+        try:
+            self.keyring.updateKeys(self.packetHandler, self.mmtpServer)
+        finally:
+            self.keyring.unlock()
+
+    def generateKeys(self):
+        """DOCDOC"""
+        def c(self=self):
+            self.keyring.lock()
+            try:
+                self.keyring.createKeysAsNeeded
+            finally:
+                self.keyring.unlock()
+        self.processingThread.addJob(c)
 
     def run(self):
         """Run the server; don't return unless we hit an exception."""
@@ -634,7 +653,12 @@
         self.scheduleRecurringComplex(self.keyring.getNextKeyRotation(),
                                       self.updateKeys,
                                       "KEY_ROTATE",
-                                      self.keyring.getKeyRotation)
+                                      self.keyring.getNextKeyRotation)
+
+        self.scheduleRecurringComplex(self.keyring.getNextKeygen(),
+                                      self.generateKeys,
+                                      "KEY_GEN",
+                                      self.keyring.getNextKeygen)
 
         nextMix = self.mixPool.getNextMixTime(now)
         LOG.debug("First mix at %s", formatTime(nextMix,1))