From bde6fb52129909cf319157dba95d65fb557d5013 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 18 Oct 2013 18:08:49 -0500 Subject: Hash Saga Part 3 - API changes + SHA1 support + tests --- cryptography/bindings/openssl/api.py | 30 +++++++++++++++++++ cryptography/primitives/hashes.py | 48 ++++++++++++++++++++++++++++++ tests/primitives/test_hash_vectors.py | 35 ++++++++++++++++++++++ tests/primitives/test_hashes.py | 28 ++++++++++++++++++ tests/primitives/test_utils.py | 24 ++++++++++++++- tests/primitives/utils.py | 55 +++++++++++++++++++++++++++++++++++ 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 cryptography/primitives/hashes.py create mode 100644 tests/primitives/test_hash_vectors.py create mode 100644 tests/primitives/test_hashes.py diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py index 79ec5eea..548a9e66 100644 --- a/cryptography/bindings/openssl/api.py +++ b/cryptography/bindings/openssl/api.py @@ -139,5 +139,35 @@ class API(object): assert res != 0 return self.ffi.buffer(buf)[:outlen[0]] + def supports_hash(self, hashname): + return (self.ffi.NULL != + self.lib.EVP_get_digestbyname(hashname.encode("ascii"))) + + def create_hash_context(self, hashobject): + ctx = self.lib.EVP_MD_CTX_create() + ctx = self.ffi.gc(ctx, self.lib.EVP_MD_CTX_destroy) + evp_md = self.lib.EVP_get_digestbyname(hashobject.name.encode("ascii")) + assert evp_md != self.ffi.NULL + res = self.lib.EVP_DigestInit_ex(ctx, evp_md, self.ffi.NULL) + assert res != 0 + return ctx + + def update_hash_context(self, ctx, data): + res = self.lib.EVP_DigestUpdate(ctx, data, len(data)) + assert res != 0 + + def finalize_hash_context(self, ctx, digest_size): + buf = self.ffi.new("unsigned char[]", digest_size) + res = self.lib.EVP_DigestFinal_ex(ctx, buf, self.ffi.NULL) + assert res != 0 + return self.ffi.buffer(buf)[:digest_size] + + def copy_hash_context(self, ctx): + copied_ctx = self.lib.EVP_MD_CTX_create() + copied_ctx = self.ffi.gc(copied_ctx, self.lib.EVP_MD_CTX_destroy) + res = self.lib.EVP_MD_CTX_copy_ex(copied_ctx, ctx) + assert res != 0 + return copied_ctx + api = API() diff --git a/cryptography/primitives/hashes.py b/cryptography/primitives/hashes.py new file mode 100644 index 00000000..d74287f9 --- /dev/null +++ b/cryptography/primitives/hashes.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. + +from __future__ import absolute_import, division, print_function + +import binascii + +from cryptography.bindings import _default_api + + +class BaseHash(object): + def __init__(self, api=None, ctx=None): + if api is None: + api = _default_api + self._api = api + self._ctx = self._api.create_hash_context(self) if ctx is None else ctx + + def update(self, string): + self._api.update_hash_context(self._ctx, string) + + def copy(self): + return self.__class__(ctx=self._copy_ctx()) + + def digest(self): + return self._api.finalize_hash_context(self._copy_ctx(), + self.digest_size) + + def hexdigest(self): + return binascii.hexlify(self.digest()).decode("ascii") + + def _copy_ctx(self): + return self._api.copy_hash_context(self._ctx) + + +class SHA1(BaseHash): + name = "sha1" + digest_size = 20 + block_size = 64 diff --git a/tests/primitives/test_hash_vectors.py b/tests/primitives/test_hash_vectors.py new file mode 100644 index 00000000..4b71ad70 --- /dev/null +++ b/tests/primitives/test_hash_vectors.py @@ -0,0 +1,35 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +import os + +from cryptography.primitives import hashes + +from .utils import generate_hash_test +from ..utils import load_hash_vectors_from_file + + +class TestSHA1(object): + test_SHA1 = generate_hash_test( + load_hash_vectors_from_file, + os.path.join("NIST", "SHABYTE"), + [ + "SHA1LongMsg.rsp", + "SHA1ShortMsg.rsp", + ], + lambda api: hashes.SHA1(api=api), + only_if=lambda api: api.supports_hash("sha1"), + skip_message="Does not support SHA1", + ) diff --git a/tests/primitives/test_hashes.py b/tests/primitives/test_hashes.py new file mode 100644 index 00000000..3419b14d --- /dev/null +++ b/tests/primitives/test_hashes.py @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +from cryptography.primitives import hashes + +from .utils import generate_base_hash_test + + +class TestSHA1(object): + test_SHA1 = generate_base_hash_test( + lambda api: hashes.SHA1(api=api), + digest_size=20, + block_size=64, + only_if=lambda api: api.supports_hash("sha1"), + skip_message="Does not support SHA1", + ) diff --git a/tests/primitives/test_utils.py b/tests/primitives/test_utils.py index 4666ece7..43ec8a71 100644 --- a/tests/primitives/test_utils.py +++ b/tests/primitives/test_utils.py @@ -1,6 +1,6 @@ import pytest -from .utils import encrypt_test +from .utils import encrypt_test, hash_test, base_hash_test class TestEncryptTest(object): @@ -12,3 +12,25 @@ class TestEncryptTest(object): skip_message="message!" ) assert exc_info.value.args[0] == "message!" + + +class TestHashTest(object): + def test_skips_if_only_if_returns_false(self): + with pytest.raises(pytest.skip.Exception) as exc_info: + hash_test( + None, None, None, + only_if=lambda api: False, + skip_message="message!" + ) + assert exc_info.value.args[0] == "message!" + + +class TestBaseHashTest(object): + def test_skips_if_only_if_returns_false(self): + with pytest.raises(pytest.skip.Exception) as exc_info: + base_hash_test( + None, None, None, None, + only_if=lambda api: False, + skip_message="message!" + ) + assert exc_info.value.args[0] == "message!" diff --git a/tests/primitives/utils.py b/tests/primitives/utils.py index 3cf08c28..f301199c 100644 --- a/tests/primitives/utils.py +++ b/tests/primitives/utils.py @@ -40,3 +40,58 @@ def encrypt_test(api, cipher_factory, mode_factory, params, only_if, actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) actual_ciphertext += cipher.finalize() assert actual_ciphertext == binascii.unhexlify(ciphertext) + + +def generate_hash_test(param_loader, path, file_names, hash_factory, + only_if=lambda api: True, skip_message=None): + def test_hash(self): + for api in _ALL_APIS: + for file_name in file_names: + for params in param_loader(os.path.join(path, file_name)): + yield ( + hash_test, + api, + hash_factory, + params, + only_if, + skip_message + ) + return test_hash + + +def hash_test(api, hash_factory, params, only_if, skip_message): + if not only_if(api): + pytest.skip(skip_message) + msg = params[0] + md = params[1] + m = hash_factory(api) + m.update(binascii.unhexlify(msg)) + assert m.hexdigest() == md.replace(" ", "").lower() + + +def generate_base_hash_test(hash_factory, digest_size, block_size, + only_if=lambda api: True, skip_message=None): + def test_base_hash(self): + for api in _ALL_APIS: + yield ( + base_hash_test, + api, + hash_factory, + digest_size, + block_size, + only_if, + skip_message, + ) + return test_base_hash + + +def base_hash_test(api, hash_factory, digest_size, block_size, only_if, + skip_message): + if not only_if(api): + pytest.skip(skip_message) + m = hash_factory(api=api) + assert m.digest_size == digest_size + assert m.block_size == block_size + m_copy = m.copy() + assert m != m_copy + assert m._ctx != m_copy._ctx -- cgit v1.2.3 From bb069c2fee6460185ee435ea848d80bab2ccec6c Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 18 Oct 2013 19:51:01 -0500 Subject: remove unneeded lambdas from tests --- tests/primitives/test_hash_vectors.py | 2 +- tests/primitives/test_hashes.py | 2 +- tests/primitives/utils.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/primitives/test_hash_vectors.py b/tests/primitives/test_hash_vectors.py index 4b71ad70..8198b086 100644 --- a/tests/primitives/test_hash_vectors.py +++ b/tests/primitives/test_hash_vectors.py @@ -29,7 +29,7 @@ class TestSHA1(object): "SHA1LongMsg.rsp", "SHA1ShortMsg.rsp", ], - lambda api: hashes.SHA1(api=api), + hashes.SHA1, only_if=lambda api: api.supports_hash("sha1"), skip_message="Does not support SHA1", ) diff --git a/tests/primitives/test_hashes.py b/tests/primitives/test_hashes.py index 3419b14d..1bc2e9e9 100644 --- a/tests/primitives/test_hashes.py +++ b/tests/primitives/test_hashes.py @@ -20,7 +20,7 @@ from .utils import generate_base_hash_test class TestSHA1(object): test_SHA1 = generate_base_hash_test( - lambda api: hashes.SHA1(api=api), + hashes.SHA1, digest_size=20, block_size=64, only_if=lambda api: api.supports_hash("sha1"), diff --git a/tests/primitives/utils.py b/tests/primitives/utils.py index f301199c..0d4c0eb3 100644 --- a/tests/primitives/utils.py +++ b/tests/primitives/utils.py @@ -42,7 +42,7 @@ def encrypt_test(api, cipher_factory, mode_factory, params, only_if, assert actual_ciphertext == binascii.unhexlify(ciphertext) -def generate_hash_test(param_loader, path, file_names, hash_factory, +def generate_hash_test(param_loader, path, file_names, hash_cls, only_if=lambda api: True, skip_message=None): def test_hash(self): for api in _ALL_APIS: @@ -51,7 +51,7 @@ def generate_hash_test(param_loader, path, file_names, hash_factory, yield ( hash_test, api, - hash_factory, + hash_cls, params, only_if, skip_message @@ -59,24 +59,24 @@ def generate_hash_test(param_loader, path, file_names, hash_factory, return test_hash -def hash_test(api, hash_factory, params, only_if, skip_message): +def hash_test(api, hash_cls, params, only_if, skip_message): if not only_if(api): pytest.skip(skip_message) msg = params[0] md = params[1] - m = hash_factory(api) + m = hash_cls(api=api) m.update(binascii.unhexlify(msg)) assert m.hexdigest() == md.replace(" ", "").lower() -def generate_base_hash_test(hash_factory, digest_size, block_size, +def generate_base_hash_test(hash_cls, digest_size, block_size, only_if=lambda api: True, skip_message=None): def test_base_hash(self): for api in _ALL_APIS: yield ( base_hash_test, api, - hash_factory, + hash_cls, digest_size, block_size, only_if, @@ -85,11 +85,11 @@ def generate_base_hash_test(hash_factory, digest_size, block_size, return test_base_hash -def base_hash_test(api, hash_factory, digest_size, block_size, only_if, +def base_hash_test(api, hash_cls, digest_size, block_size, only_if, skip_message): if not only_if(api): pytest.skip(skip_message) - m = hash_factory(api=api) + m = hash_cls(api=api) assert m.digest_size == digest_size assert m.block_size == block_size m_copy = m.copy() -- cgit v1.2.3 From ba3b471e4b0f56e65acdb9d5daf64eb726d9c371 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 18 Oct 2013 20:53:04 -0500 Subject: change api.supports_hash to take a hash class rather than a str * This change means hash class names will be byte strings and we no longer need to encode to ascii on hashobject.name in create_hash_context --- cryptography/bindings/openssl/api.py | 6 +++--- cryptography/primitives/hashes.py | 2 +- tests/primitives/test_hash_vectors.py | 2 +- tests/primitives/test_hashes.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py index 548a9e66..84458367 100644 --- a/cryptography/bindings/openssl/api.py +++ b/cryptography/bindings/openssl/api.py @@ -139,14 +139,14 @@ class API(object): assert res != 0 return self.ffi.buffer(buf)[:outlen[0]] - def supports_hash(self, hashname): + def supports_hash(self, hash_cls): return (self.ffi.NULL != - self.lib.EVP_get_digestbyname(hashname.encode("ascii"))) + self.lib.EVP_get_digestbyname(hash_cls.name)) def create_hash_context(self, hashobject): ctx = self.lib.EVP_MD_CTX_create() ctx = self.ffi.gc(ctx, self.lib.EVP_MD_CTX_destroy) - evp_md = self.lib.EVP_get_digestbyname(hashobject.name.encode("ascii")) + evp_md = self.lib.EVP_get_digestbyname(hashobject.name) assert evp_md != self.ffi.NULL res = self.lib.EVP_DigestInit_ex(ctx, evp_md, self.ffi.NULL) assert res != 0 diff --git a/cryptography/primitives/hashes.py b/cryptography/primitives/hashes.py index d74287f9..f5cf135a 100644 --- a/cryptography/primitives/hashes.py +++ b/cryptography/primitives/hashes.py @@ -43,6 +43,6 @@ class BaseHash(object): class SHA1(BaseHash): - name = "sha1" + name = b"sha1" digest_size = 20 block_size = 64 diff --git a/tests/primitives/test_hash_vectors.py b/tests/primitives/test_hash_vectors.py index 8198b086..9a925e27 100644 --- a/tests/primitives/test_hash_vectors.py +++ b/tests/primitives/test_hash_vectors.py @@ -30,6 +30,6 @@ class TestSHA1(object): "SHA1ShortMsg.rsp", ], hashes.SHA1, - only_if=lambda api: api.supports_hash("sha1"), + only_if=lambda api: api.supports_hash(hashes.SHA1), skip_message="Does not support SHA1", ) diff --git a/tests/primitives/test_hashes.py b/tests/primitives/test_hashes.py index 1bc2e9e9..4ad5c89a 100644 --- a/tests/primitives/test_hashes.py +++ b/tests/primitives/test_hashes.py @@ -23,6 +23,6 @@ class TestSHA1(object): hashes.SHA1, digest_size=20, block_size=64, - only_if=lambda api: api.supports_hash("sha1"), + only_if=lambda api: api.supports_hash(hashes.SHA1), skip_message="Does not support SHA1", ) -- cgit v1.2.3 From 3b7730cf90c6a5114391d2d5a2ccc1cdb448da9e Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 18 Oct 2013 20:59:25 -0500 Subject: change name to str --- cryptography/bindings/openssl/api.py | 4 ++-- cryptography/primitives/hashes.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py index 84458367..cf3389d2 100644 --- a/cryptography/bindings/openssl/api.py +++ b/cryptography/bindings/openssl/api.py @@ -141,12 +141,12 @@ class API(object): def supports_hash(self, hash_cls): return (self.ffi.NULL != - self.lib.EVP_get_digestbyname(hash_cls.name)) + self.lib.EVP_get_digestbyname(hash_cls.name.encode("ascii"))) def create_hash_context(self, hashobject): ctx = self.lib.EVP_MD_CTX_create() ctx = self.ffi.gc(ctx, self.lib.EVP_MD_CTX_destroy) - evp_md = self.lib.EVP_get_digestbyname(hashobject.name) + evp_md = self.lib.EVP_get_digestbyname(hashobject.name.encode("ascii")) assert evp_md != self.ffi.NULL res = self.lib.EVP_DigestInit_ex(ctx, evp_md, self.ffi.NULL) assert res != 0 diff --git a/cryptography/primitives/hashes.py b/cryptography/primitives/hashes.py index f5cf135a..d74287f9 100644 --- a/cryptography/primitives/hashes.py +++ b/cryptography/primitives/hashes.py @@ -43,6 +43,6 @@ class BaseHash(object): class SHA1(BaseHash): - name = b"sha1" + name = "sha1" digest_size = 20 block_size = 64 -- cgit v1.2.3