mirror of
https://github.com/saymrwulf/cryptography.git
synced 2026-05-14 20:37:55 +00:00
Merge branch 'master' into fernet
Conflicts: docs/index.rst
This commit is contained in:
commit
5bae063ff2
41 changed files with 434 additions and 275 deletions
|
|
@ -5,8 +5,24 @@ set -x
|
|||
|
||||
if [[ "${OPENSSL}" == "0.9.8" ]]; then
|
||||
sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ lucid main"
|
||||
sudo apt-get -y update
|
||||
fi
|
||||
|
||||
if [[ "${TOX_ENV}" == "pypy" ]]; then
|
||||
sudo add-apt-repository -y ppa:pypy/ppa
|
||||
fi
|
||||
|
||||
sudo apt-get -y update
|
||||
|
||||
if [[ "${OPENSSL}" == "0.9.8" ]]; then
|
||||
sudo apt-get install -y --force-yes libssl-dev/lucid
|
||||
fi
|
||||
|
||||
if [[ "${TOX_ENV}" == "pypy" ]]; then
|
||||
sudo apt-get install -y pypy
|
||||
|
||||
# This is required because we need to get rid of the Travis installed PyPy
|
||||
# or it'll take precedence over the PPA installed one.
|
||||
sudo rm -rf /usr/local/pypy/bin
|
||||
fi
|
||||
|
||||
pip install tox coveralls
|
||||
|
|
|
|||
|
|
@ -30,4 +30,4 @@ __author__ = ("Alex Gaynor, Hynek Schlawack, Donald Stufft, "
|
|||
__email__ = "cryptography-dev@python.org"
|
||||
|
||||
__license__ = "Apache License, Version 2.0"
|
||||
__copyright__ = "Copyright 2013 %s" % __author__
|
||||
__copyright__ = "Copyright 2013-2014 %s" % __author__
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import cffi
|
||||
|
||||
from cryptography import utils
|
||||
from cryptography.exceptions import UnsupportedAlgorithm, InvalidTag
|
||||
|
|
@ -30,24 +27,7 @@ from cryptography.hazmat.primitives.ciphers.algorithms import (
|
|||
from cryptography.hazmat.primitives.ciphers.modes import (
|
||||
CBC, CTR, ECB, OFB, CFB, GCM,
|
||||
)
|
||||
|
||||
_OSX_PRE_INCLUDE = """
|
||||
#ifdef __APPLE__
|
||||
#include <AvailabilityMacros.h>
|
||||
#define __ORIG_DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER \
|
||||
DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#undef DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
"""
|
||||
|
||||
_OSX_POST_INCLUDE = """
|
||||
#ifdef __APPLE__
|
||||
#undef DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER \
|
||||
__ORIG_DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
"""
|
||||
from cryptography.hazmat.bindings.openssl.binding import Binding
|
||||
|
||||
|
||||
@utils.register_interface(CipherBackend)
|
||||
|
|
@ -55,135 +35,34 @@ _OSX_POST_INCLUDE = """
|
|||
@utils.register_interface(HMACBackend)
|
||||
class Backend(object):
|
||||
"""
|
||||
OpenSSL API wrapper.
|
||||
|
||||
Modules listed in the ``_modules`` listed should have the following
|
||||
attributes:
|
||||
|
||||
* ``INCLUDES``: A string containg C includes.
|
||||
* ``TYPES``: A string containing C declarations for types.
|
||||
* ``FUNCTIONS``: A string containing C declarations for functions.
|
||||
* ``MACROS``: A string containing C declarations for any macros.
|
||||
* ``CUSTOMIZATIONS``: A string containing arbitrary top-level C code, this
|
||||
can be used to do things like test for a define and provide an
|
||||
alternate implementation based on that.
|
||||
* ``CONDITIONAL_NAMES``: A dict mapping strings of condition names from the
|
||||
library to a list of names which will not be present without the
|
||||
condition.
|
||||
OpenSSL API binding interfaces.
|
||||
"""
|
||||
_module_prefix = "cryptography.hazmat.backends.openssl."
|
||||
_modules = [
|
||||
"asn1",
|
||||
"bignum",
|
||||
"bio",
|
||||
"conf",
|
||||
"crypto",
|
||||
"dh",
|
||||
"dsa",
|
||||
"engine",
|
||||
"err",
|
||||
"evp",
|
||||
"hmac",
|
||||
"nid",
|
||||
"objects",
|
||||
"opensslv",
|
||||
"pem",
|
||||
"pkcs7",
|
||||
"pkcs12",
|
||||
"rand",
|
||||
"rsa",
|
||||
"ssl",
|
||||
"x509",
|
||||
"x509name",
|
||||
"x509v3",
|
||||
]
|
||||
|
||||
ffi = None
|
||||
lib = None
|
||||
|
||||
def __init__(self):
|
||||
self._ensure_ffi_initialized()
|
||||
self._binding = Binding()
|
||||
self._ffi = self._binding.ffi
|
||||
self._lib = self._binding.lib
|
||||
|
||||
self._lib.OpenSSL_add_all_algorithms()
|
||||
self._lib.SSL_load_error_strings()
|
||||
|
||||
self._cipher_registry = {}
|
||||
self._register_default_ciphers()
|
||||
|
||||
@classmethod
|
||||
def _ensure_ffi_initialized(cls):
|
||||
if cls.ffi is not None and cls.lib is not None:
|
||||
return
|
||||
|
||||
ffi = cffi.FFI()
|
||||
includes = []
|
||||
functions = []
|
||||
macros = []
|
||||
customizations = []
|
||||
for name in cls._modules:
|
||||
module_name = cls._module_prefix + name
|
||||
__import__(module_name)
|
||||
module = sys.modules[module_name]
|
||||
|
||||
ffi.cdef(module.TYPES)
|
||||
|
||||
macros.append(module.MACROS)
|
||||
functions.append(module.FUNCTIONS)
|
||||
includes.append(module.INCLUDES)
|
||||
customizations.append(module.CUSTOMIZATIONS)
|
||||
|
||||
# loop over the functions & macros after declaring all the types
|
||||
# so we can set interdependent types in different files and still
|
||||
# have them all defined before we parse the funcs & macros
|
||||
for func in functions:
|
||||
ffi.cdef(func)
|
||||
for macro in macros:
|
||||
ffi.cdef(macro)
|
||||
|
||||
# We include functions here so that if we got any of their definitions
|
||||
# wrong, the underlying C compiler will explode. In C you are allowed
|
||||
# to re-declare a function if it has the same signature. That is:
|
||||
# int foo(int);
|
||||
# int foo(int);
|
||||
# is legal, but the following will fail to compile:
|
||||
# int foo(int);
|
||||
# int foo(short);
|
||||
|
||||
lib = ffi.verify(
|
||||
source="\n".join(
|
||||
[_OSX_PRE_INCLUDE] +
|
||||
includes +
|
||||
[_OSX_POST_INCLUDE] +
|
||||
functions +
|
||||
customizations
|
||||
),
|
||||
libraries=["crypto", "ssl"],
|
||||
)
|
||||
|
||||
for name in cls._modules:
|
||||
module_name = cls._module_prefix + name
|
||||
module = sys.modules[module_name]
|
||||
for condition, names in module.CONDITIONAL_NAMES.items():
|
||||
if not getattr(lib, condition):
|
||||
for name in names:
|
||||
delattr(lib, name)
|
||||
|
||||
cls.ffi = ffi
|
||||
cls.lib = lib
|
||||
cls.lib.OpenSSL_add_all_algorithms()
|
||||
cls.lib.SSL_load_error_strings()
|
||||
|
||||
def openssl_version_text(self):
|
||||
"""
|
||||
Friendly string name of linked OpenSSL.
|
||||
|
||||
Example: OpenSSL 1.0.1e 11 Feb 2013
|
||||
"""
|
||||
return self.ffi.string(self.lib.OPENSSL_VERSION_TEXT).decode("ascii")
|
||||
return self._ffi.string(self._lib.OPENSSL_VERSION_TEXT).decode("ascii")
|
||||
|
||||
def create_hmac_ctx(self, key, algorithm):
|
||||
return _HMACContext(self, key, algorithm)
|
||||
|
||||
def hash_supported(self, algorithm):
|
||||
digest = self.lib.EVP_get_digestbyname(algorithm.name.encode("ascii"))
|
||||
return digest != self.ffi.NULL
|
||||
digest = self._lib.EVP_get_digestbyname(algorithm.name.encode("ascii"))
|
||||
return digest != self._ffi.NULL
|
||||
|
||||
def hmac_supported(self, algorithm):
|
||||
return self.hash_supported(algorithm)
|
||||
|
|
@ -197,7 +76,7 @@ class Backend(object):
|
|||
except KeyError:
|
||||
return False
|
||||
evp_cipher = adapter(self, cipher, mode)
|
||||
return self.ffi.NULL != evp_cipher
|
||||
return self._ffi.NULL != evp_cipher
|
||||
|
||||
def register_cipher_adapter(self, cipher_cls, mode_cls, adapter):
|
||||
if (cipher_cls, mode_cls) in self._cipher_registry:
|
||||
|
|
@ -251,25 +130,25 @@ class Backend(object):
|
|||
return _CipherContext(self, cipher, mode, _CipherContext._DECRYPT)
|
||||
|
||||
def _handle_error(self, mode):
|
||||
code = self.lib.ERR_get_error()
|
||||
code = self._lib.ERR_get_error()
|
||||
if not code and isinstance(mode, GCM):
|
||||
raise InvalidTag
|
||||
assert code != 0
|
||||
lib = self.lib.ERR_GET_LIB(code)
|
||||
func = self.lib.ERR_GET_FUNC(code)
|
||||
reason = self.lib.ERR_GET_REASON(code)
|
||||
lib = self._lib.ERR_GET_LIB(code)
|
||||
func = self._lib.ERR_GET_FUNC(code)
|
||||
reason = self._lib.ERR_GET_REASON(code)
|
||||
return self._handle_error_code(lib, func, reason)
|
||||
|
||||
def _handle_error_code(self, lib, func, reason):
|
||||
if lib == self.lib.ERR_LIB_EVP:
|
||||
if func == self.lib.EVP_F_EVP_ENCRYPTFINAL_EX:
|
||||
if reason == self.lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH:
|
||||
if lib == self._lib.ERR_LIB_EVP:
|
||||
if func == self._lib.EVP_F_EVP_ENCRYPTFINAL_EX:
|
||||
if reason == self._lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH:
|
||||
raise ValueError(
|
||||
"The length of the provided data is not a multiple of "
|
||||
"the block length"
|
||||
)
|
||||
elif func == self.lib.EVP_F_EVP_DECRYPTFINAL_EX:
|
||||
if reason == self.lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH:
|
||||
elif func == self._lib.EVP_F_EVP_DECRYPTFINAL_EX:
|
||||
if reason == self._lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH:
|
||||
raise ValueError(
|
||||
"The length of the provided data is not a multiple of "
|
||||
"the block length"
|
||||
|
|
@ -286,7 +165,7 @@ class GetCipherByName(object):
|
|||
|
||||
def __call__(self, backend, cipher, mode):
|
||||
cipher_name = self._fmt.format(cipher=cipher, mode=mode).lower()
|
||||
return backend.lib.EVP_get_cipherbyname(cipher_name.encode("ascii"))
|
||||
return backend._lib.EVP_get_cipherbyname(cipher_name.encode("ascii"))
|
||||
|
||||
|
||||
@utils.register_interface(interfaces.CipherContext)
|
||||
|
|
@ -308,8 +187,10 @@ class _CipherContext(object):
|
|||
else:
|
||||
self._block_size = 1
|
||||
|
||||
ctx = self._backend.lib.EVP_CIPHER_CTX_new()
|
||||
ctx = self._backend.ffi.gc(ctx, self._backend.lib.EVP_CIPHER_CTX_free)
|
||||
ctx = self._backend._lib.EVP_CIPHER_CTX_new()
|
||||
ctx = self._backend._ffi.gc(
|
||||
ctx, self._backend._lib.EVP_CIPHER_CTX_free
|
||||
)
|
||||
|
||||
registry = self._backend._cipher_registry
|
||||
try:
|
||||
|
|
@ -322,7 +203,7 @@ class _CipherContext(object):
|
|||
)
|
||||
|
||||
evp_cipher = adapter(self._backend, cipher, mode)
|
||||
if evp_cipher == self._backend.ffi.NULL:
|
||||
if evp_cipher == self._backend._ffi.NULL:
|
||||
raise UnsupportedAlgorithm(
|
||||
"cipher {0} in {1} mode is not supported "
|
||||
"by this backend".format(
|
||||
|
|
@ -334,86 +215,84 @@ class _CipherContext(object):
|
|||
elif isinstance(mode, interfaces.ModeWithNonce):
|
||||
iv_nonce = mode.nonce
|
||||
else:
|
||||
iv_nonce = self._backend.ffi.NULL
|
||||
iv_nonce = self._backend._ffi.NULL
|
||||
# begin init with cipher and operation type
|
||||
res = self._backend.lib.EVP_CipherInit_ex(ctx, evp_cipher,
|
||||
self._backend.ffi.NULL,
|
||||
self._backend.ffi.NULL,
|
||||
self._backend.ffi.NULL,
|
||||
operation)
|
||||
res = self._backend._lib.EVP_CipherInit_ex(ctx, evp_cipher,
|
||||
self._backend._ffi.NULL,
|
||||
self._backend._ffi.NULL,
|
||||
self._backend._ffi.NULL,
|
||||
operation)
|
||||
assert res != 0
|
||||
# set the key length to handle variable key ciphers
|
||||
res = self._backend.lib.EVP_CIPHER_CTX_set_key_length(
|
||||
res = self._backend._lib.EVP_CIPHER_CTX_set_key_length(
|
||||
ctx, len(cipher.key)
|
||||
)
|
||||
assert res != 0
|
||||
if isinstance(mode, GCM):
|
||||
res = self._backend.lib.EVP_CIPHER_CTX_ctrl(
|
||||
ctx, self._backend.lib.EVP_CTRL_GCM_SET_IVLEN,
|
||||
len(iv_nonce), self._backend.ffi.NULL
|
||||
res = self._backend._lib.EVP_CIPHER_CTX_ctrl(
|
||||
ctx, self._backend._lib.EVP_CTRL_GCM_SET_IVLEN,
|
||||
len(iv_nonce), self._backend._ffi.NULL
|
||||
)
|
||||
assert res != 0
|
||||
if operation == self._DECRYPT:
|
||||
if not mode.tag or len(mode.tag) < 4:
|
||||
raise ValueError("Authentication tag must be provided and "
|
||||
"be 4 bytes or longer when decrypting")
|
||||
res = self._backend.lib.EVP_CIPHER_CTX_ctrl(
|
||||
ctx, self._backend.lib.EVP_CTRL_GCM_SET_TAG,
|
||||
res = self._backend._lib.EVP_CIPHER_CTX_ctrl(
|
||||
ctx, self._backend._lib.EVP_CTRL_GCM_SET_TAG,
|
||||
len(mode.tag), mode.tag
|
||||
)
|
||||
assert res != 0
|
||||
else:
|
||||
if mode.tag:
|
||||
raise ValueError("Authentication tag must be None when "
|
||||
"encrypting")
|
||||
|
||||
# pass key/iv
|
||||
res = self._backend.lib.EVP_CipherInit_ex(ctx, self._backend.ffi.NULL,
|
||||
self._backend.ffi.NULL,
|
||||
cipher.key,
|
||||
iv_nonce,
|
||||
operation)
|
||||
res = self._backend._lib.EVP_CipherInit_ex(
|
||||
ctx,
|
||||
self._backend._ffi.NULL,
|
||||
self._backend._ffi.NULL,
|
||||
cipher.key,
|
||||
iv_nonce,
|
||||
operation
|
||||
)
|
||||
assert res != 0
|
||||
# We purposely disable padding here as it's handled higher up in the
|
||||
# API.
|
||||
self._backend.lib.EVP_CIPHER_CTX_set_padding(ctx, 0)
|
||||
self._backend._lib.EVP_CIPHER_CTX_set_padding(ctx, 0)
|
||||
self._ctx = ctx
|
||||
|
||||
def update(self, data):
|
||||
buf = self._backend.ffi.new("unsigned char[]",
|
||||
len(data) + self._block_size - 1)
|
||||
outlen = self._backend.ffi.new("int *")
|
||||
res = self._backend.lib.EVP_CipherUpdate(self._ctx, buf, outlen, data,
|
||||
len(data))
|
||||
buf = self._backend._ffi.new("unsigned char[]",
|
||||
len(data) + self._block_size - 1)
|
||||
outlen = self._backend._ffi.new("int *")
|
||||
res = self._backend._lib.EVP_CipherUpdate(self._ctx, buf, outlen, data,
|
||||
len(data))
|
||||
assert res != 0
|
||||
return self._backend.ffi.buffer(buf)[:outlen[0]]
|
||||
return self._backend._ffi.buffer(buf)[:outlen[0]]
|
||||
|
||||
def finalize(self):
|
||||
buf = self._backend.ffi.new("unsigned char[]", self._block_size)
|
||||
outlen = self._backend.ffi.new("int *")
|
||||
res = self._backend.lib.EVP_CipherFinal_ex(self._ctx, buf, outlen)
|
||||
buf = self._backend._ffi.new("unsigned char[]", self._block_size)
|
||||
outlen = self._backend._ffi.new("int *")
|
||||
res = self._backend._lib.EVP_CipherFinal_ex(self._ctx, buf, outlen)
|
||||
if res == 0:
|
||||
self._backend._handle_error(self._mode)
|
||||
|
||||
if (isinstance(self._mode, GCM) and
|
||||
self._operation == self._ENCRYPT):
|
||||
block_byte_size = self._block_size // 8
|
||||
tag_buf = self._backend.ffi.new("unsigned char[]", block_byte_size)
|
||||
res = self._backend.lib.EVP_CIPHER_CTX_ctrl(
|
||||
self._ctx, self._backend.lib.EVP_CTRL_GCM_GET_TAG,
|
||||
tag_buf = self._backend._ffi.new(
|
||||
"unsigned char[]", block_byte_size
|
||||
)
|
||||
res = self._backend._lib.EVP_CIPHER_CTX_ctrl(
|
||||
self._ctx, self._backend._lib.EVP_CTRL_GCM_GET_TAG,
|
||||
block_byte_size, tag_buf
|
||||
)
|
||||
assert res != 0
|
||||
self._tag = self._backend.ffi.buffer(tag_buf)[:]
|
||||
self._tag = self._backend._ffi.buffer(tag_buf)[:]
|
||||
|
||||
res = self._backend.lib.EVP_CIPHER_CTX_cleanup(self._ctx)
|
||||
res = self._backend._lib.EVP_CIPHER_CTX_cleanup(self._ctx)
|
||||
assert res == 1
|
||||
return self._backend.ffi.buffer(buf)[:outlen[0]]
|
||||
return self._backend._ffi.buffer(buf)[:outlen[0]]
|
||||
|
||||
def authenticate_additional_data(self, data):
|
||||
outlen = self._backend.ffi.new("int *")
|
||||
res = self._backend.lib.EVP_CipherUpdate(
|
||||
self._ctx, self._backend.ffi.NULL, outlen, data, len(data)
|
||||
outlen = self._backend._ffi.new("int *")
|
||||
res = self._backend._lib.EVP_CipherUpdate(
|
||||
self._ctx, self._backend._ffi.NULL, outlen, data, len(data)
|
||||
)
|
||||
assert res != 0
|
||||
|
||||
|
|
@ -430,43 +309,44 @@ class _HashContext(object):
|
|||
self._backend = backend
|
||||
|
||||
if ctx is None:
|
||||
ctx = self._backend.lib.EVP_MD_CTX_create()
|
||||
ctx = self._backend.ffi.gc(ctx,
|
||||
self._backend.lib.EVP_MD_CTX_destroy)
|
||||
evp_md = self._backend.lib.EVP_get_digestbyname(
|
||||
ctx = self._backend._lib.EVP_MD_CTX_create()
|
||||
ctx = self._backend._ffi.gc(ctx,
|
||||
self._backend._lib.EVP_MD_CTX_destroy)
|
||||
evp_md = self._backend._lib.EVP_get_digestbyname(
|
||||
algorithm.name.encode("ascii"))
|
||||
if evp_md == self._backend.ffi.NULL:
|
||||
if evp_md == self._backend._ffi.NULL:
|
||||
raise UnsupportedAlgorithm(
|
||||
"{0} is not a supported hash on this backend".format(
|
||||
algorithm.name)
|
||||
)
|
||||
res = self._backend.lib.EVP_DigestInit_ex(ctx, evp_md,
|
||||
self._backend.ffi.NULL)
|
||||
res = self._backend._lib.EVP_DigestInit_ex(ctx, evp_md,
|
||||
self._backend._ffi.NULL)
|
||||
assert res != 0
|
||||
|
||||
self._ctx = ctx
|
||||
|
||||
def copy(self):
|
||||
copied_ctx = self._backend.lib.EVP_MD_CTX_create()
|
||||
copied_ctx = self._backend.ffi.gc(copied_ctx,
|
||||
self._backend.lib.EVP_MD_CTX_destroy)
|
||||
res = self._backend.lib.EVP_MD_CTX_copy_ex(copied_ctx, self._ctx)
|
||||
copied_ctx = self._backend._lib.EVP_MD_CTX_create()
|
||||
copied_ctx = self._backend._ffi.gc(
|
||||
copied_ctx, self._backend._lib.EVP_MD_CTX_destroy
|
||||
)
|
||||
res = self._backend._lib.EVP_MD_CTX_copy_ex(copied_ctx, self._ctx)
|
||||
assert res != 0
|
||||
return _HashContext(self._backend, self.algorithm, ctx=copied_ctx)
|
||||
|
||||
def update(self, data):
|
||||
res = self._backend.lib.EVP_DigestUpdate(self._ctx, data, len(data))
|
||||
res = self._backend._lib.EVP_DigestUpdate(self._ctx, data, len(data))
|
||||
assert res != 0
|
||||
|
||||
def finalize(self):
|
||||
buf = self._backend.ffi.new("unsigned char[]",
|
||||
self.algorithm.digest_size)
|
||||
res = self._backend.lib.EVP_DigestFinal_ex(self._ctx, buf,
|
||||
self._backend.ffi.NULL)
|
||||
buf = self._backend._ffi.new("unsigned char[]",
|
||||
self.algorithm.digest_size)
|
||||
res = self._backend._lib.EVP_DigestFinal_ex(self._ctx, buf,
|
||||
self._backend._ffi.NULL)
|
||||
assert res != 0
|
||||
res = self._backend.lib.EVP_MD_CTX_cleanup(self._ctx)
|
||||
res = self._backend._lib.EVP_MD_CTX_cleanup(self._ctx)
|
||||
assert res == 1
|
||||
return self._backend.ffi.buffer(buf)[:]
|
||||
return self._backend._ffi.buffer(buf)[:]
|
||||
|
||||
|
||||
@utils.register_interface(interfaces.HashContext)
|
||||
|
|
@ -476,18 +356,20 @@ class _HMACContext(object):
|
|||
self._backend = backend
|
||||
|
||||
if ctx is None:
|
||||
ctx = self._backend.ffi.new("HMAC_CTX *")
|
||||
self._backend.lib.HMAC_CTX_init(ctx)
|
||||
ctx = self._backend.ffi.gc(ctx, self._backend.lib.HMAC_CTX_cleanup)
|
||||
evp_md = self._backend.lib.EVP_get_digestbyname(
|
||||
ctx = self._backend._ffi.new("HMAC_CTX *")
|
||||
self._backend._lib.HMAC_CTX_init(ctx)
|
||||
ctx = self._backend._ffi.gc(
|
||||
ctx, self._backend._lib.HMAC_CTX_cleanup
|
||||
)
|
||||
evp_md = self._backend._lib.EVP_get_digestbyname(
|
||||
algorithm.name.encode('ascii'))
|
||||
if evp_md == self._backend.ffi.NULL:
|
||||
if evp_md == self._backend._ffi.NULL:
|
||||
raise UnsupportedAlgorithm(
|
||||
"{0} is not a supported hash on this backend".format(
|
||||
algorithm.name)
|
||||
)
|
||||
res = self._backend.lib.Cryptography_HMAC_Init_ex(
|
||||
ctx, key, len(key), evp_md, self._backend.ffi.NULL
|
||||
res = self._backend._lib.Cryptography_HMAC_Init_ex(
|
||||
ctx, key, len(key), evp_md, self._backend._ffi.NULL
|
||||
)
|
||||
assert res != 0
|
||||
|
||||
|
|
@ -495,12 +377,12 @@ class _HMACContext(object):
|
|||
self._key = key
|
||||
|
||||
def copy(self):
|
||||
copied_ctx = self._backend.ffi.new("HMAC_CTX *")
|
||||
self._backend.lib.HMAC_CTX_init(copied_ctx)
|
||||
copied_ctx = self._backend.ffi.gc(
|
||||
copied_ctx, self._backend.lib.HMAC_CTX_cleanup
|
||||
copied_ctx = self._backend._ffi.new("HMAC_CTX *")
|
||||
self._backend._lib.HMAC_CTX_init(copied_ctx)
|
||||
copied_ctx = self._backend._ffi.gc(
|
||||
copied_ctx, self._backend._lib.HMAC_CTX_cleanup
|
||||
)
|
||||
res = self._backend.lib.Cryptography_HMAC_CTX_copy(
|
||||
res = self._backend._lib.Cryptography_HMAC_CTX_copy(
|
||||
copied_ctx, self._ctx
|
||||
)
|
||||
assert res != 0
|
||||
|
|
@ -509,20 +391,22 @@ class _HMACContext(object):
|
|||
)
|
||||
|
||||
def update(self, data):
|
||||
res = self._backend.lib.Cryptography_HMAC_Update(
|
||||
res = self._backend._lib.Cryptography_HMAC_Update(
|
||||
self._ctx, data, len(data)
|
||||
)
|
||||
assert res != 0
|
||||
|
||||
def finalize(self):
|
||||
buf = self._backend.ffi.new("unsigned char[]",
|
||||
self.algorithm.digest_size)
|
||||
buflen = self._backend.ffi.new("unsigned int *",
|
||||
self.algorithm.digest_size)
|
||||
res = self._backend.lib.Cryptography_HMAC_Final(self._ctx, buf, buflen)
|
||||
buf = self._backend._ffi.new("unsigned char[]",
|
||||
self.algorithm.digest_size)
|
||||
buflen = self._backend._ffi.new("unsigned int *",
|
||||
self.algorithm.digest_size)
|
||||
res = self._backend._lib.Cryptography_HMAC_Final(
|
||||
self._ctx, buf, buflen
|
||||
)
|
||||
assert res != 0
|
||||
self._backend.lib.HMAC_CTX_cleanup(self._ctx)
|
||||
return self._backend.ffi.buffer(buf)[:]
|
||||
self._backend._lib.HMAC_CTX_cleanup(self._ctx)
|
||||
return self._backend._ffi.buffer(buf)[:]
|
||||
|
||||
|
||||
backend = Backend()
|
||||
|
|
|
|||
12
cryptography/hazmat/bindings/__init__.py
Normal file
12
cryptography/hazmat/bindings/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 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.
|
||||
12
cryptography/hazmat/bindings/openssl/__init__.py
Normal file
12
cryptography/hazmat/bindings/openssl/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 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.
|
||||
149
cryptography/hazmat/bindings/openssl/binding.py
Normal file
149
cryptography/hazmat/bindings/openssl/binding.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# 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 sys
|
||||
|
||||
import cffi
|
||||
|
||||
_OSX_PRE_INCLUDE = """
|
||||
#ifdef __APPLE__
|
||||
#include <AvailabilityMacros.h>
|
||||
#define __ORIG_DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER \
|
||||
DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#undef DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
"""
|
||||
|
||||
_OSX_POST_INCLUDE = """
|
||||
#ifdef __APPLE__
|
||||
#undef DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER \
|
||||
__ORIG_DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
"""
|
||||
|
||||
|
||||
class Binding(object):
|
||||
"""
|
||||
OpenSSL API wrapper.
|
||||
|
||||
Modules listed in the ``_modules`` listed should have the following
|
||||
attributes:
|
||||
|
||||
* ``INCLUDES``: A string containg C includes.
|
||||
* ``TYPES``: A string containing C declarations for types.
|
||||
* ``FUNCTIONS``: A string containing C declarations for functions.
|
||||
* ``MACROS``: A string containing C declarations for any macros.
|
||||
* ``CUSTOMIZATIONS``: A string containing arbitrary top-level C code, this
|
||||
can be used to do things like test for a define and provide an
|
||||
alternate implementation based on that.
|
||||
* ``CONDITIONAL_NAMES``: A dict mapping strings of condition names from the
|
||||
library to a list of names which will not be present without the
|
||||
condition.
|
||||
"""
|
||||
_module_prefix = "cryptography.hazmat.bindings.openssl."
|
||||
_modules = [
|
||||
"asn1",
|
||||
"bignum",
|
||||
"bio",
|
||||
"conf",
|
||||
"crypto",
|
||||
"dh",
|
||||
"dsa",
|
||||
"engine",
|
||||
"err",
|
||||
"evp",
|
||||
"hmac",
|
||||
"nid",
|
||||
"objects",
|
||||
"opensslv",
|
||||
"pem",
|
||||
"pkcs7",
|
||||
"pkcs12",
|
||||
"rand",
|
||||
"rsa",
|
||||
"ssl",
|
||||
"x509",
|
||||
"x509name",
|
||||
"x509v3",
|
||||
]
|
||||
|
||||
ffi = None
|
||||
lib = None
|
||||
|
||||
def __init__(self):
|
||||
self._ensure_ffi_initialized()
|
||||
|
||||
@classmethod
|
||||
def _ensure_ffi_initialized(cls):
|
||||
if cls.ffi is not None and cls.lib is not None:
|
||||
return
|
||||
|
||||
ffi = cffi.FFI()
|
||||
includes = []
|
||||
functions = []
|
||||
macros = []
|
||||
customizations = []
|
||||
for name in cls._modules:
|
||||
module_name = cls._module_prefix + name
|
||||
__import__(module_name)
|
||||
module = sys.modules[module_name]
|
||||
|
||||
ffi.cdef(module.TYPES)
|
||||
|
||||
macros.append(module.MACROS)
|
||||
functions.append(module.FUNCTIONS)
|
||||
includes.append(module.INCLUDES)
|
||||
customizations.append(module.CUSTOMIZATIONS)
|
||||
|
||||
# loop over the functions & macros after declaring all the types
|
||||
# so we can set interdependent types in different files and still
|
||||
# have them all defined before we parse the funcs & macros
|
||||
for func in functions:
|
||||
ffi.cdef(func)
|
||||
for macro in macros:
|
||||
ffi.cdef(macro)
|
||||
|
||||
# We include functions here so that if we got any of their definitions
|
||||
# wrong, the underlying C compiler will explode. In C you are allowed
|
||||
# to re-declare a function if it has the same signature. That is:
|
||||
# int foo(int);
|
||||
# int foo(int);
|
||||
# is legal, but the following will fail to compile:
|
||||
# int foo(int);
|
||||
# int foo(short);
|
||||
|
||||
lib = ffi.verify(
|
||||
source="\n".join(
|
||||
[_OSX_PRE_INCLUDE] +
|
||||
includes +
|
||||
[_OSX_POST_INCLUDE] +
|
||||
functions +
|
||||
customizations
|
||||
),
|
||||
libraries=["crypto", "ssl"],
|
||||
)
|
||||
|
||||
for name in cls._modules:
|
||||
module_name = cls._module_prefix + name
|
||||
module = sys.modules[module_name]
|
||||
for condition, names in module.CONDITIONAL_NAMES.items():
|
||||
if not getattr(lib, condition):
|
||||
for name in names:
|
||||
delattr(lib, name)
|
||||
|
||||
cls.ffi = ffi
|
||||
cls.lib = lib
|
||||
|
|
@ -36,6 +36,11 @@ int CRYPTO_mem_ctrl(int);
|
|||
int CRYPTO_is_mem_check_on(void);
|
||||
void CRYPTO_mem_leaks(struct bio_st *);
|
||||
void CRYPTO_cleanup_all_ex_data(void);
|
||||
int CRYPTO_num_locks(void);
|
||||
void CRYPTO_set_locking_callback(void(*)(int, int, const char *, int));
|
||||
void CRYPTO_set_id_callback(unsigned long (*)(void));
|
||||
unsigned long (*CRYPTO_get_id_callback(void))(void);
|
||||
void (*CRYPTO_get_locking_callback(void))(int, int, const char *, int);
|
||||
|
||||
void OPENSSL_free(void *);
|
||||
"""
|
||||
|
|
@ -30,16 +30,26 @@ class Cipher(object):
|
|||
self._backend = backend
|
||||
|
||||
def encryptor(self):
|
||||
if isinstance(self.mode, interfaces.ModeWithAuthenticationTag):
|
||||
if self.mode.tag is not None:
|
||||
raise ValueError(
|
||||
"Authentication tag must be None when encrypting"
|
||||
)
|
||||
ctx = self._backend.create_symmetric_encryption_ctx(
|
||||
self.algorithm, self.mode
|
||||
)
|
||||
return self._wrap_ctx(ctx, True)
|
||||
return self._wrap_ctx(ctx, encrypt=True)
|
||||
|
||||
def decryptor(self):
|
||||
if isinstance(self.mode, interfaces.ModeWithAuthenticationTag):
|
||||
if self.mode.tag is None:
|
||||
raise ValueError(
|
||||
"Authentication tag must be provided when decrypting"
|
||||
)
|
||||
ctx = self._backend.create_symmetric_decryption_ctx(
|
||||
self.algorithm, self.mode
|
||||
)
|
||||
return self._wrap_ctx(ctx, False)
|
||||
return self._wrap_ctx(ctx, encrypt=False)
|
||||
|
||||
def _wrap_ctx(self, ctx, encrypt):
|
||||
if isinstance(self.mode, interfaces.ModeWithAuthenticationTag):
|
||||
|
|
|
|||
|
|
@ -65,5 +65,10 @@ class GCM(object):
|
|||
name = "GCM"
|
||||
|
||||
def __init__(self, initialization_vector, tag=None):
|
||||
if tag is not None and len(tag) < 4:
|
||||
raise ValueError(
|
||||
"Authentication tag must be 4 bytes or longer"
|
||||
)
|
||||
|
||||
self.initialization_vector = initialization_vector
|
||||
self.tag = tag
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
Architecture
|
||||
============
|
||||
|
||||
``cryptography`` has three different layers:
|
||||
|
||||
* ``cryptography``: This package contains higher level recipes, for example
|
||||
"encrypt and then MAC". This is implemented on top of
|
||||
``cryptography.hazmat.primitives``.
|
||||
* ``cryptography.hazmat.primitives``: This packages contains low level
|
||||
algorithms, things like ``AES`` or ``SHA1``. This is implemented on top of
|
||||
``cryptography.hazmat.backends``.
|
||||
* ``cryptography.hazmat.backends``: This package contains bindings to low level
|
||||
cryptographic libraries. Our initial target is OpenSSL.
|
||||
|
|
@ -54,7 +54,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = 'Cryptography'
|
||||
copyright = '2013, Individual Contributors'
|
||||
copyright = '2013-2014, Individual Contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
|
|||
|
|
@ -3,23 +3,37 @@
|
|||
OpenSSL Backend
|
||||
===============
|
||||
|
||||
These are `CFFI`_ bindings to the `OpenSSL`_ C library.
|
||||
The `OpenSSL`_ C library.
|
||||
|
||||
.. data:: cryptography.hazmat.backends.openssl.backend
|
||||
|
||||
This is the exposed API for the OpenSSL bindings. It has two public
|
||||
attributes:
|
||||
This is the exposed API for the OpenSSL backend. It has no public attributes.
|
||||
|
||||
.. attribute:: ffi
|
||||
Using your own OpenSSL on Linux
|
||||
-------------------------------
|
||||
|
||||
This is a :class:`cffi.FFI` instance. It can be used to allocate and
|
||||
otherwise manipulate OpenSSL structures.
|
||||
Python links to OpenSSL for its own purposes and this can sometimes cause
|
||||
problems when you wish to use a different version of OpenSSL with cryptography.
|
||||
If you want to use cryptography with your own build of OpenSSL you will need to
|
||||
make sure that the build is configured correctly so that your version of
|
||||
OpenSSL doesn't conflict with Python's.
|
||||
|
||||
.. attribute:: lib
|
||||
The options you need to add allow the linker to identify every symbol correctly
|
||||
even when multiple versions of the library are linked into the same program. If
|
||||
you are using your distribution's source packages these will probably be
|
||||
patched in for you already, otherwise you'll need to use options something like
|
||||
this when configuring OpenSSL::
|
||||
|
||||
This is a ``cffi`` library. It can be used to call OpenSSL functions,
|
||||
and access constants.
|
||||
./config -Wl,--version-script=openssl.ld -Wl,-Bsymbolic-functions -fPIC shared
|
||||
|
||||
You'll also need to generate your own ``openssl.ld`` file. For example::
|
||||
|
||||
OPENSSL_1.0.1F_CUSTOM {
|
||||
global:
|
||||
*;
|
||||
};
|
||||
|
||||
You should replace the version string on the first line as appropriate for your
|
||||
build.
|
||||
|
||||
.. _`CFFI`: https://cffi.readthedocs.org/
|
||||
.. _`OpenSSL`: https://www.openssl.org/
|
||||
|
|
|
|||
22
docs/hazmat/bindings/index.rst
Normal file
22
docs/hazmat/bindings/index.rst
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.. hazmat::
|
||||
|
||||
Bindings
|
||||
========
|
||||
|
||||
.. currentmodule:: cryptography.hazmat.bindings
|
||||
|
||||
``cryptography`` aims to provide low-level CFFI based bindings to multiple
|
||||
native C libraries. These provide no automatic initialisation of the library
|
||||
and may not provide complete wrappers for its API.
|
||||
|
||||
Using these functions directly is likely to require you to be careful in
|
||||
managing memory allocation, locking and other resources.
|
||||
|
||||
|
||||
Individual Bindings
|
||||
-------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
openssl
|
||||
27
docs/hazmat/bindings/openssl.rst
Normal file
27
docs/hazmat/bindings/openssl.rst
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.. hazmat::
|
||||
|
||||
OpenSSL Binding
|
||||
===============
|
||||
|
||||
.. currentmodule:: cryptography.hazmat.bindings.openssl.binding
|
||||
|
||||
These are `CFFI`_ bindings to the `OpenSSL`_ C library.
|
||||
|
||||
.. class:: cryptography.hazmat.bindings.openssl.binding.Binding()
|
||||
|
||||
This is the exposed API for the OpenSSL bindings. It has two public
|
||||
attributes:
|
||||
|
||||
.. attribute:: ffi
|
||||
|
||||
This is a :class:`cffi.FFI` instance. It can be used to allocate and
|
||||
otherwise manipulate OpenSSL structures.
|
||||
|
||||
.. attribute:: lib
|
||||
|
||||
This is a ``cffi`` library. It can be used to call OpenSSL functions,
|
||||
and access constants.
|
||||
|
||||
|
||||
.. _`CFFI`: https://cffi.readthedocs.org/
|
||||
.. _`OpenSSL`: https://www.openssl.org/
|
||||
|
|
@ -43,7 +43,9 @@ The other level is low-level cryptographic primitives. These are often
|
|||
dangerous and can be used incorrectly. They require making decisions and having
|
||||
an in-depth knowledge of the cryptographic concepts at work. Because of the
|
||||
potential danger in working at this level, this is referred to as the
|
||||
"hazardous materials" or "hazmat" layer.
|
||||
"hazardous materials" or "hazmat" layer. These live in the
|
||||
``cryptography.hazmat`` package, and their documentation will always contain an
|
||||
admonition at the top.
|
||||
|
||||
We recommend using the recipes layer whenever possible, and falling back to the
|
||||
hazmat layer only when necessary.
|
||||
|
|
@ -55,7 +57,6 @@ The recipes layer
|
|||
:maxdepth: 2
|
||||
|
||||
fernet
|
||||
architecture
|
||||
exceptions
|
||||
glossary
|
||||
|
||||
|
|
@ -67,6 +68,7 @@ The hazardous materials layer
|
|||
|
||||
hazmat/primitives/index
|
||||
hazmat/backends/index
|
||||
hazmat/bindings/index
|
||||
|
||||
The ``cryptography`` open source project
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -58,18 +58,13 @@ class TestOpenSSL(object):
|
|||
with pytest.raises(ValueError):
|
||||
backend.register_cipher_adapter(AES, CBC, None)
|
||||
|
||||
def test_instances_share_ffi(self):
|
||||
b = Backend()
|
||||
assert b.ffi is backend.ffi
|
||||
assert b.lib is backend.lib
|
||||
|
||||
@pytest.mark.parametrize("mode", [DummyMode(), None])
|
||||
def test_nonexistent_cipher(self, mode):
|
||||
b = Backend()
|
||||
b.register_cipher_adapter(
|
||||
DummyCipher,
|
||||
type(mode),
|
||||
lambda backend, cipher, mode: backend.ffi.NULL
|
||||
lambda backend, cipher, mode: backend._ffi.NULL
|
||||
)
|
||||
cipher = Cipher(
|
||||
DummyCipher(), mode, backend=b,
|
||||
|
|
@ -82,18 +77,18 @@ class TestOpenSSL(object):
|
|||
backend._handle_error_code(0, 0, 0)
|
||||
|
||||
with pytest.raises(SystemError):
|
||||
backend._handle_error_code(backend.lib.ERR_LIB_EVP, 0, 0)
|
||||
backend._handle_error_code(backend._lib.ERR_LIB_EVP, 0, 0)
|
||||
|
||||
with pytest.raises(SystemError):
|
||||
backend._handle_error_code(
|
||||
backend.lib.ERR_LIB_EVP,
|
||||
backend.lib.EVP_F_EVP_ENCRYPTFINAL_EX,
|
||||
backend._lib.ERR_LIB_EVP,
|
||||
backend._lib.EVP_F_EVP_ENCRYPTFINAL_EX,
|
||||
0
|
||||
)
|
||||
|
||||
with pytest.raises(SystemError):
|
||||
backend._handle_error_code(
|
||||
backend.lib.ERR_LIB_EVP,
|
||||
backend.lib.EVP_F_EVP_DECRYPTFINAL_EX,
|
||||
backend._lib.ERR_LIB_EVP,
|
||||
backend._lib.EVP_F_EVP_DECRYPTFINAL_EX,
|
||||
0
|
||||
)
|
||||
|
|
|
|||
22
tests/hazmat/bindings/test_openssl.py
Normal file
22
tests/hazmat/bindings/test_openssl.py
Normal file
|
|
@ -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 cryptography.hazmat.bindings.openssl.binding import Binding
|
||||
|
||||
|
||||
class TestOpenSSL(object):
|
||||
def test_binding_loads(self):
|
||||
binding = Binding()
|
||||
assert binding
|
||||
assert binding.lib
|
||||
assert binding.ffi
|
||||
|
|
@ -264,13 +264,10 @@ def aead_tag_exception_test(backend, cipher_factory, mode_factory):
|
|||
)
|
||||
with pytest.raises(ValueError):
|
||||
cipher.decryptor()
|
||||
cipher = Cipher(
|
||||
cipher_factory(binascii.unhexlify(b"0" * 32)),
|
||||
mode_factory(binascii.unhexlify(b"0" * 24), b"000"),
|
||||
backend
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
cipher.decryptor()
|
||||
mode_factory(binascii.unhexlify(b"0" * 24), b"000")
|
||||
|
||||
cipher = Cipher(
|
||||
cipher_factory(binascii.unhexlify(b"0" * 32)),
|
||||
mode_factory(binascii.unhexlify(b"0" * 24), b"0" * 16),
|
||||
|
|
|
|||
2
tox.ini
2
tox.ini
|
|
@ -25,7 +25,7 @@ commands =
|
|||
# Temporarily disable coverage on pypy because of performance problems with
|
||||
# coverage.py on pypy.
|
||||
[testenv:pypy]
|
||||
commands = py.test --capture=no
|
||||
commands = py.test --capture=no --strict
|
||||
|
||||
[testenv:pep8]
|
||||
deps = flake8
|
||||
|
|
|
|||
Loading…
Reference in a new issue