diff options
author | Jean-Paul Calderone <exarkun@twistedmatrix.com> | 2014-04-17 10:41:44 -0400 |
---|---|---|
committer | Jean-Paul Calderone <exarkun@twistedmatrix.com> | 2014-04-17 10:41:44 -0400 |
commit | 256090bc9ada777a045404c05864eb3fc1fcae96 (patch) | |
tree | 02e7889f2283368b07b78a1bdb12f9f769e66505 | |
parent | 61d7d393fb8ef50deed4424db94e3ea72f95c4c2 (diff) | |
parent | 434d03bbb3255cc313680826939801760b351d2a (diff) | |
download | pyopenssl-256090bc9ada777a045404c05864eb3fc1fcae96.tar.gz |
Merge remote-tracking branch 'pyca/master' into ecdhe
-rw-r--r-- | .travis.yml | 57 | ||||
-rw-r--r-- | CONTRIBUTING.rst | 43 | ||||
-rw-r--r-- | ChangeLog | 26 | ||||
-rw-r--r-- | OpenSSL/SSL.py | 106 | ||||
-rw-r--r-- | OpenSSL/crypto.py | 2 | ||||
-rw-r--r-- | OpenSSL/test/test_ssl.py | 135 | ||||
-rw-r--r-- | OpenSSL/test/test_tsafe.py | 24 | ||||
-rw-r--r-- | OpenSSL/tsafe.py | 2 | ||||
-rw-r--r-- | README.rst (renamed from README) | 3 | ||||
-rw-r--r-- | doc/api/ssl.rst | 39 | ||||
-rw-r--r-- | leakcheck/crypto.py | 46 |
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/ @@ -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', @@ -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] |