diff options
author | Jean-Paul Calderone <exarkun@twistedmatrix.com> | 2014-05-05 13:01:16 -0400 |
---|---|---|
committer | Jean-Paul Calderone <exarkun@twistedmatrix.com> | 2014-05-05 13:01:16 -0400 |
commit | 780967eaf38054ec84dbccd436dba32c8b04b469 (patch) | |
tree | e4ee8ff56b290994ef4ca92ae9318243b80b9d38 | |
parent | 4ca24eedfa42ddb326cb2cb3aef73e1397c7e70f (diff) | |
parent | 238fb74dc95828a8e9fb0369460f1096663f3b78 (diff) | |
download | pyopenssl-780967eaf38054ec84dbccd436dba32c8b04b469.tar.gz |
merge master
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | .travis.yml | 59 | ||||
-rw-r--r-- | ChangeLog | 28 | ||||
-rw-r--r-- | OpenSSL/SSL.py | 34 | ||||
-rw-r--r-- | OpenSSL/crypto.py | 164 | ||||
-rw-r--r-- | OpenSSL/test/test_crypto.py | 245 | ||||
-rw-r--r-- | OpenSSL/test/test_ssl.py | 56 | ||||
-rw-r--r-- | OpenSSL/test/util.py | 147 | ||||
-rw-r--r-- | README.rst (renamed from README) | 3 | ||||
-rw-r--r-- | doc/api/crypto.rst | 22 | ||||
-rw-r--r-- | doc/api/ssl.rst | 9 | ||||
-rw-r--r-- | leakcheck/crypto.py | 62 |
12 files changed, 801 insertions, 34 deletions
@@ -1,3 +1,7 @@ build dist -*.egg-info
\ No newline at end of file +*.egg-info +*.pyc +*.pyo +__pycache__ +.tox diff --git a/.travis.yml b/.travis.yml index 52f1032..f359de1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python os: - linux - - osx python: - "pypy" @@ -10,21 +9,42 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" matrix: include: + # Also run the tests against cryptography master. - python: "2.6" - env: CRYPTOGRAPHY_GIT_MASTER=true + env: + CRYPTOGRAPHY_GIT_MASTER=true - python: "2.7" - env: CRYPTOGRAPHY_GIT_MASTER=true + env: + CRYPTOGRAPHY_GIT_MASTER=true - python: "3.2" - env: CRYPTOGRAPHY_GIT_MASTER=true + env: + CRYPTOGRAPHY_GIT_MASTER=true - python: "3.3" - env: CRYPTOGRAPHY_GIT_MASTER=true + env: + CRYPTOGRAPHY_GIT_MASTER=true + - python: "3.4" + env: + CRYPTOGRAPHY_GIT_MASTER=true - python: "pypy" - env: CRYPTOGRAPHY_GIT_MASTER=true + 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: + 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 @@ -36,5 +56,28 @@ 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 + - | + pip install -e . + - | + coverage run --branch --source=OpenSSL setup.py bdist_wheel test + - | + coverage report -m + - | + python -c "import OpenSSL.SSL; print(OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION))" + +after_success: + - coveralls + +notifications: + email: false @@ -1,4 +1,4 @@ -2014-04-02 Jean-Paul Calderone <exarkun@twistedmatrix.com> +2014-05-05 Jean-Paul Calderone <exarkun@twistedmatrix.com> * OpenSSL/SSL.py: Fix a regression in which the first argument of the "verify" callback was incorrectly a ``Context`` instance @@ -6,6 +6,25 @@ * OpenSSL/test/test_ssl.py: Add a test for the value passed as the first argument of the "verify" callback. +2014-04-19 Jean-Paul Calderone <exarkun@twistedmatrix.com> + + * OpenSSL/crypto.py: Based on work from Alex Gaynor, Andrew + Lutomirski, Tobias Oberstein, Laurens Van Houtven, and Hynek + Schlawack, add ``get_elliptic_curve`` and ``get_elliptic_curves`` + to support TLS ECDHE modes. + * OpenSSL/SSL.py: Add ``Context.set_tmp_ecdh`` to configure a TLS + context with a particular elliptic curve for ECDHE modes. + +2014-04-19 Markus Unterwaditzer <markus@unterwaditzer.net> + + * OpenSSL/SSL.py: ``Connection.send`` and ``Connection.sendall`` + now also accept the ``buffer`` type as data. + +2014-04-05 Stephen Holsapple <sholsapp@gmail.com> + + * OpenSSL/crypto.py: Make ``load_pkcs12`` backwards compatible with + pyOpenSSL 0.13 by making passphrase optional. + 2014-03-30 Fedor Brunner <fedor.brunner@azet.sk> * OpenSSL/SSL.py: Add ``get_finished``, ``get_peer_finished`` @@ -25,6 +44,13 @@ * 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 40fe5a6..7b1cbc1 100644 --- a/OpenSSL/SSL.py +++ b/OpenSSL/SSL.py @@ -24,6 +24,12 @@ except NameError: class _memoryview(object): pass +try: + _buffer = buffer +except NameError: + class _buffer(object): + pass + OPENSSL_VERSION_NUMBER = _lib.OPENSSL_VERSION_NUMBER SSLEAY_VERSION = _lib.SSLEAY_VERSION SSLEAY_CFLAGS = _lib.SSLEAY_CFLAGS @@ -124,7 +130,6 @@ SSL_CB_CONNECT_EXIT = _lib.SSL_CB_CONNECT_EXIT SSL_CB_HANDSHAKE_START = _lib.SSL_CB_HANDSHAKE_START SSL_CB_HANDSHAKE_DONE = _lib.SSL_CB_HANDSHAKE_DONE - class Error(Exception): """ An error occurred in an `OpenSSL.SSL` API. @@ -602,6 +607,19 @@ class Context(object): _lib.SSL_CTX_set_tmp_dh(self._context, dh) + def set_tmp_ecdh(self, curve): + """ + Select a curve to use for ECDHE key exchange. + + :param curve: A curve object to use as returned by either + :py:meth:`OpenSSL.crypto.get_elliptic_curve` or + :py:meth:`OpenSSL.crypto.get_elliptic_curves`. + + :return: None + """ + _lib.SSL_CTX_set_tmp_ecdh(self._context, curve._to_EC_KEY()) + + def set_cipher_list(self, cipher_list): """ Change the cipher list @@ -944,15 +962,17 @@ class Connection(object): WantWrite or WantX509Lookup exceptions on this, you have to call the method again with the SAME buffer. - :param buf: The string to send + :param buf: The string, buffer or memoryview to send :param flags: (optional) Included for compatibility with the socket API, the value is ignored :return: The number of bytes written """ if isinstance(buf, _memoryview): buf = buf.tobytes() + if isinstance(buf, _buffer): + buf = str(buf) if not isinstance(buf, bytes): - raise TypeError("data must be a byte string") + raise TypeError("data must be a memoryview, buffer or byte string") result = _lib.SSL_write(self._ssl, buf, len(buf)) self._raise_ssl_error(self._ssl, result) @@ -966,15 +986,17 @@ class Connection(object): all data is sent. If an error occurs, it's impossible to tell how much data has been sent. - :param buf: The string to send + :param buf: The string, buffer or memoryview to send :param flags: (optional) Included for compatibility with the socket API, the value is ignored :return: The number of bytes written """ if isinstance(buf, _memoryview): buf = buf.tobytes() + if isinstance(buf, _buffer): + buf = str(buf) if not isinstance(buf, bytes): - raise TypeError("buf must be a byte string") + raise TypeError("buf must be a memoryview, buffer or byte string") left_to_send = len(buf) total_sent = 0 @@ -1218,7 +1240,7 @@ class Connection(object): The makefile() method is not implemented, since there is no dup semantics for SSL connections - :raise NotImplementedError + :raise: NotImplementedError """ raise NotImplementedError("Cannot make file object of OpenSSL.SSL.Connection") diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index ed0b629..54569ea 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -5,7 +5,8 @@ from operator import __eq__, __ne__, __lt__, __le__, __gt__, __ge__ from six import ( integer_types as _integer_types, - text_type as _text_type) + text_type as _text_type, + PY3 as _PY3) from OpenSSL._util import ( ffi as _ffi, @@ -263,6 +264,156 @@ PKeyType = PKey +class _EllipticCurve(object): + """ + A representation of a supported elliptic curve. + + @cvar _curves: :py:obj:`None` until an attempt is made to load the curves. + Thereafter, a :py:type:`set` containing :py:type:`_EllipticCurve` + instances each of which represents one curve supported by the system. + @type _curves: :py:type:`NoneType` or :py:type:`set` + """ + _curves = None + + if _PY3: + # This only necessary on Python 3. Morever, it is broken on Python 2. + def __ne__(self, other): + """ + Implement cooperation with the right-hand side argument of ``!=``. + + Python 3 seems to have dropped this cooperation in this very narrow + circumstance. + """ + if isinstance(other, _EllipticCurve): + return super(_EllipticCurve, self).__ne__(other) + return NotImplemented + + + @classmethod + def _load_elliptic_curves(cls, lib): + """ + Get the curves supported by OpenSSL. + + :param lib: The OpenSSL library binding object. + + :return: A :py:type:`set` of ``cls`` instances giving the names of the + elliptic curves the underlying library supports. + """ + if lib.Cryptography_HAS_EC: + num_curves = lib.EC_get_builtin_curves(_ffi.NULL, 0) + builtin_curves = _ffi.new('EC_builtin_curve[]', num_curves) + # The return value on this call should be num_curves again. We could + # check it to make sure but if it *isn't* then.. what could we do? + # Abort the whole process, I suppose...? -exarkun + lib.EC_get_builtin_curves(builtin_curves, num_curves) + return set( + cls.from_nid(lib, c.nid) + for c in builtin_curves) + return set() + + + @classmethod + def _get_elliptic_curves(cls, lib): + """ + Get, cache, and return the curves supported by OpenSSL. + + :param lib: The OpenSSL library binding object. + + :return: A :py:type:`set` of ``cls`` instances giving the names of the + elliptic curves the underlying library supports. + """ + if cls._curves is None: + cls._curves = cls._load_elliptic_curves(lib) + return cls._curves + + + @classmethod + def from_nid(cls, lib, nid): + """ + Instantiate a new :py:class:`_EllipticCurve` associated with the given + OpenSSL NID. + + :param lib: The OpenSSL library binding object. + + :param nid: The OpenSSL NID the resulting curve object will represent. + This must be a curve NID (and not, for example, a hash NID) or + subsequent operations will fail in unpredictable ways. + :type nid: :py:class:`int` + + :return: The curve object. + """ + return cls(lib, nid, _ffi.string(lib.OBJ_nid2sn(nid)).decode("ascii")) + + + def __init__(self, lib, nid, name): + """ + :param _lib: The :py:mod:`cryptography` binding instance used to + interface with OpenSSL. + + :param _nid: The OpenSSL NID identifying the curve this object + represents. + :type _nid: :py:class:`int` + + :param name: The OpenSSL short name identifying the curve this object + represents. + :type name: :py:class:`unicode` + """ + self._lib = lib + self._nid = nid + self.name = name + + + def __repr__(self): + return "<Curve %r>" % (self.name,) + + + def _to_EC_KEY(self): + """ + Create a new OpenSSL EC_KEY structure initialized to use this curve. + + The structure is automatically garbage collected when the Python object + is garbage collected. + """ + key = self._lib.EC_KEY_new_by_curve_name(self._nid) + return _ffi.gc(key, _lib.EC_KEY_free) + + + +def get_elliptic_curves(): + """ + Return a set of objects representing the elliptic curves supported in the + OpenSSL build in use. + + The curve objects have a :py:class:`unicode` ``name`` attribute by which + they identify themselves. + + The curve objects are useful as values for the argument accepted by + :py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be + used for ECDHE key exchange. + """ + return _EllipticCurve._get_elliptic_curves(_lib) + + + +def get_elliptic_curve(name): + """ + Return a single curve object selected by name. + + See :py:func:`get_elliptic_curves` for information about curve objects. + + :param name: The OpenSSL short name identifying the curve object to + retrieve. + :type name: :py:class:`unicode` + + If the named curve is not supported then :py:class:`ValueError` is raised. + """ + for curve in get_elliptic_curves(): + if curve.name == name: + return curve + raise ValueError("unknown curve name", name) + + + class X509Name(object): def __init__(self, name): """ @@ -1323,9 +1474,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: @@ -2213,7 +2366,7 @@ def load_pkcs7_data(type, buffer): -def load_pkcs12(buffer, passphrase): +def load_pkcs12(buffer, passphrase=None): """ Load a PKCS12 object from a buffer @@ -2226,6 +2379,13 @@ def load_pkcs12(buffer, passphrase): bio = _new_mem_buf(buffer) + # Use null passphrase if passphrase is None or empty string. With PKCS#12 + # password based encryption no password and a zero length password are two + # different things, but OpenSSL implementation will try both to figure out + # which one works. + if not passphrase: + passphrase = _ffi.NULL + p12 = _lib.d2i_PKCS12_bio(bio, _ffi.NULL) if p12 == _ffi.NULL: _raise_current_error() diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index a3685a9..bbe5d05 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -11,7 +11,7 @@ import os, re from subprocess import PIPE, Popen from datetime import datetime, timedelta -from six import binary_type +from six import u, b, binary_type from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType @@ -25,9 +25,10 @@ from OpenSSL.crypto import PKCS7Type, load_pkcs7_data from OpenSSL.crypto import PKCS12, PKCS12Type, load_pkcs12 from OpenSSL.crypto import CRL, Revoked, load_crl from OpenSSL.crypto import NetscapeSPKI, NetscapeSPKIType -from OpenSSL.crypto import sign, verify -from OpenSSL.test.util import TestCase, b -from OpenSSL._util import native +from OpenSSL.crypto import ( + sign, verify, get_elliptic_curve, get_elliptic_curves) +from OpenSSL.test.util import EqualityTestsMixin, TestCase +from OpenSSL._util import native, lib def normalize_certificate_pem(pem): return dump_certificate(FILETYPE_PEM, load_certificate(FILETYPE_PEM, pem)) @@ -1940,6 +1941,21 @@ class PKCS12Tests(TestCase): self.assertEqual(recovered_cert[-len(ca):], ca) + def verify_pkcs12_container(self, p12): + """ + Verify that the PKCS#12 container contains the correct client + certificate and private key. + + :param p12: The PKCS12 instance to verify. + :type p12: :py:class:`PKCS12` + """ + cert_pem = dump_certificate(FILETYPE_PEM, p12.get_certificate()) + key_pem = dump_privatekey(FILETYPE_PEM, p12.get_privatekey()) + self.assertEqual( + (client_cert_pem, client_key_pem, None), + (cert_pem, key_pem, p12.get_ca_certificates())) + + def test_load_pkcs12(self): """ A PKCS12 string generated using the openssl command line can be loaded @@ -1949,14 +1965,70 @@ class PKCS12Tests(TestCase): pem = client_key_pem + client_cert_pem p12_str = _runopenssl( pem, b"pkcs12", b"-export", b"-clcerts", b"-passout", b"pass:" + passwd) - p12 = load_pkcs12(p12_str, passwd) - # verify - self.assertTrue(isinstance(p12, PKCS12)) - cert_pem = dump_certificate(FILETYPE_PEM, p12.get_certificate()) - self.assertEqual(cert_pem, client_cert_pem) - key_pem = dump_privatekey(FILETYPE_PEM, p12.get_privatekey()) - self.assertEqual(key_pem, client_key_pem) - self.assertEqual(None, p12.get_ca_certificates()) + p12 = load_pkcs12(p12_str, passphrase=passwd) + self.verify_pkcs12_container(p12) + + + def test_load_pkcs12_no_passphrase(self): + """ + A PKCS12 string generated using openssl command line can be loaded with + :py:obj:`load_pkcs12` without a passphrase and its components extracted + and examined. + """ + pem = client_key_pem + client_cert_pem + p12_str = _runopenssl( + pem, b"pkcs12", b"-export", b"-clcerts", b"-passout", b"pass:") + p12 = load_pkcs12(p12_str) + self.verify_pkcs12_container(p12) + + + def _dump_and_load(self, dump_passphrase, load_passphrase): + """ + A helper method to dump and load a PKCS12 object. + """ + p12 = self.gen_pkcs12(client_cert_pem, client_key_pem) + dumped_p12 = p12.export(passphrase=dump_passphrase, iter=2, maciter=3) + return load_pkcs12(dumped_p12, passphrase=load_passphrase) + + + def test_load_pkcs12_null_passphrase_load_empty(self): + """ + A PKCS12 string can be dumped with a null passphrase, loaded with an + empty passphrase with :py:obj:`load_pkcs12`, and its components + extracted and examined. + """ + self.verify_pkcs12_container( + self._dump_and_load(dump_passphrase=None, load_passphrase=b'')) + + + def test_load_pkcs12_null_passphrase_load_null(self): + """ + A PKCS12 string can be dumped with a null passphrase, loaded with a + null passphrase with :py:obj:`load_pkcs12`, and its components + extracted and examined. + """ + self.verify_pkcs12_container( + self._dump_and_load(dump_passphrase=None, load_passphrase=None)) + + + def test_load_pkcs12_empty_passphrase_load_empty(self): + """ + A PKCS12 string can be dumped with an empty passphrase, loaded with an + empty passphrase with :py:obj:`load_pkcs12`, and its components + extracted and examined. + """ + self.verify_pkcs12_container( + self._dump_and_load(dump_passphrase=b'', load_passphrase=b'')) + + + def test_load_pkcs12_empty_passphrase_load_null(self): + """ + A PKCS12 string can be dumped with an empty passphrase, loaded with a + null passphrase with :py:obj:`load_pkcs12`, and its components + extracted and examined. + """ + self.verify_pkcs12_container( + self._dump_and_load(dump_passphrase=b'', load_passphrase=None)) def test_load_pkcs12_garbage(self): @@ -3058,5 +3130,154 @@ class SignVerifyTests(TestCase): verify(good_cert, sig, content, "sha1") + +class EllipticCurveTests(TestCase): + """ + Tests for :py:class:`_EllipticCurve`, :py:obj:`get_elliptic_curve`, and + :py:obj:`get_elliptic_curves`. + """ + def test_set(self): + """ + :py:obj:`get_elliptic_curves` returns a :py:obj:`set`. + """ + self.assertIsInstance(get_elliptic_curves(), set) + + + def test_some_curves(self): + """ + If :py:mod:`cryptography` has elliptic curve support then the set + returned by :py:obj:`get_elliptic_curves` has some elliptic curves in + it. + + There could be an OpenSSL that violates this assumption. If so, this + test will fail and we'll find out. + """ + curves = get_elliptic_curves() + if lib.Cryptography_HAS_EC: + self.assertTrue(curves) + else: + self.assertFalse(curves) + + + def test_a_curve(self): + """ + :py:obj:`get_elliptic_curve` can be used to retrieve a particular + supported curve. + """ + curves = get_elliptic_curves() + if curves: + curve = next(iter(curves)) + self.assertEqual(curve.name, get_elliptic_curve(curve.name).name) + else: + self.assertRaises(ValueError, get_elliptic_curve, u("prime256v1")) + + + def test_not_a_curve(self): + """ + :py:obj:`get_elliptic_curve` raises :py:class:`ValueError` if called + with a name which does not identify a supported curve. + """ + self.assertRaises( + ValueError, get_elliptic_curve, u("this curve was just invented")) + + + def test_repr(self): + """ + The string representation of a curve object includes simply states the + object is a curve and what its name is. + """ + curves = get_elliptic_curves() + if curves: + curve = next(iter(curves)) + self.assertEqual("<Curve %r>" % (curve.name,), repr(curve)) + + + def test_to_EC_KEY(self): + """ + The curve object can export a version of itself as an EC_KEY* via the + private :py:meth:`_EllipticCurve._to_EC_KEY`. + """ + curves = get_elliptic_curves() + if curves: + curve = next(iter(curves)) + # It's not easy to assert anything about this object. However, see + # leakcheck/crypto.py for a test that demonstrates it at least does + # not leak memory. + curve._to_EC_KEY() + + + +class EllipticCurveFactory(object): + """ + A helper to get the names of two curves. + """ + def __init__(self): + curves = iter(get_elliptic_curves()) + try: + self.curve_name = next(curves).name + self.another_curve_name = next(curves).name + except StopIteration: + self.curve_name = self.another_curve_name = None + + + +class EllipticCurveEqualityTests(TestCase, EqualityTestsMixin): + """ + Tests :py:type:`_EllipticCurve`\ 's implementation of ``==`` and ``!=``. + """ + curve_factory = EllipticCurveFactory() + + if curve_factory.curve_name is None: + skip = "There are no curves available there can be no curve objects." + + + def anInstance(self): + """ + Get the curve object for an arbitrary curve supported by the system. + """ + return get_elliptic_curve(self.curve_factory.curve_name) + + + def anotherInstance(self): + """ + Get the curve object for an arbitrary curve supported by the system - + but not the one returned by C{anInstance}. + """ + return get_elliptic_curve(self.curve_factory.another_curve_name) + + + +class EllipticCurveHashTests(TestCase): + """ + Tests for :py:type:`_EllipticCurve`\ 's implementation of hashing (thus use + as an item in a :py:type:`dict` or :py:type:`set`). + """ + curve_factory = EllipticCurveFactory() + + if curve_factory.curve_name is None: + skip = "There are no curves available there can be no curve objects." + + + def test_contains(self): + """ + The ``in`` operator reports that a :py:type:`set` containing a curve + does contain that curve. + """ + curve = get_elliptic_curve(self.curve_factory.curve_name) + curves = set([curve]) + self.assertIn(curve, curves) + + + def test_does_not_contain(self): + """ + The ``in`` operator reports that a :py:type:`set` not containing a + curve does not contain that curve. + """ + curve = get_elliptic_curve(self.curve_factory.curve_name) + curves = set([get_elliptic_curve(self.curve_factory.another_curve_name)]) + self.assertNotIn(curve, curves) + + + if __name__ == '__main__': main() diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py index 369b1b6..b30bed4 100644 --- a/OpenSSL/test/test_ssl.py +++ b/OpenSSL/test/test_ssl.py @@ -20,7 +20,9 @@ from OpenSSL.crypto import TYPE_RSA, FILETYPE_PEM from OpenSSL.crypto import PKey, X509, X509Extension, X509Store from OpenSSL.crypto import dump_privatekey, load_privatekey from OpenSSL.crypto import dump_certificate, load_certificate +from OpenSSL.crypto import get_elliptic_curves +from OpenSSL.SSL import _lib from OpenSSL.SSL import OPENSSL_VERSION_NUMBER, SSLEAY_VERSION, SSLEAY_CFLAGS from OpenSSL.SSL import SSLEAY_PLATFORM, SSLEAY_DIR, SSLEAY_BUILT_ON from OpenSSL.SSL import SENT_SHUTDOWN, RECEIVED_SHUTDOWN @@ -1213,6 +1215,18 @@ class ContextTests(TestCase, _LoopbackMixin): # XXX What should I assert here? -exarkun + def test_set_tmp_ecdh(self): + """ + :py:obj:`Context.set_tmp_ecdh` sets the elliptic curve for + Diffie-Hellman to the specified curve. + """ + context = Context(TLSv1_METHOD) + for curve in get_elliptic_curves(): + # The only easily "assertable" thing is that it does not raise an + # exception. + context.set_tmp_ecdh(curve) + + def test_set_cipher_list_bytes(self): """ :py:obj:`Context.set_cipher_list` accepts a :py:obj:`bytes` naming the @@ -1952,9 +1966,13 @@ class ConnectionTests(TestCase, _LoopbackMixin): """ client_socket, server_socket = socket_pair() # Fill up the client's send buffer so Connection won't be able to write - # anything. - msg = b"x" * 512 - for i in range(2048): + # anything. Only write a single byte at a time so we can be sure we + # completely fill the buffer. Even though the socket API is allowed to + # signal a short write via its return value it seems this doesn't + # always happen on all platforms (FreeBSD and OS X particular) for the + # very last bit of available buffer space. + msg = b"x" + for i in range(1024 * 1024 * 4): try: client_socket.send(msg) except error as e: @@ -2177,6 +2195,23 @@ class ConnectionSendTests(TestCase, _LoopbackMixin): self.assertEquals(client.recv(2), b('xy')) + try: + buffer + except NameError: + "cannot test sending buffer without buffer" + else: + def test_short_buffer(self): + """ + When passed a buffer containing a small number of bytes, + :py:obj:`Connection.send` transmits all of them and returns the number of + bytes sent. + """ + server, client = self._loopback() + count = server.send(buffer(b('xy'))) + self.assertEquals(count, 2) + self.assertEquals(client.recv(2), b('xy')) + + class ConnectionSendallTests(TestCase, _LoopbackMixin): """ @@ -2220,6 +2255,21 @@ class ConnectionSendallTests(TestCase, _LoopbackMixin): self.assertEquals(client.recv(1), b('x')) + try: + buffer + except NameError: + "cannot test sending buffers without buffers" + else: + def test_short_buffers(self): + """ + When passed a buffer containing a small number of bytes, + :py:obj:`Connection.sendall` transmits all of them. + """ + server, client = self._loopback() + server.sendall(buffer(b('x'))) + self.assertEquals(client.recv(1), b('x')) + + def test_long(self): """ :py:obj:`Connection.sendall` transmits all of the bytes in the string passed to diff --git a/OpenSSL/test/util.py b/OpenSSL/test/util.py index 4e4d812..21bbdc4 100644 --- a/OpenSSL/test/util.py +++ b/OpenSSL/test/util.py @@ -210,6 +210,23 @@ class TestCase(TestCase): return containee assertIn = failUnlessIn + def assertNotIn(self, containee, container, msg=None): + """ + Fail the test if C{containee} is found in C{container}. + + @param containee: the value that should not be in C{container} + @param container: a sequence type, or in the case of a mapping type, + will follow semantics of 'if key in dict.keys()' + @param msg: if msg is None, then the failure message will be + '%r in %r' % (first, second) + """ + if containee in container: + raise self.failureException(msg or "%r in %r" + % (containee, container)) + return containee + failIfIn = assertNotIn + + def failUnlessIdentical(self, first, second, msg=None): """ Fail the test if :py:data:`first` is not :py:data:`second`. This is an @@ -300,3 +317,133 @@ class TestCase(TestCase): self.assertTrue(isinstance(theType, type)) instance = theType(*constructionArgs) self.assertIdentical(type(instance), theType) + + + +class EqualityTestsMixin(object): + """ + A mixin defining tests for the standard implementation of C{==} and C{!=}. + """ + def anInstance(self): + """ + Return an instance of the class under test. Each call to this method + must return a different object. All objects returned must be equal to + each other. + """ + raise NotImplementedError() + + + def anotherInstance(self): + """ + Return an instance of the class under test. Each call to this method + must return a different object. The objects must not be equal to the + objects returned by C{anInstance}. They may or may not be equal to + each other (they will not be compared against each other). + """ + raise NotImplementedError() + + + def test_identicalEq(self): + """ + An object compares equal to itself using the C{==} operator. + """ + o = self.anInstance() + self.assertTrue(o == o) + + + def test_identicalNe(self): + """ + An object doesn't compare not equal to itself using the C{!=} operator. + """ + o = self.anInstance() + self.assertFalse(o != o) + + + def test_sameEq(self): + """ + Two objects that are equal to each other compare equal to each other + using the C{==} operator. + """ + a = self.anInstance() + b = self.anInstance() + self.assertTrue(a == b) + + + def test_sameNe(self): + """ + Two objects that are equal to each other do not compare not equal to + each other using the C{!=} operator. + """ + a = self.anInstance() + b = self.anInstance() + self.assertFalse(a != b) + + + def test_differentEq(self): + """ + Two objects that are not equal to each other do not compare equal to + each other using the C{==} operator. + """ + a = self.anInstance() + b = self.anotherInstance() + self.assertFalse(a == b) + + + def test_differentNe(self): + """ + Two objects that are not equal to each other compare not equal to each + other using the C{!=} operator. + """ + a = self.anInstance() + b = self.anotherInstance() + self.assertTrue(a != b) + + + def test_anotherTypeEq(self): + """ + The object does not compare equal to an object of an unrelated type + (which does not implement the comparison) using the C{==} operator. + """ + a = self.anInstance() + b = object() + self.assertFalse(a == b) + + + def test_anotherTypeNe(self): + """ + The object compares not equal to an object of an unrelated type (which + does not implement the comparison) using the C{!=} operator. + """ + a = self.anInstance() + b = object() + self.assertTrue(a != b) + + + def test_delegatedEq(self): + """ + The result of comparison using C{==} is delegated to the right-hand + operand if it is of an unrelated type. + """ + class Delegate(object): + def __eq__(self, other): + # Do something crazy and obvious. + return [self] + + a = self.anInstance() + b = Delegate() + self.assertEqual(a == b, [b]) + + + def test_delegateNe(self): + """ + The result of comparison using C{!=} is delegated to the right-hand + operand if it is of an unrelated type. + """ + class Delegate(object): + def __ne__(self, other): + # Do something crazy and obvious. + return [self] + + a = self.anInstance() + b = Delegate() + self.assertEqual(a != b, [b]) @@ -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/crypto.rst b/doc/api/crypto.rst index ee93cfb..b360e89 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -119,6 +119,28 @@ Generic exception used in the :py:mod:`.crypto` module. +.. py:function:: get_elliptic_curves + + Return a set of objects representing the elliptic curves supported in the + OpenSSL build in use. + + The curve objects have a :py:class:`unicode` ``name`` attribute by which + they identify themselves. + + The curve objects are useful as values for the argument accepted by + :py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be + used for ECDHE key exchange. + + +.. py:function:: get_elliptic_curve + + Return a single curve object selected by name. + + See :py:func:`get_elliptic_curves` for information about curve objects. + + If the named curve is not supported then :py:class:`ValueError` is raised. + + .. py:function:: dump_certificate(type, cert) Dump the certificate *cert* into a buffer string encoded with the type diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst index e1c1d8a..a75af1f 100644 --- a/doc/api/ssl.rst +++ b/doc/api/ssl.rst @@ -317,6 +317,15 @@ Context objects have the following methods: Load parameters for Ephemeral Diffie-Hellman from *dhfile*. +.. py:method:: Context.set_tmp_ecdh(curve) + + Select a curve to use for ECDHE key exchange. + + The valid values of *curve* are the objects returned by + :py:func:`OpenSSL.crypto.get_elliptic_curves` or + :py:func:`OpenSSL.crypto.get_elliptic_curve`. + + .. py:method:: Context.set_app_data(data) Associate *data* with this Context object. *data* can be retrieved diff --git a/leakcheck/crypto.py b/leakcheck/crypto.py index 6a9af92..ca79b7c 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, + get_elliptic_curves, _X509_REVOKED_dup) + +from OpenSSL._util import lib as _lib @@ -101,6 +104,63 @@ 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) + + + +class Checker_EllipticCurve(BaseChecker): + """ + Leak checks for :py:obj:`_EllipticCurve`. + """ + def check_to_EC_KEY(self): + """ + Repeatedly create an EC_KEY* from an :py:obj:`_EllipticCurve`. The + structure should be automatically garbage collected. + """ + curves = get_elliptic_curves() + if curves: + curve = next(iter(curves)) + for i in xrange(self.iterations * 1000): + curve._to_EC_KEY() + + def vmsize(): return [x for x in file('/proc/self/status').readlines() if 'VmSize' in x] |