Merge pull request #84 from exarkun/finished_messages

Introduce Connection.get_finished and Connection.get_peer_finished.
diff --git a/ChangeLog b/ChangeLog
index b0fd98a..e36f2d2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,13 @@
-2014-03-29 fedor-brunner
+2014-03-30  Fedor Brunner  <fedor.brunner@azet.sk>
+
+	* OpenSSL/SSL.py: Add ``get_finished``, ``get_peer_finished``
+	  methods to ``Connection``. If you use these methods to
+	  implement TLS channel binding (RFC 5929) disable session
+	  resumption because triple handshake attacks against TLS.
+	  <https://www.ietf.org/mail-archive/web/tls/current/msg11337.html>
+	  <https://secure-resumption.com/tlsauth.pdf>
+
+2014-03-29  Fedor Brunner  <fedor.brunner@azet.sk>
 
 	* OpenSSL/SSL.py: Add ``get_cipher_name``, ``get_cipher_bits``,
 	  and ``get_cipher_version`` to ``Connection``.
diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py
index 4a497f1..fbb18f0 100644
--- a/OpenSSL/SSL.py
+++ b/OpenSSL/SSL.py
@@ -1421,6 +1421,63 @@
             _raise_current_error()
 
 
+    def _get_finished_message(self, function):
+        """
+        Helper to implement :py:meth:`get_finished` and
+        :py:meth:`get_peer_finished`.
+
+        :param function: Either :py:data:`SSL_get_finished`: or
+            :py:data:`SSL_get_peer_finished`.
+
+        :return: :py:data:`None` if the desired message has not yet been
+            received, otherwise the contents of the message.
+        :rtype: :py:class:`bytes` or :py:class:`NoneType`
+        """
+        # The OpenSSL documentation says nothing about what might happen if the
+        # count argument given is zero.  Specifically, it doesn't say whether
+        # the output buffer may be NULL in that case or not.  Inspection of the
+        # implementation reveals that it calls memcpy() unconditionally.
+        # Section 7.1.4, paragraph 1 of the C standard suggests that
+        # memcpy(NULL, source, 0) is not guaranteed to produce defined (let
+        # alone desirable) behavior (though it probably does on just about
+        # every implementation...)
+        #
+        # Allocate a tiny buffer to pass in (instead of just passing NULL as
+        # one might expect) for the initial call so as to be safe against this
+        # potentially undefined behavior.
+        empty = _ffi.new("char[]", 0)
+        size = function(self._ssl, empty, 0)
+        if size == 0:
+            # No Finished message so far.
+            return None
+
+        buf = _ffi.new("char[]", size)
+        function(self._ssl, buf, size)
+        return _ffi.buffer(buf, size)[:]
+
+
+    def get_finished(self):
+        """
+        Obtain the latest `handshake finished` message sent to the peer.
+
+        :return: The contents of the message or :py:obj:`None` if the TLS
+            handshake has not yet completed.
+        :rtype: :py:class:`bytes` or :py:class:`NoneType`
+        """
+        return self._get_finished_message(_lib.SSL_get_finished)
+
+
+    def get_peer_finished(self):
+        """
+        Obtain the latest `handshake finished` message received from the peer.
+
+        :return: The contents of the message or :py:obj:`None` if the TLS
+            handshake has not yet completed.
+        :rtype: :py:class:`bytes` or :py:class:`NoneType`
+        """
+        return self._get_finished_message(_lib.SSL_get_peer_finished)
+
+
     def get_cipher_name(self):
         """
         Obtain the name of the currently used cipher.
diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py
index 406bc04..bfe3114 100644
--- a/OpenSSL/test/test_ssl.py
+++ b/OpenSSL/test/test_ssl.py
@@ -1932,10 +1932,69 @@
 
     # XXX want_read
 
+    def test_get_finished_before_connect(self):
+        """
+        :py:obj:`Connection.get_finished` returns :py:obj:`None` before TLS
+        handshake is completed.
+        """
+        ctx = Context(TLSv1_METHOD)
+        connection = Connection(ctx, None)
+        self.assertEqual(connection.get_finished(), None)
+
+
+    def test_get_peer_finished_before_connect(self):
+        """
+        :py:obj:`Connection.get_peer_finished` returns :py:obj:`None` before
+        TLS handshake is completed.
+        """
+        ctx = Context(TLSv1_METHOD)
+        connection = Connection(ctx, None)
+        self.assertEqual(connection.get_peer_finished(), None)
+
+
+    def test_get_finished(self):
+        """
+        :py:obj:`Connection.get_finished` method returns the TLS Finished
+        message send from client, or server. Finished messages are send during
+        TLS handshake.
+        """
+
+        server, client = self._loopback()
+
+        self.assertNotEqual(server.get_finished(), None)
+        self.assertTrue(len(server.get_finished()) > 0)
+
+
+    def test_get_peer_finished(self):
+        """
+        :py:obj:`Connection.get_peer_finished` method returns the TLS Finished
+        message received from client, or server. Finished messages are send
+        during TLS handshake.
+        """
+        server, client = self._loopback()
+
+        self.assertNotEqual(server.get_peer_finished(), None)
+        self.assertTrue(len(server.get_peer_finished()) > 0)
+
+
+    def test_tls_finished_message_symmetry(self):
+        """
+        The TLS Finished message send by server must be the TLS Finished message
+        received by client.
+
+        The TLS Finished message send by client must be the TLS Finished message
+        received by server.
+        """
+        server, client = self._loopback()
+
+        self.assertEqual(server.get_finished(), client.get_peer_finished())
+        self.assertEqual(client.get_finished(), server.get_peer_finished())
+
+
     def test_get_cipher_name_before_connect(self):
         """
