[Zope-Checkins] CVS: ZODB3/ZEO/zrpc - _hmac.py:1.1 smac.py:1.38 connection.py:1.48

Jeremy Hylton jeremy@zope.com
Fri, 30 May 2003 15:21:27 -0400


Update of /cvs-repository/ZODB3/ZEO/zrpc
In directory cvs.zope.org:/tmp/cvs-serv25334/ZEO/zrpc

Modified Files:
	smac.py connection.py 
Added Files:
	_hmac.py 
Log Message:
Merge ZODB3-auth-branch and bump a few version numbers.

After the merge, I made several Python 2.1 compatibility changes for
the auth code.


=== Added File ZODB3/ZEO/zrpc/_hmac.py ===
# This file is a slightly modified copy of Python 2.3's Lib/hmac.py.
# This file is under the Python Software Foundation (PSF) license.

"""HMAC (Keyed-Hashing for Message Authentication) Python module.

Implements the HMAC algorithm as described by RFC 2104.
"""

def _strxor(s1, s2):
    """Utility method. XOR the two strings s1 and s2 (must have same length).
    """
    return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))

# The size of the digests returned by HMAC depends on the underlying
# hashing module used.
digest_size = None

class HMAC:
    """RFC2104 HMAC class.

    This supports the API for Cryptographic Hash Functions (PEP 247).
    """

    def __init__(self, key, msg = None, digestmod = None):
        """Create a new HMAC object.

        key:       key for the keyed hash object.
        msg:       Initial input for the hash, if provided.
        digestmod: A module supporting PEP 247. Defaults to the md5 module.
        """
        if digestmod is None:
            import md5
            digestmod = md5

        self.digestmod = digestmod
        self.outer = digestmod.new()
        self.inner = digestmod.new()
        # Python 2.1 and 2.2 differ about the correct spelling
        try:
            self.digest_size = digestmod.digestsize
        except AttributeError:
            self.digest_size = digestmod.digest_size

        blocksize = 64
        ipad = "\x36" * blocksize
        opad = "\x5C" * blocksize

        if len(key) > blocksize:
            key = digestmod.new(key).digest()

        key = key + chr(0) * (blocksize - len(key))
        self.outer.update(_strxor(key, opad))
        self.inner.update(_strxor(key, ipad))
        if msg is not None:
            self.update(msg)

##    def clear(self):
##        raise NotImplementedError, "clear() method not available in HMAC."

    def update(self, msg):
        """Update this hashing object with the string msg.
        """
        self.inner.update(msg)

    def copy(self):
        """Return a separate copy of this hashing object.

        An update to this copy won't affect the original object.
        """
        other = HMAC("")
        other.digestmod = self.digestmod
        other.inner = self.inner.copy()
        other.outer = self.outer.copy()
        return other

    def digest(self):
        """Return the hash value of this hashing object.

        This returns a string containing 8-bit data.  The object is
        not altered in any way by this function; you can continue
        updating the object after calling this function.
        """
        h = self.outer.copy()
        h.update(self.inner.digest())
        return h.digest()

    def hexdigest(self):
        """Like digest(), but returns a string of hexadecimal digits instead.
        """
        return "".join([hex(ord(x))[2:].zfill(2)
                        for x in tuple(self.digest())])

def new(key, msg = None, digestmod = None):
    """Create a new hashing object and return it.

    key: The starting key for the hash.
    msg: if available, will immediately be hashed into the object's starting
    state.

    You can now feed arbitrary strings into the object using its update()
    method, and can ask for the hash value at any time by calling its digest()
    method.
    """
    return HMAC(key, msg, digestmod)


=== ZODB3/ZEO/zrpc/smac.py 1.37 => 1.38 ===
--- ZODB3/ZEO/zrpc/smac.py:1.37	Wed Jan 15 13:19:18 2003
+++ ZODB3/ZEO/zrpc/smac.py	Fri May 30 15:20:56 2003
@@ -11,9 +11,29 @@
 # FOR A PARTICULAR PURPOSE
 #
 ##############################################################################
-"""Sized Message Async Connections."""
+"""Sized Message Async Connections.
 
-import asyncore, struct
+This class extends the basic asyncore layer with a record-marking
+layer.  The message_output() method accepts an arbitrary sized string
+as its argument.  It sends over the wire the length of the string
+encoded using struct.pack('>i') and the string itself.  The receiver
+passes the original string to message_input().
+
+This layer also supports an optional message authentication code
+(MAC).  If a session key is present, it uses HMAC-SHA-1 to generate a
+20-byte MAC.  If a MAC is present, the high-order bit of the length
+is set to 1 and the MAC immediately follows the length.
+"""
+
+import asyncore
+import errno
+try:
+    import hmac
+except ImportError:
+    import _hmac as hmac
+import sha
+import socket
+import struct
 import threading
 from types import StringType
 
@@ -21,7 +41,6 @@
 from ZEO.zrpc.error import DisconnectedError
 import zLOG
 
-import socket, errno
 
 # Use the dictionary to make sure we get the minimum number of errno
 # entries.   We expect that EWOULDBLOCK == EAGAIN on most systems --
