summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean-Paul Calderone <exarkun@twistedmatrix.com>2014-05-05 13:01:16 -0400
committerJean-Paul Calderone <exarkun@twistedmatrix.com>2014-05-05 13:01:16 -0400
commit780967eaf38054ec84dbccd436dba32c8b04b469 (patch)
treee4ee8ff56b290994ef4ca92ae9318243b80b9d38
parent4ca24eedfa42ddb326cb2cb3aef73e1397c7e70f (diff)
parent238fb74dc95828a8e9fb0369460f1096663f3b78 (diff)
downloadpyopenssl-780967eaf38054ec84dbccd436dba32c8b04b469.tar.gz
merge master
-rw-r--r--.gitignore6
-rw-r--r--.travis.yml59
-rw-r--r--ChangeLog28
-rw-r--r--OpenSSL/SSL.py34
-rw-r--r--OpenSSL/crypto.py164
-rw-r--r--OpenSSL/test/test_crypto.py245
-rw-r--r--OpenSSL/test/test_ssl.py56
-rw-r--r--OpenSSL/test/util.py147
-rw-r--r--README.rst (renamed from README)3
-rw-r--r--doc/api/crypto.rst22
-rw-r--r--doc/api/ssl.rst9
-rw-r--r--leakcheck/crypto.py62
12 files changed, 801 insertions, 34 deletions
diff --git a/.gitignore b/.gitignore
index 867434b..7b60bb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/ChangeLog b/ChangeLog
index 86e644f..c22aaae 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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])
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/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]