Merge branch 'master' into simple-symmetric-encryption
diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py
index 3cc6a0e..2da156c 100644
--- a/cryptography/bindings/openssl/api.py
+++ b/cryptography/bindings/openssl/api.py
@@ -13,11 +13,107 @@
from __future__ import absolute_import, division, print_function
+import cffi
+
+
+class OpenSSLError(Exception):
+ def __init__(self, api):
+ e = api._lib.ERR_get_error()
+ if e == 0:
+ raise SystemError(
+ "Tried to create an OpenSSLError when there was None"
+ )
+ msg = api._ffi.new("char[]", 120)
+ api._lib.ERR_error_string(e, msg)
+ super(OpenSSLError, self).__init__(api._ffi.string(msg))
+
class API(object):
"""
OpenSSL API wrapper.
"""
+ def __init__(self):
+ ffi = cffi.FFI()
+ self._populate_ffi(ffi)
+ self._ffi = ffi
+ self._lib = ffi.verify("""
+ #include <openssl/evp.h>
+ """)
+ self._lib.OpenSSL_add_all_algorithms()
+ self._lib.ERR_load_crypto_strings()
+
+ def _populate_ffi(self, ffi):
+ ffi.cdef("""
+ typedef struct {
+ ...;
+ } EVP_CIPHER_CTX;
+ typedef ... EVP_CIPHER;
+ typedef ... ENGINE;
+
+ void OpenSSL_add_all_algorithms();
+ void ERR_load_crypto_strings();
+
+ const EVP_CIPHER *EVP_get_cipherbyname(const char *);
+ int EVP_EncryptInit_ex(EVP_CIPHER_CTX *, const EVP_CIPHER *,
+ ENGINE *, unsigned char *, unsigned char *);
+ int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *, int);
+ int EVP_EncryptUpdate(EVP_CIPHER_CTX *, unsigned char *, int *,
+ unsigned char *, int);
+ int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *, unsigned char *, int *);
+ int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *);
+ EVP_CIPHER *EVP_CIPHER_CTX_cipher(const EVP_CIPHER_CTX *);
+ int EVP_CIPHER_block_size(const EVP_CIPHER *);
+
+ unsigned long ERR_get_error();
+ """)
+
+ def create_block_cipher_context(self, cipher, mode):
+ ctx = self._ffi.new("EVP_CIPHER_CTX *")
+ ctx = self._ffi.gc(ctx, self._lib.EVP_CIPHER_CTX_cleanup)
+ # TODO: compute name using a better algorithm
+ ciphername = "{0}-{1}-{2}".format(
+ cipher.name, cipher.key_size, mode.name
+ )
+ evp_cipher = self._lib.EVP_get_cipherbyname(ciphername.encode("ascii"))
+ if evp_cipher == self._ffi.NULL:
+ raise OpenSSLError(self)
+ # TODO: only use the key and initialization_vector as needed. Sometimes
+ # this needs to be a DecryptInit, when?
+ res = self._lib.EVP_EncryptInit_ex(
+ ctx, evp_cipher, self._ffi.NULL, cipher.key,
+ mode.initialization_vector
+ )
+ if res == 0:
+ raise OpenSSLError(self)
+
+ # We purposely disable padding here as it's handled higher up in the
+ # API.
+ self._lib.EVP_CIPHER_CTX_set_padding(ctx, 0)
+ return ctx
+
+ def update_encrypt_context(self, ctx, plaintext):
+ buf = self._ffi.new("unsigned char[]", len(plaintext))
+ outlen = self._ffi.new("int *")
+ res = self._lib.EVP_EncryptUpdate(
+ ctx, buf, outlen, plaintext, len(plaintext)
+ )
+ if res == 0:
+ raise OpenSSLError(self)
+ return self._ffi.buffer(buf)[:outlen[0]]
+
+ def finalize_encrypt_context(self, ctx):
+ cipher = self._lib.EVP_CIPHER_CTX_cipher(ctx)
+ block_size = self._lib.EVP_CIPHER_block_size(cipher)
+ buf = self._ffi.new("unsigned char[]", block_size)
+ outlen = self._ffi.new("int *")
+ res = self._lib.EVP_EncryptFinal_ex(ctx, buf, outlen)
+ if res == 0:
+ raise OpenSSLError(self)
+ res = self._lib.EVP_CIPHER_CTX_cleanup(ctx)
+ if res == 0:
+ raise OpenSSLError(self)
+ return self._ffi.buffer(buf)[:outlen[0]]
+
api = API()
diff --git a/cryptography/primitives/__init__.py b/cryptography/primitives/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cryptography/primitives/__init__.py
diff --git a/cryptography/primitives/block/__init__.py b/cryptography/primitives/block/__init__.py
new file mode 100644
index 0000000..a0c6d03
--- /dev/null
+++ b/cryptography/primitives/block/__init__.py
@@ -0,0 +1,21 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+from cryptography.primitives.block.base import BlockCipher
+
+
+__all__ = [
+ "BlockCipher",
+]
diff --git a/cryptography/primitives/block/base.py b/cryptography/primitives/block/base.py
new file mode 100644
index 0000000..f4a04b3
--- /dev/null
+++ b/cryptography/primitives/block/base.py
@@ -0,0 +1,64 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+# TODO: which binding is used should be an option somewhere
+from enum import Enum
+
+from cryptography.bindings.openssl import api
+
+
+class _Operation(Enum):
+ encrypt = "encrypt"
+ decrypt = "decrypt"
+
+
+class BlockCipher(object):
+ def __init__(self, cipher, mode):
+ super(BlockCipher, self).__init__()
+ self.cipher = cipher
+ self.mode = mode
+ self._ctx = api.create_block_cipher_context(cipher, mode)
+ self._operation = None
+
+ @property
+ def name(self):
+ return "{0}-{1}-{2}".format(
+ self.cipher.name, self.cipher.key_size, self.mode.name,
+ )
+
+ def encrypt(self, plaintext):
+ if self._ctx is None:
+ raise ValueError("BlockCipher was already finalized")
+
+ if self._operation is None:
+ self._operation = _Operation.encrypt
+ elif self._operation is not _Operation.encrypt:
+ raise ValueError("BlockCipher cannot encrypt when the operation is"
+ " set to %s" % self._operation.name)
+
+ return api.update_encrypt_context(self._ctx, plaintext)
+
+ def finalize(self):
+ if self._ctx is None:
+ raise ValueError("BlockCipher was already finalized")
+
+ if self._operation is _Operation.encrypt:
+ result = api.finalize_encrypt_context(self._ctx)
+ else:
+ raise ValueError("BlockCipher cannot finalize the unknown "
+ "operation %s" % self._operation.name)
+
+ self._ctx = None
+ return result
diff --git a/cryptography/primitives/block/ciphers.py b/cryptography/primitives/block/ciphers.py
new file mode 100644
index 0000000..2e47870
--- /dev/null
+++ b/cryptography/primitives/block/ciphers.py
@@ -0,0 +1,34 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+
+class AES(object):
+ name = "AES"
+ block_size = 128
+ key_sizes = set([128, 192, 256])
+
+ def __init__(self, key):
+ super(AES, self).__init__()
+ self.key = key
+
+ # Verify that the key size matches the expected key size
+ if self.key_size not in self.key_sizes:
+ raise ValueError("Invalid key size (%s) for %s".format(
+ self.key_size, self.name
+ ))
+
+ @property
+ def key_size(self):
+ return len(self.key) * 8
diff --git a/cryptography/primitives/block/modes.py b/cryptography/primitives/block/modes.py
new file mode 100644
index 0000000..de31f08
--- /dev/null
+++ b/cryptography/primitives/block/modes.py
@@ -0,0 +1,22 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+
+class CBC(object):
+ name = "CBC"
+
+ def __init__(self, initialization_vector):
+ super(CBC, self).__init__()
+ self.initialization_vector = initialization_vector
diff --git a/setup.py b/setup.py
index e1cc239..a1305d2 100644
--- a/setup.py
+++ b/setup.py
@@ -10,8 +10,11 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+import sys
+
from setuptools import setup, find_packages
+
about = {}
with open("cryptography/__about__.py") as fp:
exec(fp.read(), about)
@@ -27,6 +30,12 @@
CFFI_DEPENDENCY,
]
+install_requires = [
+ "cffi>=0.6",
+]
+
+if sys.version_info[:2] < (3, 4):
+ install_requires += ["enum34"]
setup(
name=about["__title__"],
diff --git a/tests/primitives/__init__.py b/tests/primitives/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/primitives/__init__.py
diff --git a/tests/primitives/test_block.py b/tests/primitives/test_block.py
new file mode 100644
index 0000000..774409c
--- /dev/null
+++ b/tests/primitives/test_block.py
@@ -0,0 +1,63 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+
+import pretend
+import pytest
+
+from cryptography.primitives.block import BlockCipher, ciphers, modes
+from cryptography.primitives.block.base import _Operation
+
+
+class TestBlockCipher(object):
+ def test_cipher_name(self):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(b"0" * 32)),
+ modes.CBC(binascii.unhexlify(b"0" * 32))
+ )
+ assert cipher.name == "AES-128-CBC"
+
+ def test_use_after_finalize(self):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(b"0" * 32)),
+ modes.CBC(binascii.unhexlify(b"0" * 32))
+ )
+ cipher.encrypt(b"a" * 16)
+ cipher.finalize()
+ with pytest.raises(ValueError):
+ cipher.encrypt(b"b" * 16)
+ with pytest.raises(ValueError):
+ cipher.finalize()
+
+ def test_encrypt_with_invalid_operation(self):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(b"0" * 32)),
+ modes.CBC(binascii.unhexlify(b"0" * 32))
+ )
+ cipher._operation = _Operation.decrypt
+
+ with pytest.raises(ValueError):
+ cipher.encrypt(b"b" * 16)
+
+ def test_finalize_with_invalid_operation(self):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(b"0" * 32)),
+ modes.CBC(binascii.unhexlify(b"0" * 32))
+ )
+ cipher._operation = pretend.stub(name="wat")
+
+ with pytest.raises(ValueError):
+ cipher.finalize()
diff --git a/tests/primitives/test_ciphers.py b/tests/primitives/test_ciphers.py
new file mode 100644
index 0000000..5ee9f22
--- /dev/null
+++ b/tests/primitives/test_ciphers.py
@@ -0,0 +1,35 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+
+import pytest
+
+from cryptography.primitives.block.ciphers import AES
+
+
+class TestAES(object):
+ @pytest.mark.parametrize(("key", "keysize"), [
+ (b"0" * 32, 128),
+ (b"0" * 48, 192),
+ (b"0" * 64, 256),
+ ])
+ def test_key_size(self, key, keysize):
+ cipher = AES(binascii.unhexlify(key))
+ assert cipher.key_size == keysize
+
+ def test_invalid_key_size(self):
+ with pytest.raises(ValueError):
+ AES(binascii.unhexlify(b"0" * 12))
diff --git a/tests/primitives/test_nist.py b/tests/primitives/test_nist.py
new file mode 100644
index 0000000..db5e034
--- /dev/null
+++ b/tests/primitives/test_nist.py
@@ -0,0 +1,199 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Test using the NIST Test Vectors
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+import os
+
+import pytest
+
+from cryptography.primitives.block import BlockCipher, ciphers, modes
+
+from ..utils import load_nist_vectors_from_file
+
+
+def parameterize_kat_encrypt(fname):
+ return pytest.mark.parametrize(("key", "iv", "plaintext", "ciphertext"),
+ load_nist_vectors_from_file(
+ os.path.join("AES/KAT/", fname),
+ "ENCRYPT",
+ ["key", "iv", "plaintext", "ciphertext"],
+ ),
+ )
+
+
+def paramterize_mmt_encrypt(fname):
+ return pytest.mark.parametrize(("key", "iv", "plaintext", "ciphertext"),
+ load_nist_vectors_from_file(
+ os.path.join("AES/MMT/", fname),
+ "ENCRYPT",
+ ["key", "iv", "plaintext", "ciphertext"],
+ )
+ )
+
+
+class TestAES_CBC(object):
+ @parameterize_kat_encrypt("CBCGFSbox128.rsp")
+ def test_KAT_GFSbox_128_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCGFSbox192.rsp")
+ def test_KAT_GFSbox_192_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCGFSbox256.rsp")
+ def test_KAT_GFSbox_256_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCKeySbox128.rsp")
+ def test_KAT_KeySbox_128_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCKeySbox192.rsp")
+ def test_KAT_KeySbox_192_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCKeySbox256.rsp")
+ def test_KAT_KeySbox_256_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarKey128.rsp")
+ def test_KAT_VarKey_128_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarKey192.rsp")
+ def test_KAT_VarKey_192_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarKey256.rsp")
+ def test_KAT_VarKey_256_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarTxt128.rsp")
+ def test_KAT_VarTxt_128_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarTxt192.rsp")
+ def test_KAT_VarTxt_192_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @parameterize_kat_encrypt("CBCVarTxt256.rsp")
+ def test_KAT_VarTxt_256_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @paramterize_mmt_encrypt("CBCMMT128.rsp")
+ def test_MMT_128_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @paramterize_mmt_encrypt("CBCMMT192.rsp")
+ def test_MMT_192_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
+
+ @paramterize_mmt_encrypt("CBCMMT256.rsp")
+ def test_MMT_256_encrypt(self, key, iv, plaintext, ciphertext):
+ cipher = BlockCipher(
+ ciphers.AES(binascii.unhexlify(key)),
+ modes.CBC(binascii.unhexlify(iv)),
+ )
+ actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext))
+ actual_ciphertext += cipher.finalize()
+ assert binascii.hexlify(actual_ciphertext) == ciphertext
diff --git a/tox.ini b/tox.ini
index 2f4c263..4d17ebe 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,9 @@
envlist = py26,py27,pypy,py32,py33,docs,pep8
[testenv]
-deps = pytest-cov
+deps =
+ pytest-cov
+ pretend
commands = py.test --cov=cryptography/ --cov=tests/
[testenv:docs]