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