Skip to content

Commit

Permalink
Merge pull request #145 from n1analytics/api-remove-hkdfconfig
Browse files Browse the repository at this point in the history
Remove HKDFConfig
  • Loading branch information
nbgl authored May 10, 2018
2 parents f90c5ff + db8d106 commit f6cd281
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 121 deletions.
169 changes: 66 additions & 103 deletions clkhash/key_derivation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,118 +4,82 @@
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.backends import default_backend
from future.builtins import range, zip
from future.utils import raise_from


"""
We use the block-size of SHA1 and MD5 as the default key size for HMAC
"""
DEFAULT_KEY_SIZE = 64


class HKDFconfig:
supported_hash_algos = 'SHA256', 'SHA512'

def __init__(self,
master_secret, # type: bytes
salt=None, # type: Optional[bytes]
info=None, # type: Optional[bytes]
hash_algo='SHA256' # type: str
): # type: (...) -> None
"""
The parameters for the HDKF are defined as follows:
master_secret
input keying material
salt
HKDF is defined to operate with and without random salt. This is
done to accommodate applications where a salt value is not available.
We stress, however, that the use of salt adds significantly to the
strength of HKDF, ensuring independence between different uses of the
hash function, supporting "source-independent" extraction, and
strengthening the analytical results that back the HKDF design.
Random salt differs fundamentally from the initial keying material in
two ways: it is non-secret and can be re-used.
Ideally, the salt value is a random (or pseudorandom) string of the
length HashLen. Yet, even a salt value of less quality (shorter in
size or with limited entropy) may still make a significant
contribution to the security of the output keying material.
info
While the 'info' value is optional in the definition of HKDF, it is
often of great importance in applications. Its main objective is to
bind the derived key material to application- and context-specific
information. For example, 'info' may contain a protocol number,
algorithm identifiers, user identities, etc. In particular, it may
prevent the derivation of the same keying material for different
contexts (when the same input key material (IKM) is used in such
different contexts). It may also accommodate additional inputs to
the key expansion part, if so desired (e.g., an application may want
to bind the key material to its length L, thus making L part of the
'info' field). There is one technical requirement from 'info': it
should be independent of the input key material value IKM.
hash_algo
The hash function used by HKDF for the internal HMAC calls. The
choice of hash function defines the maximum length of the output
key material. Output bytes <= 255 * hash digest size (in bytes).
:param master_secret: the input keying material for the kdf as bytes
:param salt: optional salt value (a non-secret random value) as bytes
:param info: optional context and application specific information (can be a zero-length string) as bytes
:param hash_algo: the type of hash function to be used. Either 'SHA256' or 'SHA512'.
"""
self.master_secret = self.check_is_bytes(master_secret)
self.info = self.check_is_bytes_or_none(info)
self.salt = self.check_is_bytes_or_none(salt)
if hash_algo in HKDFconfig.supported_hash_algos:
self.hash_algo = hash_algo
else:
raise ValueError('hash algorithm "{}" is not supported. Has to be one of {}'.format(hash_algo,
HKDFconfig.supported_hash_algos))

@staticmethod
def check_is_bytes(value):
# type: (Any) -> bytes
if isinstance(value, bytes):
return value
else:
raise TypeError('provided value is not of type "bytes"')

@staticmethod
def check_is_bytes_or_none(value):
# type: (Any) -> Optional[bytes]
if value is None:
return value
else:
return HKDFconfig.check_is_bytes(value)


def hkdf(hkdf_config, num_keys, key_size=DEFAULT_KEY_SIZE):
# type: (HKDFconfig, int, int) -> Tuple[bytes, ...]
_HASH_FUNCTIONS = {
'SHA256': hashes.SHA256,
'SHA512': hashes.SHA512
}

def hkdf(master_secret, # type: bytes
num_keys, # type: int
hash_algo='SHA256', # type: str
salt=None, # type: Optional[bytes]
info=None, # type: Optional[bytes]
key_size=DEFAULT_KEY_SIZE # type: int
):
# type: (...) -> Tuple[bytes, ...]
"""
Executes the HKDF key derivation function as described in rfc5869 to derive
`num_keys` keys of size `key_size` from the master_secret.
Executes the HKDF key derivation function as described in rfc5869 to
derive `num_keys` keys of size `key_size` from the master_secret.
:param hkdf_config: an HKDFconfig object containing the configuration for the HKDF.
:param master_secret: input keying material
:param num_keys: the number of keys the kdf should produce
:param hash_algo: The hash function used by HKDF for the internal
HMAC calls. The choice of hash function defines the maximum
length of the output key material. Output bytes <= 255 * hash
digest size (in bytes).
:param salt: HKDF is defined to operate with and without random
salt. This is done to accommodate applications where a salt
value is not available. We stress, however, that the use of salt
adds significantly to themstrength of HKDF, ensuring
independence between different uses of the hash function,
supporting "source-independent" extraction, and strengthening
the analytical results that back the HKDF design.
Random salt differs fundamentally from the initial keying
material in two ways: it is non-secret and can be re-used.
Ideally, the salt value is a random (or pseudorandom) string
of the length HashLen. Yet, even a salt value of less quality
(shorter in size or with limited entropy) may still make a
significant contribution to the security of the output keying
material.
:param info: While the 'info' value is optional in the definition of
HKDF, it is often of great importance in applications. Its main
objective is to bind the derived key material to application-
and context-specific information. For example, 'info' may
contain a protocol number, algorithm identifiers, user
identities, etc. In particular, it may prevent the derivation
of the same keying material for different contexts (when the
same input key material (IKM) is used in such different
contexts). It may also accommodate additional inputs to the key
expansion part, if so desired (e.g., an application may want to
bind the key material to its length L, thus making L part of the
'info' field). There is one technical requirement from 'info':
it should be independent of the input key material value IKM.
:param key_size: the size of the produced keys
:return: Derived keys
"""
hash_dict = {
'SHA256': hashes.SHA256,
'SHA512': hashes.SHA512
}
if not isinstance(hkdf_config, HKDFconfig):
raise TypeError('provided config has to be of type "HKDFconfig"')
hkdf = HKDF(algorithm=hash_dict[hkdf_config.hash_algo](), length=num_keys * key_size, salt=hkdf_config.salt,
info=hkdf_config.info, backend=default_backend())
# hkdf.derive returns a block of num_keys * key_size bytes which we divide up into num_keys chunks,
# each of size key_size
keybytes = hkdf.derive(hkdf_config.master_secret)
try:
hash_function = _HASH_FUNCTIONS[hash_algo]
except KeyError:
msg = "unsupported hash function '{}'".format(hash_algo)
raise_from(ValueError(msg), None)