-        :py:obj:`Connection.get_cipher_name` returns  :py:obj:`None`
-        if no connection has been established.
+        :py:obj:`Connection.get_cipher_name` returns :py:obj:`None` if no
+        connection has been established.
         """
         ctx = Context(TLSv1_METHOD)
         conn = Connection(ctx, None)
@@ -1959,8 +2018,8 @@
 
     def test_get_cipher_version_before_connect(self):
         """
-        :py:obj:`Connection.get_cipher_version` returns :py:obj:`None`
-        if no connection has been established.
+        :py:obj:`Connection.get_cipher_version` returns :py:obj:`None` if no
+        connection has been established.
         """
         ctx = Context(TLSv1_METHOD)
         conn = Connection(ctx, None)
@@ -1984,8 +2043,8 @@
 
     def test_get_cipher_bits_before_connect(self):
         """
-        :py:obj:`Connection.get_cipher_bits` returns :py:obj:`None`
-        if no connection has been established.
+        :py:obj:`Connection.get_cipher_bits` returns :py:obj:`None` if no
+        connection has been established.
         """
         ctx = Context(TLSv1_METHOD)
         conn = Connection(ctx, None)
@@ -1994,8 +2053,8 @@
 
     def test_get_cipher_bits(self):
         """
-        :py:obj:`Connection.get_cipher_bits` returns the number of secret bits of the currently
-        used cipher.
+        :py:obj:`Connection.get_cipher_bits` returns the number of secret bits
+        of the currently used cipher.
         """
         server, client = self._loopback()
         server_cipher_bits, client_cipher_bits = \
diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst
index 7c45bc8..e1c1d8a 100644
--- a/doc/api/ssl.rst
+++ b/doc/api/ssl.rst
@@ -759,24 +759,44 @@
 
     .. versionadded:: 0.14
 
+
+.. py:method:: Connection.get_finished()
+
+    Obtain latest TLS Finished message that we sent, or :py:obj:`None` if
+    handshake is not completed.
+
+    .. versionadded:: 0.15
+
+
+.. py:method:: Connection.get_peer_finished()
+
+    Obtain latest TLS Finished message that we expected from peer, or
+    :py:obj:`None` if handshake is not completed.
+
+    .. versionadded:: 0.15
+
+
 .. py:method:: Connection.get_cipher_name()
 
     Obtain the name of the currently used cipher.
 
     .. versionadded:: 0.15
 
+
 .. py:method:: Connection.get_cipher_bits()
 
     Obtain the number of secret bits of the currently used cipher.
 
     .. versionadded:: 0.15
 
+
 .. py:method:: Connection.get_cipher_version()
 
     Obtain the protocol name of the currently used cipher.
 
     .. versionadded:: 0.15
 
+
 .. Rubric:: Footnotes
 
 .. [#connection-context-socket] Actually, all that is required is an object that
diff --git a/setup.py b/setup.py
index 3e7605d..f12714d 100755
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@
       maintainer_email = 'exarkun@twistedmatrix.com',
       url = 'https://github.com/pyca/pyopenssl',
       license = 'APL2',
-      install_requires=["cryptography>=0.2.2", "six>=1.5.2"],
+      install_requires=["cryptography>=0.3", "six>=1.5.2"],
       long_description = """\
 High-level wrapper around a subset of the OpenSSL library, includes
  * SSL.Connection objects, wrapping the methods of Python's portable