summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean-Paul Calderone <exarkun@twistedmatrix.com>2014-04-17 10:41:44 -0400
committerJean-Paul Calderone <exarkun@twistedmatrix.com>2014-04-17 10:41:44 -0400
commit256090bc9ada777a045404c05864eb3fc1fcae96 (patch)
tree02e7889f2283368b07b78a1bdb12f9f769e66505
parent61d7d393fb8ef50deed4424db94e3ea72f95c4c2 (diff)
parent434d03bbb3255cc313680826939801760b351d2a (diff)
downloadpyopenssl-256090bc9ada777a045404c05864eb3fc1fcae96.tar.gz
Merge remote-tracking branch 'pyca/master' into ecdhe
-rw-r--r--.travis.yml57
-rw-r--r--CONTRIBUTING.rst43
-rw-r--r--ChangeLog26
-rw-r--r--OpenSSL/SSL.py106
-rw-r--r--OpenSSL/crypto.py2
-rw-r--r--OpenSSL/test/test_ssl.py135
-rw-r--r--OpenSSL/test/test_tsafe.py24
-rw-r--r--OpenSSL/tsafe.py2
-rw-r--r--README.rst (renamed from README)3
-rw-r--r--doc/api/ssl.rst39
-rw-r--r--leakcheck/crypto.py46
11 files changed, 477 insertions, 6 deletions
diff --git a/.travis.yml b/.travis.yml
index 68149b4..595e37c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,6 @@ language: python
os:
- linux
- - osx
python:
- "pypy"
@@ -11,6 +10,41 @@ python:
- "3.2"
- "3.3"
+matrix:
+ include:
+ # Also run the tests against cryptography master.
+ - python: "2.6"
+ env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+ - python: "2.7"
+ env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+ - python: "3.2"
+ env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+ - python: "3.3"
+ env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+ - python: "pypy"
+ env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+
+ # Also run at least a little bit against an older version of OpenSSL.
+ - python: "2.7"
+ env:
+ OPENSSL=0.9.8
+
+ # Let the cryptography master builds fail because they might be triggered by
+ # cryptography changes beyond our control.
+ allow_failures:
+ - env:
+ CRYPTOGRAPHY_GIT_MASTER=true
+ - env:
+ OPENSSL=0.9.8
+
+before_install:
+ - if [ -n "$CRYPTOGRAPHY_GIT_MASTER" ]; then pip install git+https://github.com/pyca/cryptography.git;fi
+
install:
# Install the wheel library explicitly here. It is not really a setup
# dependency. It is not an install dependency. It is only a dependency for
@@ -18,5 +52,24 @@ install:
# travis.
- pip install wheel
+ # Also install some tools for measuring code coverage and sending the results
+ # to coveralls.
+ - pip install coveralls coverage
+
script:
- - python setup.py bdist_wheel test
+ - |
+ if [[ "${OPENSSL}" == "0.9.8" ]]; then
+ sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ lucid main"
+ sudo apt-get -y update
+ sudo apt-get install -y --force-yes libssl-dev/lucid
+ fi
+ - |
+ coverage run --branch --source=OpenSSL setup.py bdist_wheel test
+ - |
+ coverage report -m
+
+after_success:
+ - coveralls
+
+notifications:
+ email: false
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..a4040e4
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,43 @@
+Contributing
+============
+
+First of all, thank you for your interest in contributing to pyOpenSSL!
+
+Filing bug reports
+------------------
+
+Bug reports are very welcome.
+Please file them on the Github issue tracker.
+Good bug reports come with extensive descriptions of the error and how to reproduce it.
+Reporters are strongly encouraged to include an `short, self contained, correct example <http://www.sscce.org/>`_.
+
+Patches
+-------
+
+All patches to pyOpenSSL should be submitted in the form of pull requests to the main pyOpenSSL repository, ``pyca/pyopenssl``.
+These pull requests should satisfy the following properties:
+
+- The branch referenced should be a `feature branch`_ focusing on one particular improvement to pyOpenSSL.
+ Create different branches and different pull requests for unrelated features or bugfixes.
+- The branch referenced should have a distinctive name (in particular, please do not open pull requests for your ``master`` branch).
+- Code should follow `PEP 8`_, especially in the "do what code around you does" sense.
+ One notable way pyOpenSSL code differs, for example, is that there should be three empty lines between module-level elements,and two empty lines between class-level elements.
+ Methods and functions are named in ``snake_case``.
+ Follow OpenSSL naming for callables whenever possible is preferred.
+- Pull requests that introduce code must test all new behavior they introduce as well as for previously untested or poorly tested behavior that they touch.
+- Pull requests are not allowed to break existing tests.
+- Pull requests that introduce features or fix bugs should note those changes in the ``ChangeLog`` text file in the root of the repository.
+ They should also document the changes, both in docstrings and in the documentation in the ``doc/`` directory.
+
+Finally, pull requests must be reviewed before merging.
+This process mirrors the `cryptography code review process`_.
+Everyone can perform reviews; this is a very valuable way to contribute, and is highly encouraged.
+
+Pull requests are merged by members of the `pyopenssl-committers team <https://github.com/orgs/pyca/teams/pyopenssl-committers>`_.
+They should, of course, keep all the requirements detailed in this document as well as the pyca/cryptography merge requirements in mind.
+
+The final responsibility for the reviewing of merged code lies with the person merging it; since pyOpenSSL is obviously a sensitive project from a security perspective, so reviewers are strongly encouraged to take this review and merge process very seriously.
+
+.. _PEP 8: http://legacy.python.org/dev/peps/pep-0008/
+.. _cryptography code review process: https://cryptography.io/en/latest/development/reviewing-patches/
+.. _feature branch: http://nvie.com/posts/a-successful-git-branching-model/
diff --git a/ChangeLog b/ChangeLog
index 873c1c4..164c9ad 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,29 @@
+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``.
+
+2014-03-28 Jean-Paul Calderone <exarkun@twistedmatrix.com>
+
+ * OpenSSL/tsafe.py: Replace the use of ``apply`` (which has been
+ removed in Python 3) with the equivalent syntax.
+
+2014-03-28 Jonathan Giannuzzi <jonathan@giannuzzi.be>
+
+ * OpenSSL/crypto.py: Fix memory leak in _X509_REVOKED_dup.
+ * leakcheck/crypto.py: Add checks for _X509_REVOKED_dup, CRL.add_revoked
+ and CRL.get_revoked.
+ * setup.py: Require cryptography 0.3 to have the ASN1_TIME_free binding.
+
2014-03-02 Stephen Holsapple <sholsapp@gmail.com>
* OpenSSL/crypto.py: Add ``get_extensions`` method to ``X509Req``.
diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py
index a3a525e..f04f732 100644
--- a/OpenSSL/SSL.py
+++ b/OpenSSL/SSL.py
@@ -1493,6 +1493,112 @@ class Connection(object):
if not result:
_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.
+
+ :returns: The name of the currently used cipher or :py:obj:`None`
+ if no connection has been established.
+ :rtype: :py:class:`unicode` or :py:class:`NoneType`
+ """
+ cipher = _lib.SSL_get_current_cipher(self._ssl)
+ if cipher == _ffi.NULL:
+ return None
+ else:
+ name = _ffi.string(_lib.SSL_CIPHER_get_name(cipher))
+ return name.decode("utf-8")
+
+
+ def get_cipher_bits(self):
+ """
+ Obtain the number of secret bits of the currently used cipher.
+
+ :returns: The number of secret bits of the currently used cipher
+ or :py:obj:`None` if no connection has been established.
+ :rtype: :py:class:`int` or :py:class:`NoneType`
+ """
+ cipher = _lib.SSL_get_current_cipher(self._ssl)
+ if cipher == _ffi.NULL:
+ return None
+ else:
+ return _lib.SSL_CIPHER_get_bits(cipher, _ffi.NULL)
+
+
+ def get_cipher_version(self):
+ """
+ Obtain the protocol version of the currently used cipher.
+
+ :returns: The protocol name of the currently used cipher
+ or :py:obj:`None` if no connection has been established.
+ :rtype: :py:class:`unicode` or :py:class:`NoneType`
+ """
+ cipher = _lib.SSL_get_current_cipher(self._ssl)
+ if cipher == _ffi.NULL:
+ return None
+ else:
+ version =_ffi.string(_lib.SSL_CIPHER_get_version(cipher))
+ return version.decode("utf-8")
+
+
+
ConnectionType = Connection
# This is similar to the initialization calls at the end of OpenSSL/crypto.py
diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py
index ed0b629..65e28d7 100644
--- a/OpenSSL/crypto.py
+++ b/OpenSSL/crypto.py
@@ -1323,9 +1323,11 @@ def _X509_REVOKED_dup(original):
_raise_current_error()
if original.serialNumber != _ffi.NULL:
+ _lib.ASN1_INTEGER_free(copy.serialNumber)
copy.serialNumber = _lib.ASN1_INTEGER_dup(original.serialNumber)
if original.revocationDate != _ffi.NULL:
+ _lib.ASN1_TIME_free(copy.revocationDate)
copy.revocationDate = _lib.M_ASN1_TIME_dup(original.revocationDate)
if original.extensions != _ffi.NULL:
diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py
index a1cb417..ca896d9 100644
--- a/OpenSSL/test/test_ssl.py
+++ b/OpenSSL/test/test_ssl.py
@@ -14,7 +14,7 @@ from os.path import join
from unittest import main
from weakref import ref
-from six import PY3, u
+from six import PY3, text_type, u
from OpenSSL.crypto import TYPE_RSA, FILETYPE_PEM
from OpenSSL.crypto import PKey, X509, X509Extension, X509Store
@@ -1994,6 +1994,139 @@ class ConnectionTests(TestCase, _LoopbackMixin):
# 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.
+ """
+ ctx = Context(TLSv1_METHOD)
+ conn = Connection(ctx, None)
+ self.assertIdentical(conn.get_cipher_name(), None)
+
+
+ def test_get_cipher_name(self):
+ """
+ :py:obj:`Connection.get_cipher_name` returns a :py:class:`unicode`
+ string giving the name of the currently used cipher.
+ """
+ server, client = self._loopback()
+ server_cipher_name, client_cipher_name = \
+ server.get_cipher_name(), client.get_cipher_name()
+
+ self.assertIsInstance(server_cipher_name, text_type)
+ self.assertIsInstance(client_cipher_name, text_type)
+
+ self.assertEqual(server_cipher_name, client_cipher_name)
+
+
+ def test_get_cipher_version_before_connect(self):
+ """
+ :py:obj:`Connection.get_cipher_version` returns :py:obj:`None` if no
+ connection has been established.
+ """
+ ctx = Context(TLSv1_METHOD)
+ conn = Connection(ctx, None)
+ self.assertIdentical(conn.get_cipher_version(), None)
+
+
+ def test_get_cipher_version(self):
+ """
+ :py:obj:`Connection.get_cipher_version` returns a :py:class:`unicode`
+ string giving the protocol name of the currently used cipher.
+ """
+ server, client = self._loopback()
+ server_cipher_version, client_cipher_version = \
+ server.get_cipher_version(), client.get_cipher_version()
+
+ self.assertIsInstance(server_cipher_version, text_type)
+ self.assertIsInstance(client_cipher_version, text_type)
+
+ self.assertEqual(server_cipher_version, client_cipher_version)
+
+
+ def test_get_cipher_bits_before_connect(self):
+ """
+ :py:obj:`Connection.get_cipher_bits` returns :py:obj:`None` if no
+ connection has been established.
+ """
+ ctx = Context(TLSv1_METHOD)
+ conn = Connection(ctx, None)
+ self.assertIdentical(conn.get_cipher_bits(), None)
+
+
+ def test_get_cipher_bits(self):
+ """
+ :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 = \
+ server.get_cipher_bits(), client.get_cipher_bits()
+
+ self.assertIsInstance(server_cipher_bits, int)
+ self.assertIsInstance(client_cipher_bits, int)
+
+ self.assertEqual(server_cipher_bits, client_cipher_bits)
+
class ConnectionGetCipherListTests(TestCase):
diff --git a/OpenSSL/test/test_tsafe.py b/OpenSSL/test/test_tsafe.py
new file mode 100644
index 0000000..0456957
--- /dev/null
+++ b/OpenSSL/test/test_tsafe.py
@@ -0,0 +1,24 @@
+# Copyright (C) Jean-Paul Calderone
+# See LICENSE for details.
+
+"""
+Unit tests for :py:obj:`OpenSSL.tsafe`.
+"""
+
+from OpenSSL.SSL import TLSv1_METHOD, Context
+from OpenSSL.tsafe import Connection
+from OpenSSL.test.util import TestCase
+
+
+class ConnectionTest(TestCase):
+ """
+ Tests for :py:obj:`OpenSSL.tsafe.Connection`.
+ """
+ def test_instantiation(self):
+ """
+ :py:obj:`OpenSSL.tsafe.Connection` can be instantiated.
+ """
+ # The following line should not throw an error. This isn't an ideal
+ # test. It would be great to refactor the other Connection tests so
+ # they could automatically be applied to this class too.
+ Connection(Context(TLSv1_METHOD), None)
diff --git a/OpenSSL/tsafe.py b/OpenSSL/tsafe.py
index 9d7ad2f..3a9c710 100644
--- a/OpenSSL/tsafe.py
+++ b/OpenSSL/tsafe.py
@@ -8,7 +8,7 @@ del threading
class Connection:
def __init__(self, *args):
- self._ssl_conn = apply(_ssl.Connection, args)
+ self._ssl_conn = _ssl.Connection(*args)
self._lock = _RLock()
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
diff --git a/README b/README.rst
index 1b2a093..de9aa4e 100644
--- a/README
+++ b/README.rst
@@ -7,3 +7,6 @@ See the file INSTALL for installation instructions.
See http://github.com/pyca/pyopenssl for development.
See https://mail.python.org/mailman/listinfo/pyopenssl-users for the discussion mailing list.
+
+.. image:: https://coveralls.io/repos/pyca/pyopenssl/badge.png
+ :target: https://coveralls.io/r/pyca/pyopenssl
diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst
index 54d790c..b7eca70 100644
--- a/doc/api/ssl.rst
+++ b/doc/api/ssl.rst
@@ -263,7 +263,7 @@ Context objects have the following methods:
.. py:method:: Context.get_cert_store()
Retrieve the certificate store (a X509Store object) that the context uses.
- This can be used to add "trusted" certificates without using the.
+ This can be used to add "trusted" certificates without using the
:py:meth:`load_verify_locations` method.
@@ -786,6 +786,43 @@ Connection objects have the following methods:
.. 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/leakcheck/crypto.py b/leakcheck/crypto.py
index 6a9af92..f5fe2f8 100644
--- a/leakcheck/crypto.py
+++ b/leakcheck/crypto.py
@@ -4,7 +4,10 @@
import sys
from OpenSSL.crypto import (
- FILETYPE_PEM, TYPE_DSA, Error, PKey, X509, load_privatekey)
+ FILETYPE_PEM, TYPE_DSA, Error, PKey, X509, load_privatekey, CRL, Revoked,
+ _X509_REVOKED_dup)
+
+from OpenSSL._util import lib as _lib
@@ -101,6 +104,47 @@ FCB5K3c2kkTv2KjcCAimjxkE+SBKfHg35W0wB0AWkXpVFO5W/TbHg4tqtkpt/KMn
pass
+
+class Checker_CRL(BaseChecker):
+ """
+ Leak checks for L{CRL.add_revoked} and L{CRL.get_revoked}.
+ """
+ def check_add_revoked(self):
+ """
+ Call the add_revoked method repeatedly on an empty CRL.
+ """
+ for i in xrange(self.iterations * 200):
+ CRL().add_revoked(Revoked())
+
+
+ def check_get_revoked(self):
+ """
+ Create a CRL object with 100 Revoked objects, then call the
+ get_revoked method repeatedly.
+ """
+ crl = CRL()
+ for i in xrange(100):
+ crl.add_revoked(Revoked())
+ for i in xrange(self.iterations):
+ crl.get_revoked()
+
+
+
+class Checker_X509_REVOKED_dup(BaseChecker):
+ """
+ Leak checks for :py:obj:`_X509_REVOKED_dup`.
+ """
+ def check_X509_REVOKED_dup(self):
+ """
+ Copy an empty Revoked object repeatedly. The copy is not garbage
+ collected, therefore it needs to be manually freed.
+ """
+ for i in xrange(self.iterations * 100):
+ revoked_copy = _X509_REVOKED_dup(Revoked()._revoked)
+ _lib.X509_REVOKED_free(revoked_copy)
+
+
+
def vmsize():
return [x for x in file('/proc/self/status').readlines() if 'VmSize' in x]