diff options
56 files changed, 2145 insertions, 110 deletions
diff --git a/.coveragerc b/.coveragerc index 20e3224e..03fc621e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,5 +3,6 @@ branch = True [report] exclude_lines = + pragma: no cover @abc.abstractmethod @abc.abstractproperty diff --git a/.travis/install.sh b/.travis/install.sh index 1e3b1702..b6dd5acc 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -29,13 +29,11 @@ if [[ "$(uname -s)" == "Darwin" ]]; then py26) curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py sudo python get-pip.py - sudo pip install setuptools --no-use-wheel --upgrade sudo pip install virtualenv ;; py27) curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py sudo python get-pip.py - sudo pip install setuptools --no-use-wheel --upgrade sudo pip install virtualenv ;; pypy) diff --git a/.travis/run.sh b/.travis/run.sh index 4c01d67a..6739c886 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -10,7 +10,12 @@ if [[ "$(uname -s)" == "Darwin" ]]; then export ARCHFLAGS="-arch x86_64" export LDFLAGS="-L/usr/local/opt/openssl/lib" export CFLAGS="-I/usr/local/opt/openssl/include" + # The Travis OS X jobs are run for two versions + # of OpenSSL, but we only need to run the + # CommonCrypto backend tests once. Exclude + # CommonCrypto when we test against brew OpenSSL + export TOX_FLAGS="--backend=openssl" fi fi source ~/.venv/bin/activate -tox -e $TOX_ENV +tox -e $TOX_ENV -- $TOX_FLAGS diff --git a/AUTHORS.rst b/AUTHORS.rst index ad27cec6..0e3979ad 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -12,3 +12,5 @@ PGP key fingerprints are enclosed in parentheses. * Jarret Raim <jarito@gmail.com> * Alex Stapleton <alexs@prol.etari.at> (A1C7 E50B 66DE 39ED C847 9665 8E3C 20D1 9BD9 5C4C) * David Reid <dreid@dreid.org> (0F83 CC87 B32F 482B C726 B58A 9FBF D8F4 DA89 6D74) +* Konstantinos Koukopoulos <koukopoulos@gmail.com> (D6BD 52B6 8C99 A91C E2C8 934D 3300 566B 3A46 726E) +* Stephen Holsapple <sholsapp@gmail.com> diff --git a/cryptography/exceptions.py b/cryptography/exceptions.py index 44363c24..e2542a1f 100644 --- a/cryptography/exceptions.py +++ b/cryptography/exceptions.py @@ -34,3 +34,11 @@ class InvalidTag(Exception): class InvalidSignature(Exception): pass + + +class InternalError(Exception): + pass + + +class InvalidKey(Exception): + pass diff --git a/cryptography/hazmat/backends/__init__.py b/cryptography/hazmat/backends/__init__.py index 215aa4d3..cb1fee90 100644 --- a/cryptography/hazmat/backends/__init__.py +++ b/cryptography/hazmat/backends/__init__.py @@ -12,11 +12,15 @@ # limitations under the License. from cryptography.hazmat.backends import openssl +from cryptography.hazmat.bindings.commoncrypto.binding import ( + Binding as CCBinding +) +_ALL_BACKENDS = [openssl.backend] -_ALL_BACKENDS = [ - openssl.backend -] +if CCBinding.is_available(): + from cryptography.hazmat.backends import commoncrypto + _ALL_BACKENDS.append(commoncrypto.backend) def default_backend(): diff --git a/cryptography/hazmat/backends/commoncrypto/__init__.py b/cryptography/hazmat/backends/commoncrypto/__init__.py new file mode 100644 index 00000000..64a1c01c --- /dev/null +++ b/cryptography/hazmat/backends/commoncrypto/__init__.py @@ -0,0 +1,17 @@ +# 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.backends.commoncrypto.backend import backend + + +__all__ = ["backend"] diff --git a/cryptography/hazmat/backends/commoncrypto/backend.py b/cryptography/hazmat/backends/commoncrypto/backend.py new file mode 100644 index 00000000..e5d4ee00 --- /dev/null +++ b/cryptography/hazmat/backends/commoncrypto/backend.py @@ -0,0 +1,482 @@ +# 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 collections import namedtuple + +from cryptography import utils +from cryptography.exceptions import ( + UnsupportedAlgorithm, InvalidTag, InternalError +) +from cryptography.hazmat.backends.interfaces import ( + HashBackend, HMACBackend, CipherBackend, PBKDF2HMACBackend +) +from cryptography.hazmat.bindings.commoncrypto.binding import Binding +from cryptography.hazmat.primitives import interfaces, constant_time +from cryptography.hazmat.primitives.ciphers.algorithms import ( + AES, Blowfish, TripleDES, ARC4 +) +from cryptography.hazmat.primitives.ciphers.modes import ( + CBC, CTR, ECB, OFB, CFB, GCM +) + + +HashMethods = namedtuple( + "HashMethods", ["ctx", "hash_init", "hash_update", "hash_final"] +) + + +@utils.register_interface(CipherBackend) +@utils.register_interface(HashBackend) +@utils.register_interface(HMACBackend) +@utils.register_interface(PBKDF2HMACBackend) +class Backend(object): + """ + CommonCrypto API wrapper. + """ + name = "commoncrypto" + + def __init__(self): + self._binding = Binding() + self._ffi = self._binding.ffi + self._lib = self._binding.lib + + self._cipher_registry = {} + self._register_default_ciphers() + self._hash_mapping = { + "md5": HashMethods( + "CC_MD5_CTX *", self._lib.CC_MD5_Init, + self._lib.CC_MD5_Update, self._lib.CC_MD5_Final + ), + "sha1": HashMethods( + "CC_SHA1_CTX *", self._lib.CC_SHA1_Init, + self._lib.CC_SHA1_Update, self._lib.CC_SHA1_Final + ), + "sha224": HashMethods( + "CC_SHA256_CTX *", self._lib.CC_SHA224_Init, + self._lib.CC_SHA224_Update, self._lib.CC_SHA224_Final + ), + "sha256": HashMethods( + "CC_SHA256_CTX *", self._lib.CC_SHA256_Init, + self._lib.CC_SHA256_Update, self._lib.CC_SHA256_Final + ), + "sha384": HashMethods( + "CC_SHA512_CTX *", self._lib.CC_SHA384_Init, + self._lib.CC_SHA384_Update, self._lib.CC_SHA384_Final + ), + "sha512": HashMethods( + "CC_SHA512_CTX *", self._lib.CC_SHA512_Init, + self._lib.CC_SHA512_Update, self._lib.CC_SHA512_Final + ), + } + + self._supported_hmac_algorithms = { + "md5": self._lib.kCCHmacAlgMD5, + "sha1": self._lib.kCCHmacAlgSHA1, + "sha224": self._lib.kCCHmacAlgSHA224, + "sha256": self._lib.kCCHmacAlgSHA256, + "sha384": self._lib.kCCHmacAlgSHA384, + "sha512": self._lib.kCCHmacAlgSHA512, + } + + self._supported_pbkdf2_hmac_algorithms = { + "sha1": self._lib.kCCPRFHmacAlgSHA1, + "sha224": self._lib.kCCPRFHmacAlgSHA224, + "sha256": self._lib.kCCPRFHmacAlgSHA256, + "sha384": self._lib.kCCPRFHmacAlgSHA384, + "sha512": self._lib.kCCPRFHmacAlgSHA512, + } + + def hash_supported(self, algorithm): + return algorithm.name in self._hash_mapping + + def hmac_supported(self, algorithm): + return algorithm.name in self._supported_hmac_algorithms + + def create_hash_ctx(self, algorithm): + return _HashContext(self, algorithm) + + def create_hmac_ctx(self, key, algorithm): + return _HMACContext(self, key, algorithm) + + def cipher_supported(self, cipher, mode): + return (type(cipher), type(mode)) in self._cipher_registry + + def create_symmetric_encryption_ctx(self, cipher, mode): + if isinstance(mode, GCM): + return _GCMCipherContext( + self, cipher, mode, self._lib.kCCEncrypt + ) + else: + return _CipherContext(self, cipher, mode, self._lib.kCCEncrypt) + + def create_symmetric_decryption_ctx(self, cipher, mode): + if isinstance(mode, GCM): + return _GCMCipherContext( + self, cipher, mode, self._lib.kCCDecrypt + ) + else: + return _CipherContext(self, cipher, mode, self._lib.kCCDecrypt) + + def pbkdf2_hmac_supported(self, algorithm): + return algorithm.name in self._supported_pbkdf2_hmac_algorithms + + def derive_pbkdf2_hmac(self, algorithm, length, salt, iterations, + key_material): + alg_enum = self._supported_pbkdf2_hmac_algorithms[algorithm.name] + buf = self._ffi.new("char[]", length) + res = self._lib.CCKeyDerivationPBKDF( + self._lib.kCCPBKDF2, + key_material, + len(key_material), + salt, + len(salt), + alg_enum, + iterations, + buf, + length + ) + self._check_response(res) + + return self._ffi.buffer(buf)[:] + + def _register_cipher_adapter(self, cipher_cls, cipher_const, mode_cls, + mode_const): + if (cipher_cls, mode_cls) in self._cipher_registry: + raise ValueError("Duplicate registration for: {0} {1}".format( + cipher_cls, mode_cls) + ) + self._cipher_registry[cipher_cls, mode_cls] = (cipher_const, + mode_const) + + def _register_default_ciphers(self): + for mode_cls, mode_const in [ + (CBC, self._lib.kCCModeCBC), + (ECB, self._lib.kCCModeECB), + (CFB, self._lib.kCCModeCFB), + (OFB, self._lib.kCCModeOFB), + (CTR, self._lib.kCCModeCTR), + (GCM, self._lib.kCCModeGCM), + ]: + self._register_cipher_adapter( + AES, + self._lib.kCCAlgorithmAES128, + mode_cls, + mode_const + ) + for mode_cls, mode_const in [ + (CBC, self._lib.kCCModeCBC), + (CFB, self._lib.kCCModeCFB), + (OFB, self._lib.kCCModeOFB), + ]: + self._register_cipher_adapter( + TripleDES, + self._lib.kCCAlgorithm3DES, + mode_cls, + mode_const + ) + for mode_cls, mode_const in [ + (CBC, self._lib.kCCModeCBC), + (ECB, self._lib.kCCModeECB), + (CFB, self._lib.kCCModeCFB), + (OFB, self._lib.kCCModeOFB) + ]: + self._register_cipher_adapter( + Blowfish, + self._lib.kCCAlgorithmBlowfish, + mode_cls, + mode_const + ) + self._register_cipher_adapter( + ARC4, + self._lib.kCCAlgorithmRC4, + type(None), + self._lib.kCCModeRC4 + ) + + def _check_response(self, response): + if response == self._lib.kCCSuccess: + return + elif response == self._lib.kCCAlignmentError: + # This error is not currently triggered due to a bug filed as + # rdar://15589470 + raise ValueError( + "The length of the provided data is not a multiple of " + "the block length" + ) + else: + raise InternalError( + "The backend returned an unknown error, consider filing a bug." + " Code: {0}.".format(response) + ) + + +def _release_cipher_ctx(ctx): + """ + Called by the garbage collector and used to safely dereference and + release the context. + """ + if ctx[0] != backend._ffi.NULL: + res = backend._lib.CCCryptorRelease(ctx[0]) + backend._check_response(res) + ctx[0] = backend._ffi.NULL + + +@utils.register_interface(interfaces.CipherContext) +class _CipherContext(object): + def __init__(self, backend, cipher, mode, operation): + self._backend = backend + self._cipher = cipher + self._mode = mode + self._operation = operation + # There is a bug in CommonCrypto where block ciphers do not raise + # kCCAlignmentError when finalizing if you supply non-block aligned + # data. To work around this we need to keep track of the block + # alignment ourselves, but only for alg+mode combos that require + # block alignment. OFB, CFB, and CTR make a block cipher algorithm + # into a stream cipher so we don't need to track them (and thus their + # block size is effectively 1 byte just like OpenSSL/CommonCrypto + # treat RC4 and other stream cipher block sizes). + # This bug has been filed as rdar://15589470 + self._bytes_processed = 0 + if (isinstance(cipher, interfaces.BlockCipherAlgorithm) and not + isinstance(mode, (OFB, CFB, CTR))): + self._byte_block_size = cipher.block_size // 8 + else: + self._byte_block_size = 1 + + registry = self._backend._cipher_registry + try: + cipher_enum, mode_enum = registry[type(cipher), type(mode)] + except KeyError: + raise UnsupportedAlgorithm( + "cipher {0} in {1} mode is not supported " + "by this backend".format( + cipher.name, mode.name if mode else mode) + ) + + ctx = self._backend._ffi.new("CCCryptorRef *") + ctx = self._backend._ffi.gc(ctx, _release_cipher_ctx) + + if isinstance(mode, interfaces.ModeWithInitializationVector): + iv_nonce = mode.initialization_vector + elif isinstance(mode, interfaces.ModeWithNonce): + iv_nonce = mode.nonce + else: + iv_nonce = self._backend._ffi.NULL + + if isinstance(mode, CTR): + mode_option = self._backend._lib.kCCModeOptionCTR_BE + else: + mode_option = 0 + + res = self._backend._lib.CCCryptorCreateWithMode( + operation, + mode_enum, cipher_enum, + self._backend._lib.ccNoPadding, iv_nonce, + cipher.key, len(cipher.key), + self._backend._ffi.NULL, 0, 0, mode_option, ctx) + self._backend._check_response(res) + + self._ctx = ctx + + def update(self, data): + # Count bytes processed to handle block alignment. + self._bytes_processed += len(data) + buf = self._backend._ffi.new( + "unsigned char[]", len(data) + self._byte_block_size - 1) + outlen = self._backend._ffi.new("size_t *") + res = self._backend._lib.CCCryptorUpdate( + self._ctx[0], data, len(data), buf, + len(data) + self._byte_block_size - 1, outlen) + self._backend._check_response(res) + return self._backend._ffi.buffer(buf)[:outlen[0]] + + def finalize(self): + # Raise error if block alignment is wrong. + if self._bytes_processed % self._byte_block_size: + raise ValueError( + "The length of the provided data is not a multiple of " + "the block length" + ) + buf = self._backend._ffi.new("unsigned char[]", self._byte_block_size) + outlen = self._backend._ffi.new("size_t *") + res = self._backend._lib.CCCryptorFinal( + self._ctx[0], buf, len(buf), outlen) + self._backend._check_response(res) + _release_cipher_ctx(self._ctx) + return self._backend._ffi.buffer(buf)[:outlen[0]] + + +@utils.register_interface(interfaces.AEADCipherContext) +@utils.register_interface(interfaces.AEADEncryptionContext) +class _GCMCipherContext(object): + def __init__(self, backend, cipher, mode, operation): + self._backend = backend + self._cipher = cipher + self._mode = mode + self._operation = operation + self._tag = None + + registry = self._backend._cipher_registry + try: + cipher_enum, mode_enum = registry[type(cipher), type(mode)] + except KeyError: + raise UnsupportedAlgorithm( + "cipher {0} in {1} mode is not supported " + "by this backend".format( + cipher.name, mode.name if mode else mode) + ) + + ctx = self._backend._ffi.new("CCCryptorRef *") + ctx = self._backend._ffi.gc(ctx, _release_cipher_ctx) + + self._ctx = ctx + + res = self._backend._lib.CCCryptorCreateWithMode( + operation, + mode_enum, cipher_enum, + self._backend._lib.ccNoPadding, + self._backend._ffi.NULL, + cipher.key, len(cipher.key), + self._backend._ffi.NULL, 0, 0, 0, self._ctx) + self._backend._check_response(res) + + res = self._backend._lib.CCCryptorGCMAddIV( + self._ctx[0], + mode.initialization_vector, + len(mode.initialization_vector) + ) + self._backend._check_response(res) + + def update(self, data): + buf = self._backend._ffi.new("unsigned char[]", len(data)) + args = (self._ctx[0], data, len(data), buf) + if self._operation == self._backend._lib.kCCEncrypt: + res = self._backend._lib.CCCryptorGCMEncrypt(*args) + else: + res = self._backend._lib.CCCryptorGCMDecrypt(*args) + + self._backend._check_response(res) + return self._backend._ffi.buffer(buf)[:] + + def finalize(self): + tag_size = self._cipher.block_size // 8 + tag_buf = self._backend._ffi.new("unsigned char[]", tag_size) + tag_len = self._backend._ffi.new("size_t *", tag_size) + res = backend._lib.CCCryptorGCMFinal(self._ctx[0], tag_buf, tag_len) + self._backend._check_response(res) + _release_cipher_ctx(self._ctx) + self._tag = self._backend._ffi.buffer(tag_buf)[:] + if (self._operation == self._backend._lib.kCCDecrypt and + not constant_time.bytes_eq( + self._tag[:len(self._mode.tag)], self._mode.tag + )): + raise InvalidTag + return b"" + + def authenticate_additional_data(self, data): + res = self._backend._lib.CCCryptorGCMAddAAD( + self._ctx[0], data, len(data) + ) + self._backend._check_response(res) + + @property + def tag(self): + return self._tag + + +@utils.register_interface(interfaces.HashContext) +class _HashContext(object): + def __init__(self, backend, algorithm, ctx=None): + self.algorithm = algorithm + self._backend = backend + + if ctx is None: + try: + methods = self._backend._hash_mapping[self.algorithm.name] + except KeyError: + raise UnsupportedAlgorithm( + "{0} is not a supported hash on this backend".format( + algorithm.name) + ) + ctx = self._backend._ffi.new(methods.ctx) + res = methods.hash_init(ctx) + assert res == 1 + + self._ctx = ctx + + def copy(self): + methods = self._backend._hash_mapping[self.algorithm.name] + new_ctx = self._backend._ffi.new(methods.ctx) + # CommonCrypto has no APIs for copying hashes, so we have to copy the + # underlying struct. + new_ctx[0] = self._ctx[0] + + return _HashContext(self._backend, self.algorithm, ctx=new_ctx) + + def update(self, data): + methods = self._backend._hash_mapping[self.algorithm.name] + res = methods.hash_update(self._ctx, data, len(data)) + assert res == 1 + + def finalize(self): + methods = self._backend._hash_mapping[self.algorithm.name] + buf = self._backend._ffi.new("unsigned char[]", + self.algorithm.digest_size) + res = methods.hash_final(buf, self._ctx) + assert res == 1 + return self._backend._ffi.buffer(buf)[:] + + +@utils.register_interface(interfaces.HashContext) +class _HMACContext(object): + def __init__(self, backend, key, algorithm, ctx=None): + self.algorithm = algorithm + self._backend = backend + if ctx is None: + ctx = self._backend._ffi.new("CCHmacContext *") + try: + alg = self._backend._supported_hmac_algorithms[algorithm.name] + except KeyError: + raise UnsupportedAlgorithm( + "{0} is not a supported HMAC hash on this backend".format( + algorithm.name) + ) + + self._backend._lib.CCHmacInit(ctx, alg, key, len(key)) + + self._ctx = ctx + self._key = key + + def copy(self): + copied_ctx = self._backend._ffi.new("CCHmacContext *") + # CommonCrypto has no APIs for copying HMACs, so we have to copy the + # underlying struct. + copied_ctx[0] = self._ctx[0] + return _HMACContext( + self._backend, self._key, self.algorithm, ctx=copied_ctx + ) + + def update(self, data): + self._backend._lib.CCHmacUpdate(self._ctx, data, len(data)) + + def finalize(self): + buf = self._backend._ffi.new("unsigned char[]", + self.algorithm.digest_size) + self._backend._lib.CCHmacFinal(self._ctx, buf) + return self._backend._ffi.buffer(buf)[:] + + +backend = Backend() diff --git a/cryptography/hazmat/backends/interfaces.py b/cryptography/hazmat/backends/interfaces.py index 9a570968..53c75181 100644 --- a/cryptography/hazmat/backends/interfaces.py +++ b/cryptography/hazmat/backends/interfaces.py @@ -26,12 +26,6 @@ class CipherBackend(six.with_metaclass(abc.ABCMeta)): """ @abc.abstractmethod - def register_cipher_adapter(self, cipher, mode, adapter): - """ - Register an adapter for a cipher and mode to a backend specific object. - """ - - @abc.abstractmethod def create_symmetric_encryption_ctx(self, cipher, mode): """ Get a CipherContext that can be used for encryption. @@ -71,3 +65,19 @@ class HMACBackend(six.with_metaclass(abc.ABCMeta)): """ Create a HashContext for calculating a message authentication code. """ + + +class PBKDF2HMACBackend(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def pbkdf2_hmac_supported(self, algorithm): + """ + Return True if the hash algorithm is supported for PBKDF2 by this + backend. + """ + + @abc.abstractmethod + def derive_pbkdf2_hmac(self, algorithm, length, salt, iterations, + key_material): + """ + Return length bytes derived from provided PBKDF2 parameters. + """ diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index 07ee58c1..cf931dab 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -16,11 +16,13 @@ from __future__ import absolute_import, division, print_function import itertools from cryptography import utils -from cryptography.exceptions import UnsupportedAlgorithm, InvalidTag +from cryptography.exceptions import ( + UnsupportedAlgorithm, InvalidTag, InternalError +) from cryptography.hazmat.backends.interfaces import ( - CipherBackend, HashBackend, HMACBackend + CipherBackend, HashBackend, HMACBackend, PBKDF2HMACBackend ) -from cryptography.hazmat.primitives import interfaces +from cryptography.hazmat.primitives import interfaces, hashes from cryptography.hazmat.primitives.ciphers.algorithms import ( AES, Blowfish, Camellia, TripleDES, ARC4, ) @@ -33,16 +35,20 @@ from cryptography.hazmat.bindings.openssl.binding import Binding @utils.register_interface(CipherBackend) @utils.register_interface(HashBackend) @utils.register_interface(HMACBackend) +@utils.register_interface(PBKDF2HMACBackend) class Backend(object): """ OpenSSL API binding interfaces. """ + name = "openssl" def __init__(self): self._binding = Binding() self._ffi = self._binding.ffi self._lib = self._binding.lib + self._binding.init_static_locks() + # adds all ciphers/digests for EVP self._lib.OpenSSL_add_all_algorithms() # registers available SSL/TLS ciphers and digests @@ -128,6 +134,49 @@ class Backend(object): def create_symmetric_decryption_ctx(self, cipher, mode): return _CipherContext(self, cipher, mode, _CipherContext._DECRYPT) + def pbkdf2_hmac_supported(self, algorithm): + if self._lib.Cryptography_HAS_PBKDF2_HMAC: + return self.hmac_supported(algorithm) + else: + # OpenSSL < 1.0.0 has an explicit PBKDF2-HMAC-SHA1 function, + # so if the PBKDF2_HMAC function is missing we only support + # SHA1 via PBKDF2_HMAC_SHA1. + return isinstance(algorithm, hashes.SHA1) + + def derive_pbkdf2_hmac(self, algorithm, length, salt, iterations, + key_material): + buf = self._ffi.new("char[]", length) + if self._lib.Cryptography_HAS_PBKDF2_HMAC: + evp_md = self._lib.EVP_get_digestbyname( + algorithm.name.encode("ascii")) + assert evp_md != self._ffi.NULL + res = self._lib.PKCS5_PBKDF2_HMAC( + key_material, + len(key_material), + salt, + len(salt), + iterations, + evp_md, + length, + buf + ) + assert res == 1 + else: + # OpenSSL < 1.0.0 + assert isinstance(algorithm, hashes.SHA1) + res = self._lib.PKCS5_PBKDF2_HMAC_SHA1( + key_material, + len(key_material), + salt, + len(salt), + iterations, + length, + buf + ) + assert res == 1 + + return self._ffi.buffer(buf)[:] + def _handle_error(self, mode): code = self._lib.ERR_get_error() if not code and isinstance(mode, GCM): @@ -153,7 +202,7 @@ class Backend(object): "the block length" ) - raise SystemError( + raise InternalError( "Unknown error code from OpenSSL, you should probably file a bug." ) diff --git a/cryptography/hazmat/bindings/commoncrypto/binding.py b/cryptography/hazmat/bindings/commoncrypto/binding.py index 9c1af40a..45c0eaad 100644 --- a/cryptography/hazmat/bindings/commoncrypto/binding.py +++ b/cryptography/hazmat/bindings/commoncrypto/binding.py @@ -26,6 +26,8 @@ class Binding(object): _modules = [ "common_digest", "common_hmac", + "common_key_derivation", + "common_cryptor", ] ffi = None diff --git a/cryptography/hazmat/bindings/commoncrypto/common_cryptor.py b/cryptography/hazmat/bindings/commoncrypto/common_cryptor.py new file mode 100644 index 00000000..8f03bc3f --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/common_cryptor.py @@ -0,0 +1,108 @@ +# 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. + +INCLUDES = """ +#include <CommonCrypto/CommonCryptor.h> +""" + +TYPES = """ +enum { + kCCAlgorithmAES128 = 0, + kCCAlgorithmDES, + kCCAlgorithm3DES, + kCCAlgorithmCAST, + kCCAlgorithmRC4, + kCCAlgorithmRC2, + kCCAlgorithmBlowfish +}; +typedef uint32_t CCAlgorithm; +enum { + kCCSuccess = 0, + kCCParamError = -4300, + kCCBufferTooSmall = -4301, + kCCMemoryFailure = -4302, + kCCAlignmentError = -4303, + kCCDecodeError = -4304, + kCCUnimplemented = -4305 +}; +typedef int32_t CCCryptorStatus; +typedef uint32_t CCOptions; +enum { + kCCEncrypt = 0, + kCCDecrypt, +}; +typedef uint32_t CCOperation; +typedef ... *CCCryptorRef; + +enum { + kCCModeOptionCTR_LE = 0x0001, + kCCModeOptionCTR_BE = 0x0002 +}; + +typedef uint32_t CCModeOptions; + +enum { + kCCModeECB = 1, + kCCModeCBC = 2, + kCCModeCFB = 3, + kCCModeCTR = 4, + kCCModeF8 = 5, + kCCModeLRW = 6, + kCCModeOFB = 7, + kCCModeXTS = 8, + kCCModeRC4 = 9, + kCCModeCFB8 = 10, + kCCModeGCM = 11 +}; +typedef uint32_t CCMode; +enum { + ccNoPadding = 0, + ccPKCS7Padding = 1, +}; +typedef uint32_t CCPadding; +""" + +FUNCTIONS = """ +CCCryptorStatus CCCryptorCreateWithMode(CCOperation, CCMode, CCAlgorithm, + CCPadding, const void *, const void *, + size_t, const void *, size_t, int, + CCModeOptions, CCCryptorRef *); +CCCryptorStatus CCCryptorCreate(CCOperation, CCAlgorithm, CCOptions, + const void *, size_t, const void *, + CCCryptorRef *); +CCCryptorStatus CCCryptorUpdate(CCCryptorRef, const void *, size_t, void *, + size_t, size_t *); +CCCryptorStatus CCCryptorFinal(CCCryptorRef, void *, size_t, size_t *); +CCCryptorStatus CCCryptorRelease(CCCryptorRef); + +CCCryptorStatus CCCryptorGCMAddIV(CCCryptorRef, const void *, size_t); +CCCryptorStatus CCCryptorGCMAddAAD(CCCryptorRef, const void *, size_t); +CCCryptorStatus CCCryptorGCMEncrypt(CCCryptorRef, const void *, size_t, + void *); +CCCryptorStatus CCCryptorGCMDecrypt(CCCryptorRef, const void *, size_t, + void *); +CCCryptorStatus CCCryptorGCMFinal(CCCryptorRef, const void *, size_t *); +CCCryptorStatus CCCryptorGCMReset(CCCryptorRef); +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +// Not defined in the public header +enum { + kCCModeGCM = 11 +}; +""" + +CONDITIONAL_NAMES = {} diff --git a/cryptography/hazmat/bindings/commoncrypto/common_key_derivation.py b/cryptography/hazmat/bindings/commoncrypto/common_key_derivation.py new file mode 100644 index 00000000..85def1e9 --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/common_key_derivation.py @@ -0,0 +1,48 @@ +# 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. + +INCLUDES = """ +#include <CommonCrypto/CommonKeyDerivation.h> +""" + +TYPES = """ +enum { + kCCPBKDF2 = 2, +}; +typedef uint32_t CCPBKDFAlgorithm; +enum { + kCCPRFHmacAlgSHA1 = 1, + kCCPRFHmacAlgSHA224 = 2, + kCCPRFHmacAlgSHA256 = 3, + kCCPRFHmacAlgSHA384 = 4, + kCCPRFHmacAlgSHA512 = 5, +}; +typedef uint32_t CCPseudoRandomAlgorithm; +typedef unsigned int uint; +""" + +FUNCTIONS = """ +int CCKeyDerivationPBKDF(CCPBKDFAlgorithm, const char *, size_t, + const uint8_t *, size_t, CCPseudoRandomAlgorithm, + uint, uint8_t *, size_t); +uint CCCalibratePBKDF(CCPBKDFAlgorithm, size_t, size_t, + CCPseudoRandomAlgorithm, size_t, uint32_t); +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +""" + +CONDITIONAL_NAMES = {} diff --git a/cryptography/hazmat/bindings/openssl/aes.py b/cryptography/hazmat/bindings/openssl/aes.py new file mode 100644 index 00000000..6cbcd577 --- /dev/null +++ b/cryptography/hazmat/bindings/openssl/aes.py @@ -0,0 +1,40 @@ +# 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. + +INCLUDES = """ +#include <openssl/aes.h> +""" + +TYPES = """ +struct aes_key_st { + ...; +}; +typedef struct aes_key_st AES_KEY; +""" + +FUNCTIONS = """ +int AES_set_encrypt_key(const unsigned char *, const int, AES_KEY *); +int AES_set_decrypt_key(const unsigned char *, const int, AES_KEY *); +int AES_wrap_key(AES_KEY *, const unsigned char *, unsigned char *, + const unsigned char *, unsigned int); +int AES_unwrap_key(AES_KEY *, const unsigned char *, unsigned char *, + const unsigned char *, unsigned int); +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +""" + +CONDITIONAL_NAMES = {} diff --git a/cryptography/hazmat/bindings/openssl/bignum.py b/cryptography/hazmat/bindings/openssl/bignum.py index 59efd171..6545f329 100644 --- a/cryptography/hazmat/bindings/openssl/bignum.py +++ b/cryptography/hazmat/bindings/openssl/bignum.py @@ -47,6 +47,9 @@ char *BN_bn2hex(const BIGNUM *); int BN_hex2bn(BIGNUM **, const char *); int BN_dec2bn(BIGNUM **, const char *); +int BN_bn2bin(const BIGNUM *, unsigned char *); +BIGNUM *BN_bin2bn(const unsigned char *, int, BIGNUM *); + int BN_num_bits(const BIGNUM *); """ diff --git a/cryptography/hazmat/bindings/openssl/binding.py b/cryptography/hazmat/bindings/openssl/binding.py index 88299d14..cde3bdbd 100644 --- a/cryptography/hazmat/bindings/openssl/binding.py +++ b/cryptography/hazmat/bindings/openssl/binding.py @@ -13,6 +13,9 @@ from __future__ import absolute_import, division, print_function +import sys +import threading + from cryptography.hazmat.bindings.utils import build_ffi @@ -41,6 +44,7 @@ class Binding(object): """ _module_prefix = "cryptography.hazmat.bindings.openssl." _modules = [ + "aes", "asn1", "bignum", "bio", @@ -67,6 +71,10 @@ class Binding(object): "x509v3", ] + _locks = None + _lock_cb_handle = None + _lock_init_lock = threading.Lock() + ffi = None lib = None @@ -78,11 +86,57 @@ class Binding(object): if cls.ffi is not None and cls.lib is not None: return + # platform check to set the right library names + if sys.platform != "win32": + libraries = ["crypto", "ssl"] + else: # pragma: no cover + libraries = ["libeay32", "ssleay32"] + cls.ffi, cls.lib = build_ffi(cls._module_prefix, cls._modules, _OSX_PRE_INCLUDE, _OSX_POST_INCLUDE, - ["crypto", "ssl"]) + libraries) @classmethod def is_available(cls): # OpenSSL is the only binding so for now it must always be available return True + + @classmethod + def init_static_locks(cls): + with cls._lock_init_lock: + cls._ensure_ffi_initialized() + + if not cls._lock_cb_handle: + cls._lock_cb_handle = cls.ffi.callback( + "void(int, int, const char *, int)", + cls._lock_cb + ) + + # use Python's implementation if available + + __import__("_ssl") + + if cls.lib.CRYPTO_get_locking_callback() != cls.ffi.NULL: + return + + # otherwise setup our version + + num_locks = cls.lib.CRYPTO_num_locks() + cls._locks = [threading.Lock() for n in range(num_locks)] + + cls.lib.CRYPTO_set_locking_callback(cls._lock_cb_handle) + + @classmethod + def _lock_cb(cls, mode, n, file, line): + lock = cls._locks[n] + + if mode & cls.lib.CRYPTO_LOCK: + lock.acquire() + elif mode & cls.lib.CRYPTO_UNLOCK: + lock.release() + else: + raise RuntimeError( + "Unknown lock mode {0}: lock={1}, file={2}, line={3}".format( + mode, n, file, line + ) + ) diff --git a/cryptography/hazmat/bindings/openssl/crypto.py b/cryptography/hazmat/bindings/openssl/crypto.py index 40d91bf2..81d13b73 100644 --- a/cryptography/hazmat/bindings/openssl/crypto.py +++ b/cryptography/hazmat/bindings/openssl/crypto.py @@ -27,6 +27,11 @@ static const int CRYPTO_MEM_CHECK_ON; static const int CRYPTO_MEM_CHECK_OFF; static const int CRYPTO_MEM_CHECK_ENABLE; static const int CRYPTO_MEM_CHECK_DISABLE; +static const int CRYPTO_LOCK; +static const int CRYPTO_UNLOCK; +static const int CRYPTO_READ; +static const int CRYPTO_WRITE; +static const int CRYPTO_LOCK_SSL; """ FUNCTIONS = """ @@ -43,6 +48,7 @@ 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 CRYPTO_lock(int, int, const char *, int); void OPENSSL_free(void *); """ @@ -51,7 +57,6 @@ MACROS = """ void CRYPTO_add(int *, int, int); void CRYPTO_malloc_init(void); void CRYPTO_malloc_debug_init(void); - """ CUSTOMIZATIONS = """ diff --git a/cryptography/hazmat/bindings/openssl/dh.py b/cryptography/hazmat/bindings/openssl/dh.py index 3c12fbc6..ecc62e98 100644 --- a/cryptography/hazmat/bindings/openssl/dh.py +++ b/cryptography/hazmat/bindings/openssl/dh.py @@ -16,7 +16,17 @@ INCLUDES = """ """ TYPES = """ -typedef ... DH; +typedef struct dh_st { + // prime number (shared) + BIGNUM *p; + // generator of Z_p (shared) + BIGNUM *g; + // private DH value x + BIGNUM *priv_key; + // public DH value g^x + BIGNUM *pub_key; + ...; +} DH; """ FUNCTIONS = """ diff --git a/cryptography/hazmat/bindings/openssl/dsa.py b/cryptography/hazmat/bindings/openssl/dsa.py index 3b77d7ae..609a33bf 100644 --- a/cryptography/hazmat/bindings/openssl/dsa.py +++ b/cryptography/hazmat/bindings/openssl/dsa.py @@ -16,7 +16,19 @@ INCLUDES = """ """ TYPES = """ -typedef ... DSA; +typedef struct dsa_st { + // prime number (public) + BIGNUM *p; + // 160-bit subprime, q | p-1 (public) + BIGNUM *q; + // generator of subgroup (public) + BIGNUM *g; + // private key x + BIGNUM *priv_key; + // public key y = g^x + BIGNUM *pub_key; + ...; +} DSA; """ FUNCTIONS = """ diff --git a/cryptography/hazmat/bindings/openssl/ec.py b/cryptography/hazmat/bindings/openssl/ec.py index 9f10365a..39403ff2 100644 --- a/cryptography/hazmat/bindings/openssl/ec.py +++ b/cryptography/hazmat/bindings/openssl/ec.py @@ -12,7 +12,10 @@ # limitations under the License. INCLUDES = """ +#ifndef OPENSSL_NO_EC #include <openssl/ec.h> +#endif + #include <openssl/obj_mac.h> """ @@ -31,16 +34,17 @@ static const int NID_X9_62_prime256v1; """ FUNCTIONS = """ -EC_KEY *EC_KEY_new_by_curve_name(int); -void EC_KEY_free(EC_KEY *); """ MACROS = """ +EC_KEY *EC_KEY_new_by_curve_name(int); +void EC_KEY_free(EC_KEY *); """ CUSTOMIZATIONS = """ #ifdef OPENSSL_NO_EC static const long Cryptography_HAS_EC = 0; +typedef void EC_KEY; EC_KEY* (*EC_KEY_new_by_curve_name)(int) = NULL; void (*EC_KEY_free)(EC_KEY *) = NULL; #else diff --git a/cryptography/hazmat/bindings/openssl/evp.py b/cryptography/hazmat/bindings/openssl/evp.py index c426e52e..c7cc154f 100644 --- a/cryptography/hazmat/bindings/openssl/evp.py +++ b/cryptography/hazmat/bindings/openssl/evp.py @@ -40,6 +40,7 @@ static const int EVP_CTRL_GCM_GET_TAG; static const int EVP_CTRL_GCM_SET_TAG; static const int Cryptography_HAS_GCM; +static const int Cryptography_HAS_PBKDF2_HMAC; """ FUNCTIONS = """ @@ -95,6 +96,9 @@ int EVP_VerifyFinal(EVP_MD_CTX *, const unsigned char *, unsigned int, EVP_PKEY *); const EVP_MD *EVP_md5(void); + +int PKCS5_PBKDF2_HMAC_SHA1(const char *, int, const unsigned char *, int, int, + int, unsigned char *); """ MACROS = """ @@ -103,6 +107,9 @@ int EVP_PKEY_assign_RSA(EVP_PKEY *, RSA *); int EVP_PKEY_assign_DSA(EVP_PKEY *, DSA *); int EVP_CIPHER_CTX_block_size(const EVP_CIPHER_CTX *); int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *, int, int, void *); + +int PKCS5_PBKDF2_HMAC(const char *, int, const unsigned char *, int, int, + const EVP_MD *, int, unsigned char *); """ CUSTOMIZATIONS = """ @@ -114,6 +121,13 @@ const long EVP_CTRL_GCM_GET_TAG = -1; const long EVP_CTRL_GCM_SET_TAG = -1; const long EVP_CTRL_GCM_SET_IVLEN = -1; #endif +#if OPENSSL_VERSION_NUMBER >= 0x10000000 +const long Cryptography_HAS_PBKDF2_HMAC = 1; +#else +const long Cryptography_HAS_PBKDF2_HMAC = 0; +int (*PKCS5_PBKDF2_HMAC)(const char *, int, const unsigned char *, int, int, + const EVP_MD *, int, unsigned char *) = NULL; +#endif """ CONDITIONAL_NAMES = { @@ -121,5 +135,8 @@ CONDITIONAL_NAMES = { "EVP_CTRL_GCM_GET_TAG", "EVP_CTRL_GCM_SET_TAG", "EVP_CTRL_GCM_SET_IVLEN", + ], + "Cryptography_HAS_PBKDF2_HMAC": [ + "PKCS5_PBKDF2_HMAC" ] } diff --git a/cryptography/hazmat/bindings/openssl/ssl.py b/cryptography/hazmat/bindings/openssl/ssl.py index cd872d18..2b4e54f1 100644 --- a/cryptography/hazmat/bindings/openssl/ssl.py +++ b/cryptography/hazmat/bindings/openssl/ssl.py @@ -393,6 +393,6 @@ CONDITIONAL_NAMES = { ], "Cryptography_HAS_EC": [ - "EC_KEY_new_by_curve_name", + "SSL_CTX_set_tmp_ecdh", ] } diff --git a/cryptography/hazmat/bindings/openssl/x509.py b/cryptography/hazmat/bindings/openssl/x509.py index 840254a2..e4021a12 100644 --- a/cryptography/hazmat/bindings/openssl/x509.py +++ b/cryptography/hazmat/bindings/openssl/x509.py @@ -119,6 +119,7 @@ int X509_REQ_sign(X509_REQ *, EVP_PKEY *, const EVP_MD *); int X509_REQ_verify(X509_REQ *, EVP_PKEY *); EVP_PKEY *X509_REQ_get_pubkey(X509_REQ *); int X509_REQ_add_extensions(X509_REQ *, X509_EXTENSIONS *); +X509_EXTENSIONS *X509_REQ_get_extensions(X509_REQ *); int X509_REQ_print_ex(BIO *, X509_REQ *, unsigned long, unsigned long); int X509V3_EXT_print(BIO *, X509_EXTENSION *, unsigned long, int); @@ -165,6 +166,7 @@ int X509_set_serialNumber(X509 *, ASN1_INTEGER *); X509_STORE *X509_STORE_new(void); void X509_STORE_free(X509_STORE *); int X509_STORE_add_cert(X509_STORE *, X509 *); +int X509_verify_cert(X509_STORE_CTX *); """ MACROS = """ diff --git a/cryptography/hazmat/primitives/interfaces.py b/cryptography/hazmat/primitives/interfaces.py index 7a6bf3e2..1a27644f 100644 --- a/cryptography/hazmat/primitives/interfaces.py +++ b/cryptography/hazmat/primitives/interfaces.py @@ -169,3 +169,107 @@ class HashContext(six.with_metaclass(abc.ABCMeta)): """ Return a HashContext that is a copy of the current context. """ + + +class RSAPrivateKey(six.with_metaclass(abc.ABCMeta)): + @abc.abstractproperty + def modulus(self): + """ + The public modulus of the RSA key. + """ + + @abc.abstractproperty + def public_exponent(self): + """ + The public exponent of the RSA key. + """ + + @abc.abstractproperty + def key_length(self): + """ + The bit length of the public modulus. + """ + + @abc.abstractmethod + def public_key(self): + """ + The RSAPublicKey associated with this private key. + """ + + @abc.abstractproperty + def n(self): + """ + The public modulus of the RSA key. Alias for modulus. + """ + + @abc.abstractproperty + def p(self): + """ + One of the two primes used to generate d. + """ + + @abc.abstractproperty + def q(self): + """ + One of the two primes used to generate d. + """ + + @abc.abstractproperty + def d(self): + """ + The private exponent. This can be calculated using p and q. + """ + + @abc.abstractproperty + def e(self): + """ + The public exponent of the RSA key. Alias for public_exponent. + """ + + +class RSAPublicKey(six.with_metaclass(abc.ABCMeta)): + @abc.abstractproperty + def modulus(self): + """ + The public modulus of the RSA key. + """ + + @abc.abstractproperty + def public_exponent(self): + """ + The public exponent of the RSA key. + """ + + @abc.abstractproperty + def key_length(self): + """ + The bit length of the public modulus. + """ + + @abc.abstractproperty + def n(self): + """ + The public modulus of the RSA key. Alias for modulus. + """ + + @abc.abstractproperty + def e(self): + """ + The public exponent of the RSA key. Alias for public_exponent. + """ + + +class KeyDerivationFunction(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def derive(self, key_material): + """ + Deterministically generates and returns a new key based on the existing + key material. + """ + + @abc.abstractmethod + def verify(self, key_material, expected_key): + """ + Checks whether the key generated by the key material matches the + expected derived key. Raises an exception if they do not match. + """ diff --git a/cryptography/hazmat/primitives/kdf/__init__.py b/cryptography/hazmat/primitives/kdf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cryptography/hazmat/primitives/kdf/__init__.py diff --git a/cryptography/hazmat/primitives/kdf/pbkdf2.py b/cryptography/hazmat/primitives/kdf/pbkdf2.py new file mode 100644 index 00000000..71b88211 --- /dev/null +++ b/cryptography/hazmat/primitives/kdf/pbkdf2.py @@ -0,0 +1,66 @@ +# 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 six + +from cryptography import utils +from cryptography.exceptions import ( + InvalidKey, UnsupportedAlgorithm, AlreadyFinalized +) +from cryptography.hazmat.primitives import constant_time, interfaces + + +@utils.register_interface(interfaces.KeyDerivationFunction) +class PBKDF2HMAC(object): + def __init__(self, algorithm, length, salt, iterations, backend): + if not backend.pbkdf2_hmac_supported(algorithm): + raise UnsupportedAlgorithm( + "{0} is not supported for PBKDF2 by this backend".format( + algorithm.name) + ) + self._used = False + self._algorithm = algorithm + self._length = length + if isinstance(salt, six.text_type): + raise TypeError( + "Unicode-objects must be encoded before using them as key " + "material." + ) + self._salt = salt + self._iterations = iterations + self._backend = backend + + def derive(self, key_material): + if self._used: + raise AlreadyFinalized("PBKDF2 instances can only be used once") + self._used = True + + if isinstance(key_material, six.text_type): + raise TypeError( + "Unicode-objects must be encoded before using them as key " + "material." + ) + return self._backend.derive_pbkdf2_hmac( + self._algorithm, + self._length, + self._salt, + self._iterations, + key_material + ) + + def verify(self, key_material, expected_key): + derived_key = self.derive(key_material) + if not constant_time.bytes_eq(derived_key, expected_key): + raise InvalidKey("Keys do not match.") diff --git a/docs/changelog.rst b/docs/changelog.rst index 289992f4..f401fe7c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,9 +6,18 @@ Changelog **In development** -* Added initial CommonCrypto bindings. +* Added :doc:`/hazmat/backends/commoncrypto`. +* Added initial :doc:`/hazmat/bindings/commoncrypto`. +* Removed ``register_cipher_adapter`` method from + :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`. +* Added support for the OpenSSL backend under Windows. +* Improved thread-safety for the OpenSSL backend. +* Fixed compilation on systems where OpenSSL's ``ec.h`` header is not + available, such as CentOS. +* Added :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`. 0.1 - 2014-01-08 ~~~~~~~~~~~~~~~~ * Initial release. + diff --git a/docs/contributing.rst b/docs/contributing.rst index 8e32c368..184ba214 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -60,6 +60,12 @@ always indistinguishable. As a result ``cryptography`` has, as a design philosophy: "make it hard to do insecure things". Here are a few strategies for API design which should be both followed, and should inspire other API choices: +If it is necessary to compare a user provided value with a computed value (for +example, verifying a signature), there should be an API provided which performs +the verification in a secure way (for example, using a constant time +comparison), rather than requiring the user to perform the comparison +themselves. + If it is incorrect to ignore the result of a method, it should raise an exception, and not return a boolean ``True``/``False`` flag. For example, a method to verify a signature should raise ``InvalidSignature``, and not return @@ -250,6 +256,16 @@ each supported Python version and run the tests. For example: You may not have all the required Python versions installed, in which case you will see one or more ``InterpreterNotFound`` errors. + +Explicit Backend Selection +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While testing you may want to run tests against a subset of the backends that +cryptography supports. Explicit backend selection can be done via the +``--backend`` flag. This flag should be passed to ``py.test`` with a comma +delimited list of backend names. To use it with ``tox`` you must pass it as +``tox -- --backend=openssl``. + Building Documentation ~~~~~~~~~~~~~~~~~~~~~~ @@ -277,5 +293,5 @@ The HTML documentation index can now be found at .. _`tox`: https://pypi.python.org/pypi/tox .. _`virtualenv`: https://pypi.python.org/pypi/virtualenv .. _`pip`: https://pypi.python.org/pypi/pip -.. _`sphinx`: https://pypi.python.org/pypi/sphinx +.. _`sphinx`: https://pypi.python.org/pypi/Sphinx .. _`reStructured Text`: http://sphinx-doc.org/rest.html diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 1fbd3267..1e31e31c 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -10,8 +10,8 @@ Exceptions .. class:: InvalidSignature - This is raised when the verify method of a hash context does not - compare equal. + This is raised when the verify method of a hash context's computed digest + does not match the expected digest. .. class:: NotYetFinalized @@ -30,3 +30,9 @@ Exceptions This is raised when a backend doesn't support the requested algorithm (or combination of algorithms). + + +.. class:: InvalidKey + + This is raised when the verify method of a key derivation function's + computed key does not match the expected key. diff --git a/docs/fernet.rst b/docs/fernet.rst index 13295c0c..b0215e32 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -72,5 +72,22 @@ symmetric (also known as "secret key") authenticated cryptography. See :meth:`Fernet.decrypt` for more information. +Implementation +-------------- + +Fernet is built on top of a number of standard cryptographic primitives. +Specifically it uses: + +* :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES` in + :class:`~cryptography.hazmat.primitives.ciphers.modes.CBC` mode with a + 128-bit key for encryption; using + :class:`~cryptography.hazmat.primitives.ciphers.PKCS7` padding. +* :class:`~cryptography.hazmat.primitives.hmac.HMAC` using + :class:`~cryptography.hazmat.primitives.hashes.SHA256` for authentication. +* Initialization vectors are generated using ``os.urandom()``. + +For complete details consult the `specification`_. + .. _`Fernet`: https://github.com/fernet/spec/ +.. _`specification`: https://github.com/fernet/spec/blob/master/Spec.md diff --git a/docs/hazmat/backends/commoncrypto.rst b/docs/hazmat/backends/commoncrypto.rst new file mode 100644 index 00000000..af2032b6 --- /dev/null +++ b/docs/hazmat/backends/commoncrypto.rst @@ -0,0 +1,20 @@ +.. hazmat:: + +CommonCrypto Backend +==================== + +The `CommonCrypto`_ C library provided by Apple on OS X and iOS. + +.. currentmodule:: cryptography.hazmat.backends.commoncrypto.backend + +.. versionadded:: 0.2 + +.. data:: cryptography.hazmat.backends.commoncrypto.backend + + This is the exposed API for the CommonCrypto backend. It has one public attribute. + + .. attribute:: name + + The string name of this backend: ``"commoncrypto"`` + +.. _`CommonCrypto`: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/Common%20Crypto.3cc.html diff --git a/docs/hazmat/backends/index.rst b/docs/hazmat/backends/index.rst index 06951281..dbc0724e 100644 --- a/docs/hazmat/backends/index.rst +++ b/docs/hazmat/backends/index.rst @@ -31,4 +31,5 @@ Individual Backends :maxdepth: 1 openssl + commoncrypto interfaces diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst index 5b6cd64d..49e4c88c 100644 --- a/docs/hazmat/backends/interfaces.rst +++ b/docs/hazmat/backends/interfaces.rst @@ -33,30 +33,11 @@ A specific ``backend`` may provide one or more of these interfaces. :returns: ``True`` if the specified ``cipher`` and ``mode`` combination is supported by this backend, otherwise ``False`` - .. method:: register_cipher_adapter(cipher_cls, mode_cls, adapter) - - Register an adapter which can be used to create a backend specific - object from instances of the - :class:`~cryptography.hazmat.primitives.interfaces.CipherAlgorithm` and - the :class:`~cryptography.hazmat.primitives.interfaces.Mode` primitives. - - :param cipher_cls: A class whose instances provide - :class:`~cryptography.hazmat.primitives.interfaces.CipherAlgorithm` - :param mode_cls: A class whose instances provide: - :class:`~cryptography.hazmat.primitives.interfaces.Mode` - :param adapter: A ``function`` that takes 3 arguments, ``backend`` (a - :class:`CipherBackend` provider), ``cipher`` (a - :class:`~cryptography.hazmat.primitives.interfaces.CipherAlgorithm` - provider ), and ``mode`` (a - :class:`~cryptography.hazmat.primitives.interfaces.Mode` provider). - It returns a backend specific object which may be used to construct - a :class:`~cryptogrpahy.hazmat.primitives.interfaces.CipherContext`. - .. method:: create_symmetric_encryption_ctx(cipher, mode) Create a - :class:`~cryptogrpahy.hazmat.primitives.interfaces.CipherContext` that + :class:`~cryptography.hazmat.primitives.interfaces.CipherContext` that can be used for encrypting data with the symmetric ``cipher`` using the given ``mode``. @@ -75,7 +56,7 @@ A specific ``backend`` may provide one or more of these interfaces. .. method:: create_symmetric_decryption_ctx(cipher, mode) Create a - :class:`~cryptogrpahy.hazmat.primitives.interfaces.CipherContext` that + :class:`~cryptography.hazmat.primitives.interfaces.CipherContext` that can be used for decrypting data with the symmetric ``cipher`` using the given ``mode``. @@ -110,7 +91,7 @@ A specific ``backend`` may provide one or more of these interfaces. .. method:: create_hash_ctx(algorithm) Create a - :class:`~cryptogrpahy.hazmat.primitives.interfaces.HashContext` that + :class:`~cryptography.hazmat.primitives.interfaces.HashContext` that uses the specified ``algorithm`` to calculate a message digest. :param algorithm: An instance of a @@ -140,7 +121,7 @@ A specific ``backend`` may provide one or more of these interfaces. .. method:: create_hmac_ctx(algorithm) Create a - :class:`~cryptogrpahy.hazmat.primitives.interfaces.HashContext` that + :class:`~cryptography.hazmat.primitives.interfaces.HashContext` that uses the specified ``algorithm`` to calculate a hash-based message authentication code. @@ -150,3 +131,44 @@ A specific ``backend`` may provide one or more of these interfaces. :returns: :class:`~cryptography.hazmat.primitives.interfaces.HashContext` + + +.. class:: PBKDF2HMACBackend + + .. versionadded:: 0.2 + + A backend with methods for using PBKDF2 using HMAC as a PRF. + + .. method:: pbkdf2_hmac_supported(algorithm) + + Check if the specified ``algorithm`` is supported by this backend. + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` + provider. + + :returns: ``True`` if the specified ``algorithm`` is supported for + PBKDF2 HMAC by this backend, otherwise ``False``. + + .. method:: derive_pbkdf2_hmac(self, algorithm, length, salt, iterations, + key_material) + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` + provider. + + :param int length: The desired length of the derived key. Maximum is + (2\ :sup:`32` - 1) * ``algorithm.digest_size`` + + :param bytes salt: A salt. + + :param int iterations: The number of iterations to perform of the hash + function. This can be used to control the length of time the + operation takes. Higher numbers help mitigate brute force attacks + against derived keys. + + :param bytes key_material: The key material to use as a basis for + the derived key. This is typically a password. + + :return bytes: Derived key. + diff --git a/docs/hazmat/backends/openssl.rst b/docs/hazmat/backends/openssl.rst index 404573a3..12d2d9f6 100644 --- a/docs/hazmat/backends/openssl.rst +++ b/docs/hazmat/backends/openssl.rst @@ -7,33 +7,10 @@ The `OpenSSL`_ C library. .. data:: cryptography.hazmat.backends.openssl.backend - This is the exposed API for the OpenSSL backend. It has no public attributes. + This is the exposed API for the OpenSSL backend. It has one public attribute. -Using your own OpenSSL on Linux -------------------------------- + .. attribute:: name -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. - -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:: - - ./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. + The string name of this backend: ``"openssl"`` .. _`OpenSSL`: https://www.openssl.org/ diff --git a/docs/hazmat/bindings/openssl.rst b/docs/hazmat/bindings/openssl.rst index 373fe472..557f8c4d 100644 --- a/docs/hazmat/bindings/openssl.rst +++ b/docs/hazmat/bindings/openssl.rst @@ -22,6 +22,26 @@ These are `CFFI`_ bindings to the `OpenSSL`_ C library. This is a ``cffi`` library. It can be used to call OpenSSL functions, and access constants. + .. classmethod:: init_static_locks + + Enables the best available locking callback for OpenSSL. + See :ref:`openssl-threading`. + +.. _openssl-threading: + +Threading +--------- + +``cryptography`` enables OpenSSLs `thread safety facilities`_ in two different +ways depending on the configuration of your system. Normally the locking +callbacks provided by your Python implementation specifically for OpenSSL will +be used. However if you have linked ``cryptography`` to a different version of +OpenSSL than that used by your Python implementation we enable an alternative +locking callback. This version is implemented in Python and so may result in +lower performance in some situations. In particular parallelism is reduced +because it has to acquire the GIL whenever any lock operations occur within +OpenSSL. .. _`CFFI`: https://cffi.readthedocs.org/ .. _`OpenSSL`: https://www.openssl.org/ +.. _`thread safety facilities`: http://www.openssl.org/docs/crypto/threads.html diff --git a/docs/hazmat/primitives/index.rst b/docs/hazmat/primitives/index.rst index b115fdbc..bde07392 100644 --- a/docs/hazmat/primitives/index.rst +++ b/docs/hazmat/primitives/index.rst @@ -10,5 +10,6 @@ Primitives hmac symmetric-encryption padding + key-derivation-functions constant-time interfaces diff --git a/docs/hazmat/primitives/interfaces.rst b/docs/hazmat/primitives/interfaces.rst index edb24cd9..09a5a4ce 100644 --- a/docs/hazmat/primitives/interfaces.rst +++ b/docs/hazmat/primitives/interfaces.rst @@ -102,3 +102,175 @@ Interfaces used by the symmetric cipher modes described in Exact requirements of the nonce are described by the documentation of individual modes. + +Asymmetric Interfaces +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: RSAPrivateKey + + .. versionadded:: 0.2 + + An `RSA`_ private key. + + .. method:: public_key() + + :return: :class:`~cryptography.hazmat.primitives.interfaces.RSAPublicKey` + + An RSA public key object corresponding to the values of the private key. + + .. attribute:: modulus + + :type: int + + The public modulus. + + .. attribute:: public_exponent + + :type: int + + The public exponent. + + .. attribute:: key_length + + :type: int + + The bit length of the modulus. + + .. attribute:: p + + :type: int + + ``p``, one of the two primes composing the :attr:`modulus`. + + .. attribute:: q + + :type: int + + ``q``, one of the two primes composing the :attr:`modulus`. + + .. attribute:: d + + :type: int + + The private exponent. + + .. attribute:: n + + :type: int + + The public modulus. Alias for :attr:`modulus`. + + .. attribute:: e + + :type: int + + The public exponent. Alias for :attr:`public_exponent`. + + +.. class:: RSAPublicKey + + .. versionadded:: 0.2 + + An `RSA`_ public key. + + .. attribute:: modulus + + :type: int + + The public modulus. + + .. attribute:: key_length + + :type: int + + The bit length of the modulus. + + .. attribute:: public_exponent + + :type: int + + The public exponent. + + .. attribute:: n + + :type: int + + The public modulus. Alias for :attr:`modulus`. + + .. attribute:: e + + :type: int + + The public exponent. Alias for :attr:`public_exponent`. + + +Hash Algorithms +~~~~~~~~~~~~~~~ + +.. class:: HashAlgorithm + + .. attribute:: name + + :type: str + + The standard name for the hash algorithm, for example: ``"sha256"`` or + ``"whirlpool"``. + + .. attribute:: digest_size + + :type: int + + The size of the resulting digest in bytes. + + .. attribute:: block_size + + :type: int + + The internal block size of the hash algorithm in bytes. + + +Key Derivation Functions +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: KeyDerivationFunction + + .. versionadded:: 0.2 + + .. method:: derive(key_material) + + :param key_material bytes: The input key material. Depending on what + key derivation function you are using this + could be either random material, or a user + supplied password. + :return: The new key. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive` or + :meth:`verify` is + called more than + once. + + This generates and returns a new key from the supplied key material. + + .. method:: verify(key_material, expected_key) + + :param key_material bytes: The input key material. This is the same as + ``key_material`` in :meth:`derive`. + :param expected_key bytes: The expected result of deriving a new key, + this is the same as the return value of + :meth:`derive`. + :raises cryptography.exceptions.InvalidKey: This is raised when the + derived key does not match + the expected key. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive` or + :meth:`verify` is + called more than + once. + + This checks whether deriving a new key from the supplied + ``key_material`` generates the same key as the ``expected_key``, and + raises an exception if they do not match. This can be used for + something like checking whether a user's password attempt matches the + stored derived key. + +.. _`RSA`: http://en.wikipedia.org/wiki/RSA_(cryptosystem) diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst new file mode 100644 index 00000000..f96eae06 --- /dev/null +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -0,0 +1,125 @@ +.. hazmat:: + +Key Derivation Functions +======================== + +.. currentmodule:: cryptography.hazmat.primitives.kdf + +Key derivation functions derive bytes suitable for cryptographic operations +from passwords or other data sources using a pseudo-random function (PRF). +Different KDFs are suitable for different tasks such as: + +* Cryptographic key derivation + + Deriving a key suitable for use as input to an encryption algorithm. + Typically this means taking a password and running it through an algorithm + such as :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC` or HKDF. + This process is typically known as `key stretching`_. + +* Password storage + + When storing passwords you want to use an algorithm that is computationally + intensive. Legitimate users will only need to compute it once (for example, + taking the user's password, running it through the KDF, then comparing it + to the stored value), while attackers will need to do it billions of times. + Ideal password storage KDFs will be demanding on both computational and + memory resources. + +.. currentmodule:: cryptography.hazmat.primitives.kdf.pbkdf2 + +.. class:: PBKDF2HMAC(algorithm, length, salt, iterations, backend) + + .. versionadded:: 0.2 + + `PBKDF2`_ (Password Based Key Derivation Function 2) is typically used for + deriving a cryptographic key from a password. It may also be used for + key storage, but an alternate key storage KDF such as `scrypt`_ is generally + considered a better solution. + + This class conforms to the + :class:`~cryptography.hazmat.primitives.interfaces.KeyDerivationFunction` + interface. + + .. doctest:: + + >>> import os + >>> from cryptography.hazmat.primitives import hashes + >>> from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + >>> from cryptography.hazmat.backends import default_backend + >>> backend = default_backend() + >>> salt = os.urandom(16) + >>> # derive + >>> kdf = PBKDF2HMAC( + ... algorithm=hashes.SHA256(), + ... length=32, + ... salt=salt, + ... iterations=100000, + ... backend=backend + ... ) + >>> key = kdf.derive(b"my great password") + >>> # verify + >>> kdf = PBKDF2HMAC( + ... algorithm=hashes.SHA256(), + ... length=32, + ... salt=salt, + ... iterations=100000, + ... backend=backend + ... ) + >>> kdf.verify(b"my great password", key) + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` + provider. + :param int length: The desired length of the derived key. Maximum is + (2\ :sup:`32` - 1) * ``algorithm.digest_size``. + :param bytes salt: A salt. `NIST SP 800-132`_ recommends 128-bits or + longer. + :param int iterations: The number of iterations to perform of the hash + function. This can be used to control the length of time the operation + takes. Higher numbers help mitigate brute force attacks against derived + keys. See OWASP's `Password Storage Cheat Sheet`_ for more + detailed recommendations if you intend to use this for password storage. + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.PBKDF2HMACBackend` + provider. + + .. method:: derive(key_material) + + :param bytes key_material: The input key material. For PBKDF2 this + should be a password. + :return bytes: the derived key. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive` or + :meth:`verify` is + called more than + once. + + This generates and returns a new key from the supplied password. + + .. method:: verify(key_material, expected_key) + + :param bytes key_material: The input key material. This is the same as + ``key_material`` in :meth:`derive`. + :param bytes expected_key: The expected result of deriving a new key, + this is the same as the return value of + :meth:`derive`. + :raises cryptography.exceptions.InvalidKey: This is raised when the + derived key does not match + the expected key. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive` or + :meth:`verify` is + called more than + once. + + This checks whether deriving a new key from the supplied + ``key_material`` generates the same key as the ``expected_key``, and + raises an exception if they do not match. This can be used for + checking whether the password a user provides matches the stored derived + key. + +.. _`NIST SP 800-132`: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf +.. _`Password Storage Cheat Sheet`: https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet +.. _`PBKDF2`: http://en.wikipedia.org/wiki/PBKDF2 +.. _`scrypt`: http://en.wikipedia.org/wiki/Scrypt +.. _`key stretching`: http://en.wikipedia.org/wiki/Key_stretching diff --git a/docs/index.rst b/docs/index.rst index e17b4f9f..86cd42c6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,15 +5,16 @@ Welcome to ``cryptography`` primitives. We hope it'll be your one-stop-shop for all your cryptographic needs in Python. -Installing ----------- - +Installation +------------ You can install ``cryptography`` with ``pip``: .. code-block:: console $ pip install cryptography +See :doc:`Installation <installation>` for more information. + Why a new crypto library for Python? ------------------------------------ @@ -75,9 +76,13 @@ The ``cryptography`` open source project .. toctree:: :maxdepth: 2 + installation contributing security api-stability doing-a-release changelog community + + +.. _`pre-compiled binaries`: https://www.openssl.org/related/binaries.html diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 00000000..2206107e --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,74 @@ +Installing +========== + +You can install ``cryptography`` with ``pip``: + +.. code-block:: console + + $ pip install cryptography + +Installation Notes +================== +On Windows +---------- +If you're on Windows you'll need to make sure you have OpenSSL installed. +There are `pre-compiled binaries`_ available. If your installation is in +an unusual location set the ``LIB`` and ``INCLUDE`` environment variables +to include the corresponding locations. For example: + +.. code-block:: console + + C:\> \path\to\vcvarsall.bat x86_amd64 + C:\> set LIB=C:\OpenSSL-1.0.1f-64bit\lib;%LIB% + C:\> set INCLUDE=C:\OpenSSL-1.0.1f-64bit\include;%INCLUDE% + C:\> pip install cryptography + +Using your own OpenSSL on Linux +------------------------------- + +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. + +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: + +.. code-block:: console + + $ ./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. + +Using your own OpenSSL on OS X +------------------------------ + +To link cryptography against a custom version of OpenSSL you'll need to set +``ARCHFLAGS``, ``LDFLAGS``, and ``CFLAGS``. OpenSSL can be installed via +`Homebrew`_: + +.. code-block:: console + + $ brew install openssl + +Then install cryptography linking against the brewed version: + +.. code-block:: console + + $ env ARCHFLAGS="-arch x86_64" LDFLAGS="-L/usr/local/opt/openssl/lib" CFLAGS="-I/usr/local/opt/openssl/include" pip install cryptography + + +.. _`Homebrew`: http://brew.sh +.. _`pre-compiled binaries`: https://www.openssl.org/related/binaries.html diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 97356c24..75628ba5 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -14,6 +14,7 @@ hazmat indistinguishability introspectability invariants +iOS pickleable plaintext testability @@ -1,7 +1,8 @@ [pytest] addopts = -r s markers = - hmac: this test requires a backend providing HMACBackend cipher: this test requires a backend providing CipherBackend hash: this test requires a backend providing HashBackend + hmac: this test requires a backend providing HMACBackend + pbkdf2hmac: this test requires a backend providing PBKDF2HMACBackend supported: parametrized test requiring only_if and skip_message @@ -43,14 +43,23 @@ class cffi_build(build): """ def finalize_options(self): - from cryptography.hazmat.bindings.openssl.binding import Binding + from cryptography.hazmat.bindings.commoncrypto.binding import ( + Binding as CommonCryptoBinding + ) + from cryptography.hazmat.bindings.openssl.binding import ( + Binding as OpenSSLBinding + ) from cryptography.hazmat.primitives import constant_time, padding self.distribution.ext_modules = [ - Binding().ffi.verifier.get_extension(), + OpenSSLBinding().ffi.verifier.get_extension(), constant_time._ffi.verifier.get_extension(), padding._ffi.verifier.get_extension() ] + if CommonCryptoBinding.is_available(): + self.distribution.ext_modules.append( + CommonCryptoBinding().ffi.verifier.get_extension() + ) build.finalize_options(self) diff --git a/tests/conftest.py b/tests/conftest.py index 1d9f96ed..ecad1b23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,15 +2,18 @@ import pytest from cryptography.hazmat.backends import _ALL_BACKENDS from cryptography.hazmat.backends.interfaces import ( - HMACBackend, CipherBackend, HashBackend + HMACBackend, CipherBackend, HashBackend, PBKDF2HMACBackend ) -from .utils import check_for_iface, check_backend_support +from .utils import check_for_iface, check_backend_support, select_backends -@pytest.fixture(params=_ALL_BACKENDS) -def backend(request): - return request.param +def pytest_generate_tests(metafunc): + names = metafunc.config.getoption("--backend") + selected_backends = select_backends(names, _ALL_BACKENDS) + + if "backend" in metafunc.fixturenames: + metafunc.parametrize("backend", selected_backends) @pytest.mark.trylast @@ -18,4 +21,12 @@ def pytest_runtest_setup(item): check_for_iface("hmac", HMACBackend, item) check_for_iface("cipher", CipherBackend, item) check_for_iface("hash", HashBackend, item) + check_for_iface("pbkdf2hmac", PBKDF2HMACBackend, item) check_backend_support(item) + + +def pytest_addoption(parser): + parser.addoption( + "--backend", action="store", metavar="NAME", + help="Only run tests matching the backend NAME." + ) diff --git a/tests/hazmat/backends/test_commoncrypto.py b/tests/hazmat/backends/test_commoncrypto.py new file mode 100644 index 00000000..7cc0f72f --- /dev/null +++ b/tests/hazmat/backends/test_commoncrypto.py @@ -0,0 +1,65 @@ +# 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. + +import pytest + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, InternalError +from cryptography.hazmat.bindings.commoncrypto.binding import Binding +from cryptography.hazmat.primitives import interfaces +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.base import Cipher +from cryptography.hazmat.primitives.ciphers.modes import CBC, GCM + + +@utils.register_interface(interfaces.CipherAlgorithm) +class DummyCipher(object): + name = "dummy-cipher" + block_size = 128 + + +@pytest.mark.skipif(not Binding.is_available(), + reason="CommonCrypto not available") +class TestCommonCrypto(object): + def test_supports_cipher(self): + from cryptography.hazmat.backends.commoncrypto.backend import backend + assert backend.cipher_supported(None, None) is False + + def test_register_duplicate_cipher_adapter(self): + from cryptography.hazmat.backends.commoncrypto.backend import backend + with pytest.raises(ValueError): + backend._register_cipher_adapter( + AES, backend._lib.kCCAlgorithmAES128, + CBC, backend._lib.kCCModeCBC + ) + + def test_handle_response(self): + from cryptography.hazmat.backends.commoncrypto.backend import backend + + with pytest.raises(ValueError): + backend._check_response(backend._lib.kCCAlignmentError) + + with pytest.raises(InternalError): + backend._check_response(backend._lib.kCCMemoryFailure) + + with pytest.raises(InternalError): + backend._check_response(backend._lib.kCCDecodeError) + + def test_nonexistent_aead_cipher(self): + from cryptography.hazmat.backends.commoncrypto.backend import Backend + b = Backend() + cipher = Cipher( + DummyCipher(), GCM(b"fake_iv_here"), backend=b, + ) + with pytest.raises(UnsupportedAlgorithm): + cipher.encryptor() diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index 2a329920..f01c3f64 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -14,7 +14,7 @@ import pytest from cryptography import utils -from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.exceptions import UnsupportedAlgorithm, InternalError from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends.openssl.backend import backend, Backend from cryptography.hazmat.primitives import interfaces @@ -76,20 +76,20 @@ class TestOpenSSL(object): cipher.encryptor() def test_handle_unknown_error(self): - with pytest.raises(SystemError): + with pytest.raises(InternalError): backend._handle_error_code(0, 0, 0) - with pytest.raises(SystemError): + with pytest.raises(InternalError): backend._handle_error_code(backend._lib.ERR_LIB_EVP, 0, 0) - with pytest.raises(SystemError): + with pytest.raises(InternalError): backend._handle_error_code( backend._lib.ERR_LIB_EVP, backend._lib.EVP_F_EVP_ENCRYPTFINAL_EX, 0 ) - with pytest.raises(SystemError): + with pytest.raises(InternalError): backend._handle_error_code( backend._lib.ERR_LIB_EVP, backend._lib.EVP_F_EVP_DECRYPTFINAL_EX, diff --git a/tests/hazmat/bindings/test_openssl.py b/tests/hazmat/bindings/test_openssl.py index d1e85058..35eb7e8d 100644 --- a/tests/hazmat/bindings/test_openssl.py +++ b/tests/hazmat/bindings/test_openssl.py @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + from cryptography.hazmat.bindings.openssl.binding import Binding @@ -23,3 +25,74 @@ class TestOpenSSL(object): def test_is_available(self): assert Binding.is_available() is True + + def test_crypto_lock_init(self): + b = Binding() + b.init_static_locks() + lock_cb = b.lib.CRYPTO_get_locking_callback() + assert lock_cb != b.ffi.NULL + + def _skip_if_not_fallback_lock(self, b): + # only run this test if we are using our locking cb + original_cb = b.lib.CRYPTO_get_locking_callback() + if original_cb != b._lock_cb_handle: + pytest.skip( + "Not using the fallback Python locking callback " + "implementation. Probably because import _ssl set one" + ) + + def test_fallback_crypto_lock_via_openssl_api(self): + b = Binding() + b.init_static_locks() + + self._skip_if_not_fallback_lock(b) + + # check that the lock state changes appropriately + lock = b._locks[b.lib.CRYPTO_LOCK_SSL] + + # starts out unlocked + assert lock.acquire(False) + lock.release() + + b.lib.CRYPTO_lock( + b.lib.CRYPTO_LOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, b.ffi.NULL, 0 + ) + + # becomes locked + assert not lock.acquire(False) + + b.lib.CRYPTO_lock( + b.lib.CRYPTO_UNLOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, b.ffi.NULL, 0 + ) + + # then unlocked + assert lock.acquire(False) + lock.release() + + def test_fallback_crypto_lock_via_binding_api(self): + b = Binding() + b.init_static_locks() + + self._skip_if_not_fallback_lock(b) + + lock = b._locks[b.lib.CRYPTO_LOCK_SSL] + + with pytest.raises(RuntimeError): + b._lock_cb(0, b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + + # errors shouldnt cause locking + assert lock.acquire(False) + lock.release() + + b._lock_cb(b.lib.CRYPTO_LOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + # locked + assert not lock.acquire(False) + + b._lock_cb(b.lib.CRYPTO_UNLOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + # unlocked + assert lock.acquire(False) + lock.release() diff --git a/tests/hazmat/primitives/test_pbkdf2hmac.py b/tests/hazmat/primitives/test_pbkdf2hmac.py new file mode 100644 index 00000000..6ad225a8 --- /dev/null +++ b/tests/hazmat/primitives/test_pbkdf2hmac.py @@ -0,0 +1,69 @@ +# 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 pytest +import six + +from cryptography import utils +from cryptography.exceptions import ( + InvalidKey, UnsupportedAlgorithm, AlreadyFinalized +) +from cryptography.hazmat.primitives import hashes, interfaces +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend + + +@utils.register_interface(interfaces.HashAlgorithm) +class DummyHash(object): + name = "dummy-hash" + + +class TestPBKDF2HMAC(object): + def test_already_finalized(self): + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + kdf.derive(b"password") + with pytest.raises(AlreadyFinalized): + kdf.derive(b"password2") + + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + key = kdf.derive(b"password") + with pytest.raises(AlreadyFinalized): + kdf.verify(b"password", key) + + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + kdf.verify(b"password", key) + with pytest.raises(AlreadyFinalized): + kdf.verify(b"password", key) + + def test_unsupported_algorithm(self): + with pytest.raises(UnsupportedAlgorithm): + PBKDF2HMAC(DummyHash(), 20, b"salt", 10, default_backend()) + + def test_invalid_key(self): + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + key = kdf.derive(b"password") + + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + with pytest.raises(InvalidKey): + kdf.verify(b"password2", key) + + def test_unicode_error_with_salt(self): + with pytest.raises(TypeError): + PBKDF2HMAC(hashes.SHA1(), 20, six.u("salt"), 10, default_backend()) + + def test_unicode_error_with_key_material(self): + kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, default_backend()) + with pytest.raises(TypeError): + kdf.derive(six.u("unicode here")) diff --git a/tests/hazmat/primitives/test_pbkdf2hmac_vectors.py b/tests/hazmat/primitives/test_pbkdf2hmac_vectors.py new file mode 100644 index 00000000..cbd4cc9d --- /dev/null +++ b/tests/hazmat/primitives/test_pbkdf2hmac_vectors.py @@ -0,0 +1,37 @@ +# 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 pytest + +from cryptography.hazmat.primitives import hashes + +from .utils import generate_pbkdf2_test +from ...utils import load_nist_vectors + + +@pytest.mark.supported( + only_if=lambda backend: backend.pbkdf2_hmac_supported(hashes.SHA1()), + skip_message="Does not support SHA1 for PBKDF2HMAC", +) +@pytest.mark.pbkdf2hmac +class TestPBKDF2HMAC_SHA1(object): + test_pbkdf2_sha1 = generate_pbkdf2_test( + load_nist_vectors, + "KDF", + [ + "rfc-6070-PBKDF2-SHA1.txt", + ], + hashes.SHA1(), + ) diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 6ecc70ff..6b1d055d 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -4,6 +4,7 @@ import os import pytest from cryptography.hazmat.primitives import hashes, hmac +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.exceptions import ( AlreadyFinalized, NotYetFinalized, AlreadyUpdated, InvalidTag, @@ -141,8 +142,7 @@ def generate_hash_test(param_loader, path, file_names, hash_cls): def hash_test(backend, algorithm, params): - msg = params[0] - md = params[1] + msg, md = params m = hashes.Hash(algorithm, backend=backend) m.update(binascii.unhexlify(msg)) expected_md = md.replace(" ", "").lower().encode("ascii") @@ -206,14 +206,36 @@ def generate_hmac_test(param_loader, path, file_names, algorithm): def hmac_test(backend, algorithm, params): - msg = params[0] - md = params[1] - key = params[2] + msg, md, key = params h = hmac.HMAC(binascii.unhexlify(key), algorithm, backend=backend) h.update(binascii.unhexlify(msg)) assert h.finalize() == binascii.unhexlify(md.encode("ascii")) +def generate_pbkdf2_test(param_loader, path, file_names, algorithm): + all_params = _load_all_params(path, file_names, param_loader) + + @pytest.mark.parametrize("params", all_params) + def test_pbkdf2(self, backend, params): + pbkdf2_test(backend, algorithm, params) + return test_pbkdf2 + + +def pbkdf2_test(backend, algorithm, params): + # Password and salt can contain \0, which should be loaded as a null char. + # The NIST loader loads them as literal strings so we replace with the + # proper value. + kdf = PBKDF2HMAC( + algorithm, + int(params["length"]), + params["salt"], + int(params["iterations"]), + backend + ) + derived_key = kdf.derive(params["password"]) + assert binascii.hexlify(derived_key) == params["derived_key"] + + def generate_aead_exception_test(cipher_factory, mode_factory): def test_aead_exception(self, backend): aead_exception_test(backend, cipher_factory, mode_factory) diff --git a/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA1.txt b/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA1.txt new file mode 100644 index 00000000..b3fd03aa --- /dev/null +++ b/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA1.txt @@ -0,0 +1,56 @@ +# A.4. Test Case 4 +# Basic test case with SHA-1 + +COUNT = 4 + +Hash = SHA-1 +IKM = 0b0b0b0b0b0b0b0b0b0b0b +salt = 000102030405060708090a0b0c +info = f0f1f2f3f4f5f6f7f8f9 +L = 42 + +PRK = 9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243 +OKM = 085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896 + +# A.5. Test Case 5 +# Test with SHA-1 and longer inputs/outputs + +COUNT = 5 + +Hash = SHA-1 +IKM = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f +salt = 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf +info = b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff +L = 82 + +PRK = 8adae09a2a307059478d309b26c4115a224cfaf6 +OKM = 0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4 + +# A.6. Test Case 6 +# Test with SHA-1 and zero-length salt/info + +COUNT = 6 + +Hash = SHA-1 +IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +salt = +info = +L = 42 + +PRK = da8c8a73c7fa77288ec6f5e7c297786aa0d32d01 +OKM = 0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918 + +# A.7. Test Case 7 +# Test with SHA-1, salt not provided (defaults to HashLen zero octets), +# zero-length info + +COUNT = 7 + +Hash = SHA-1 +IKM = 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c +salt = +info = +L = 42 + +PRK = 2adccada18779e7c2077ad2eb19d3f3e731385dd +OKM = 2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48 diff --git a/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA256.txt b/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA256.txt new file mode 100644 index 00000000..9068a739 --- /dev/null +++ b/tests/hazmat/primitives/vectors/KDF/rfc-5869-HKDF-SHA256.txt @@ -0,0 +1,40 @@ +# A.1. Test Case 1 +# Basic test case with SHA-256 + +COUNT = 1 + +Hash = SHA-256 +IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +salt = 000102030405060708090a0b0c +info = f0f1f2f3f4f5f6f7f8f9 +L = 42 +PRK = 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5 +OKM = 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865 + +# A.2. Test Case 2 +# Test with SHA-256 and longer inputs/outputs + +COUNT = 2 + +Hash = SHA-256 +IKM = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f +salt = 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf +info = b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff +L = 82 + +PRK = 06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244 +OKM = b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87 + +# A.3. Test Case 3 +# Test with SHA-256 and zero-length salt/info + +COUNT = 3 + +Hash = SHA-256 +IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +salt = +info = +L = 42 + +PRK = 19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04 +OKM = 8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8 diff --git a/tests/hazmat/primitives/vectors/KDF/rfc-6070-PBKDF2-SHA1.txt b/tests/hazmat/primitives/vectors/KDF/rfc-6070-PBKDF2-SHA1.txt new file mode 100644 index 00000000..739f3f36 --- /dev/null +++ b/tests/hazmat/primitives/vectors/KDF/rfc-6070-PBKDF2-SHA1.txt @@ -0,0 +1,48 @@ +# PBKDF2 SHA1 vectors from http://www.ietf.org/rfc/rfc6070.txt + +COUNT = 0 +PASSWORD = password +SALT = salt +ITERATIONS = 1 +LENGTH = 20 +DERIVED_KEY = 0c60c80f961f0e71f3a9b524af6012062fe037a6 + + +COUNT = 1 +PASSWORD = password +SALT = salt +ITERATIONS = 2 +LENGTH = 20 +DERIVED_KEY = ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957 + + +COUNT = 2 +PASSWORD = password +SALT = salt +ITERATIONS = 4096 +LENGTH = 20 +DERIVED_KEY = 4b007901b765489abead49d926f721d065a429c1 + + +COUNT = 3 +PASSWORD = password +SALT = salt +ITERATIONS = 16777216 +LENGTH = 20 +DERIVED_KEY = eefe3d61cd4da4e4e9945b3d6ba2158c2634e984 + + +COUNT = 4 +PASSWORD = passwordPASSWORDpassword +SALT = saltSALTsaltSALTsaltSALTsaltSALTsalt +ITERATIONS = 4096 +LENGTH = 25 +DERIVED_KEY = 3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038 + + +COUNT = 5 +PASSWORD = pass\0word +SALT = sa\0lt +ITERATIONS = 4096 +LENGTH = 16 +DERIVED_KEY = 56fa6aa75548099dcc37d7f03425e0c3 diff --git a/tests/test_utils.py b/tests/test_utils.py index e3e53d63..8ecb33f9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,7 +21,7 @@ import pytest from .utils import ( load_nist_vectors, load_vectors_from_file, load_cryptrec_vectors, load_openssl_vectors, load_hash_vectors, check_for_iface, - check_backend_support + check_backend_support, select_backends ) @@ -29,6 +29,48 @@ class FakeInterface(object): pass +def test_select_one_backend(): + b1 = pretend.stub(name="b1") + b2 = pretend.stub(name="b2") + b3 = pretend.stub(name="b3") + backends = [b1, b2, b3] + name = "b2" + selected_backends = select_backends(name, backends) + assert len(selected_backends) == 1 + assert selected_backends[0] == b2 + + +def test_select_no_backend(): + b1 = pretend.stub(name="b1") + b2 = pretend.stub(name="b2") + b3 = pretend.stub(name="b3") + backends = [b1, b2, b3] + name = "back!" + with pytest.raises(ValueError): + select_backends(name, backends) + + +def test_select_backends_none(): + b1 = pretend.stub(name="b1") + b2 = pretend.stub(name="b2") + b3 = pretend.stub(name="b3") + backends = [b1, b2, b3] + name = None + selected_backends = select_backends(name, backends) + assert len(selected_backends) == 3 + + +def test_select_two_backends(): + b1 = pretend.stub(name="b1") + b2 = pretend.stub(name="b2") + b3 = pretend.stub(name="b3") + backends = [b1, b2, b3] + name = "b2 ,b1 " + selected_backends = select_backends(name, backends) + assert len(selected_backends) == 2 + assert selected_backends == [b1, b2] + + def test_check_for_iface(): item = pretend.stub(keywords=["fake_name"], funcargs={"backend": True}) with pytest.raises(pytest.skip.Exception) as exc_info: @@ -138,6 +180,25 @@ def test_load_nist_vectors(): ] +def test_load_nist_vectors_with_null_chars(): + vector_data = textwrap.dedent(""" + COUNT = 0 + KEY = thing\\0withnulls + + COUNT = 1 + KEY = 00000000000000000000000000000000 + """).splitlines() + + assert load_nist_vectors(vector_data) == [ + { + "key": b"thing\x00withnulls", + }, + { + "key": b"00000000000000000000000000000000", + }, + ] + + def test_load_cryptrec_vectors(): vector_data = textwrap.dedent(""" # Vectors taken from http://info.isl.ntt.co.jp/crypt/eng/camellia/ diff --git a/tests/utils.py b/tests/utils.py index 693a0c8f..5c0e524f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,11 +11,37 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections import os import pytest +HashVector = collections.namedtuple("HashVector", ["message", "digest"]) +KeyedHashVector = collections.namedtuple( + "KeyedHashVector", ["message", "digest", "key"] +) + + +def select_backends(names, backend_list): + if names is None: + return backend_list + split_names = [x.strip() for x in names.split(',')] + # this must be duplicated and then removed to preserve the metadata + # pytest associates. Appending backends to a new list doesn't seem to work + selected_backends = [] + for backend in backend_list: + if backend.name in split_names: + selected_backends.append(backend) + + if len(selected_backends) > 0: + return selected_backends + else: + raise ValueError( + "No backend selected. Tried to select: {0}".format(split_names) + ) + + def check_for_iface(name, iface, item): if name in item.keywords and "backend" in item.funcargs: if not isinstance(item.funcargs["backend"], iface): @@ -63,6 +89,10 @@ def load_nist_vectors(vector_data): # Build our data using a simple Key = Value format name, value = [c.strip() for c in line.split("=")] + # Some tests (PBKDF2) contain \0, which should be interpreted as a + # null character rather than literal. + value = value.replace("\\0", "\0") + # COUNT is a special token that indicates a new block of data if name.upper() == "COUNT": test_data = {} @@ -139,27 +169,23 @@ def load_hash_vectors(vector_data): if line.startswith("Len"): length = int(line.split(" = ")[1]) elif line.startswith("Key"): - """ - HMAC vectors contain a key attribute. Hash vectors do not. - """ + # HMAC vectors contain a key attribute. Hash vectors do not. key = line.split(" = ")[1].encode("ascii") elif line.startswith("Msg"): - """ - In the NIST vectors they have chosen to represent an empty - string as hex 00, which is of course not actually an empty - string. So we parse the provided length and catch this edge case. - """ + # In the NIST vectors they have chosen to represent an empty + # string as hex 00, which is of course not actually an empty + # string. So we parse the provided length and catch this edge case. msg = line.split(" = ")[1].encode("ascii") if length > 0 else b"" elif line.startswith("MD"): md = line.split(" = ")[1] # after MD is found the Msg+MD (+ potential key) tuple is complete if key is not None: - vectors.append((msg, md, key)) + vectors.append(KeyedHashVector(msg, md, key)) key = None msg = None md = None else: - vectors.append((msg, md)) + vectors.append(HashVector(msg, md)) msg = None md = None else: @@ -8,7 +8,7 @@ deps = pretend pytest commands = - coverage run --source=cryptography/,tests/ -m pytest --capture=no --strict + coverage run --source=cryptography/,tests/ -m pytest --capture=no --strict {posargs} coverage report -m [testenv:docs] @@ -28,7 +28,7 @@ commands = # Temporarily disable coverage on pypy because of performance problems with # coverage.py on pypy. [testenv:pypy] -commands = py.test --capture=no --strict +commands = py.test --capture=no --strict {posargs} [testenv:pep8] deps = flake8 |