hkdf = HKDF(algorithm=hash_function(),
length=num_keys * key_size,
salt=salt,
info=info,
backend=default_backend())
# hkdf.derive returns a block of num_keys * key_size bytes which we
# divide up into num_keys chunks, each of size key_size
keybytes = hkdf.derive(master_secret)
keys = tuple(keybytes[i * key_size:(i + 1) * key_size] for i in range(num_keys))
return keys

Expand Down Expand Up @@ -159,10 +123,9 @@ def generate_key_lists(master_secrets, # type: Sequence[Union[bytes
except AttributeError:
raise TypeError("provided 'master_secrets' have to be either of type bytes or strings.")
if kdf == 'HKDF':
key_lists = [hkdf(HKDFconfig(key, salt=salt, info=info,
hash_algo=hash_algo),
num_identifier,
key_size)
key_lists = [hkdf(key, num_identifier,
hash_algo=hash_algo, salt=salt,
info=info, key_size=key_size)
for key in keys]
# regroup such that we get a tuple of keys for each identifier
return tuple(zip(*key_lists))
Expand Down
4 changes: 2 additions & 2 deletions clkhash/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ class GlobalHashingProperties(object):
:param kdf_hash: The hash function to use in key derivation. The
options are 'SHA256' and 'SHA512'.
:param kdf_info: The info for key derivation. See documentation
of :class:`HKDFconfig` for details.
of :ref:`hkdf` for details.
:param kdf_salt: The salt for key derivation. See documentation
of :class:`HKDFconfig` for details.
of :ref:`hkdf` for details.
:param kdf_key_size: The size of the derived keys in bytes.
"""
def __init__(self,
Expand Down
26 changes: 10 additions & 16 deletions tests/test_key_derivation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from future.builtins import zip

from clkhash.bloomfilter import stream_bloom_filters
from clkhash.key_derivation import hkdf, generate_key_lists, DEFAULT_KEY_SIZE, HKDFconfig
from clkhash.key_derivation import hkdf, generate_key_lists, DEFAULT_KEY_SIZE
from clkhash.schema import GlobalHashingProperties, Schema
from clkhash.field_formats import FieldHashingProperties, StringSpec

Expand All @@ -13,11 +13,11 @@ class TestKeyDerivation(unittest.TestCase):
def test_kdf(self):
master_secret = 'No, I am your father'.encode()
for num_keys in (1, 10, 50):
for key_length in (2, 20):
keys = hkdf(HKDFconfig(master_secret), num_keys, key_length)
for key_size in (2, 20):
keys = hkdf(master_secret, num_keys, key_size=key_size)
self.assertEqual(len(keys), num_keys)
for key in keys:
self.assertEqual(len(key), key_length)
self.assertEqual(len(key), key_size)

def test_generate_key_lists(self):
master_secrets = ['No, I am your father'.encode(), "No... that's not true! That's impossible!".encode()]
Expand All @@ -35,8 +35,8 @@ def test_fail_generate_key_lists(self):

def test_nacl(self):
master_secret = 'No, I am your father'.encode()
keys_1 = hkdf(HKDFconfig(master_secret, salt=b'and pepper'), 5)
keys_2 = hkdf(HKDFconfig(master_secret, salt=b'and vinegar'), 5)
keys_1 = hkdf(master_secret, 5, salt=b'and pepper')
keys_2 = hkdf(master_secret, 5, salt=b'and vinegar')
for k1, k2 in zip(keys_1, keys_2):
self.assertNotEqual(k1, k2, msg='using different salts should result in different keys')

Expand Down Expand Up @@ -129,14 +129,8 @@ def test_wrong_kdf(self):
with self.assertRaises(ValueError):
generate_key_lists([b'0'], 1, kdf='breakMe')

def test_HKDFconfig_wrong_hash(self):
def test_wrong_hash_function(self):
with self.assertRaises(ValueError):
HKDFconfig(b'', hash_algo='SHA0815')

def test_HKDFconfig_wrong_type(self):
with self.assertRaises(TypeError):
HKDFconfig(42)

def test_hkdf_wrong_config_type(self):
with self.assertRaises(TypeError):
hkdf('not your type', 1)
hkdf('foo'.encode('ascii'),
3,
hash_algo='obviously_unsupported')

0 comments on commit f6cd281

Please sign in to comment.