@@ -45,6 +64,8 @@
 # that we could pass to send() without blocking.
 SEND_SIZE = 60000
 
+MAC_BIT = 0x80000000
+
 class SizedMessageAsyncConnection(asyncore.dispatcher):
     __super_init = asyncore.dispatcher.__init__
     __super_close = asyncore.dispatcher.close
@@ -75,8 +96,13 @@
         self.__output_lock = threading.Lock() # Protects __output
         self.__output = []
         self.__closed = 0
+        self.__hmac = None
         self.__super_init(sock, map)
 
+    def setSessionKey(self, sesskey):
+        log("set session key %r" % sesskey)
+        self.__hmac = hmac.HMAC(sesskey, digestmod=sha)
+
     def get_addr(self):
         return self.addr
 
@@ -124,12 +150,16 @@
                 inp = "".join(inp)
 
             offset = 0
+            expect_mac = 0
             while (offset + msg_size) <= input_len:
                 msg = inp[offset:offset + msg_size]
                 offset = offset + msg_size
                 if not state:
-                    # waiting for message
                     msg_size = struct.unpack(">i", msg)[0]
+                    expect_mac = msg_size & MAC_BIT
+                    if expect_mac:
+                        msg_size ^= MAC_BIT
+                        msg_size += 20
                     state = 1
                 else:
                     msg_size = 4
@@ -144,6 +174,17 @@
                     # incoming call to be handled.  During all this
                     # time, the __input_lock is held.  That's a good
                     # thing, because it serializes incoming calls.
+                    if expect_mac:
+                        mac = msg[:20]
+                        msg = msg[20:]
+                        if self.__hmac:
+                            self.__hmac.update(msg)
+                            _mac = self.__hmac.digest()
+                            if mac != _mac:
+                                raise ValueError("MAC failed: %r != %r"
+                                                 % (_mac, mac))
+                        else:
+                            log("Received MAC but no session key set")
                     self.message_input(msg)
 
             self.__state = state
@@ -214,7 +255,12 @@
         self.__output_lock.acquire()
         try:
             # do two separate appends to avoid copying the message string
-            self.__output.append(struct.pack(">i", len(message)))
+            if self.__hmac:
+                self.__output.append(struct.pack(">i", len(message) | MAC_BIT))
+                self.__hmac.update(message)
+                self.__output.append(self.__hmac.digest())
+            else:
+                self.__output.append(struct.pack(">i", len(message)))
             if len(message) <= SEND_SIZE:
                 self.__output.append(message)
             else:


=== ZODB3/ZEO/zrpc/connection.py 1.47 => 1.48 ===
--- ZODB3/ZEO/zrpc/connection.py:1.47	Thu Apr 24 18:04:27 2003
+++ ZODB3/ZEO/zrpc/connection.py	Fri May 30 15:20:56 2003
@@ -114,6 +114,7 @@
 
     __super_init = smac.SizedMessageAsyncConnection.__init__
     __super_close = smac.SizedMessageAsyncConnection.close
+    __super_setSessionKey = smac.SizedMessageAsyncConnection.setSessionKey
 
     # Protocol variables:
     #
@@ -152,12 +153,17 @@
         self.trigger = None
         self._prepare_async()
         self._map = {self._fileno: self}
-        # __msgid_lock guards access to msgid
+        # msgid_lock guards access to msgid
         self.msgid_lock = threading.Lock()
-        # __replies_cond is used to block when a synchronous call is
+        # replies_cond is used to block when a synchronous call is
         # waiting for a response
         self.replies_cond = threading.Condition()
         self.replies = {}
+        # waiting_for_reply is used internally to indicate whether
+        # a call is in progress.  setting a session key is deferred
+        # until after the call returns.
+        self.waiting_for_reply = 0
+        self.delay_sesskey = None
         self.register_object(obj)
         self.handshake()
 
@@ -249,7 +255,11 @@
 
         meth = getattr(self.obj, name)
         try:
-            ret = meth(*args)
+            self.waiting_for_reply = 1
+            try:
+                ret = meth(*args)
+            finally:
+                self.waiting_for_reply = 0
         except (SystemExit, KeyboardInterrupt):
             raise
         except Exception, msg:
@@ -271,6 +281,10 @@
             else:
                 self.send_reply(msgid, ret)
 
+        if self.delay_sesskey:
+            self.__super_setSessionKey(self.delay_sesskey)
+            self.delay_sesskey = None
+
     def handle_error(self):
         if sys.exc_info()[0] == SystemExit:
             raise sys.exc_info()
@@ -317,6 +331,12 @@
             msg = self.marshal.encode(msgid, 0, REPLY, (ZRPCError, err))
         self.message_output(msg)
         self.poll()
+
+    def setSessionKey(self, key):
+        if self.waiting_for_reply:
+            self.delay_sesskey = key
+        else:
+            self.__super_setSessionKey(key)
 
     # The next two public methods (call and callAsync) are used by
     # clients to invoke methods on remote objects