aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-12 11:17:25 -0500
committerPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-12 11:17:25 -0500
commit600a78fc34643a8d7ee65afad749acf67edfaecc (patch)
tree72d2dedd449a5c6943254a3231bf07b6375d8050
parent6e040cb7deb3ed16963f5185eeff7b44d7e8fac3 (diff)
parent8a690fba1f5742be02da490d1fa528fdcc1c9710 (diff)
downloadcryptography-600a78fc34643a8d7ee65afad749acf67edfaecc.tar.gz
cryptography-600a78fc34643a8d7ee65afad749acf67edfaecc.tar.bz2
cryptography-600a78fc34643a8d7ee65afad749acf67edfaecc.zip
Merge pull request #1898 from simo5/ConcatKDF
Add ConcatKDF from NIST SP 800-56A
-rw-r--r--docs/hazmat/primitives/key-derivation-functions.rst187
-rw-r--r--src/cryptography/hazmat/primitives/kdf/concatkdf.py125
-rw-r--r--tests/hazmat/primitives/test_concatkdf.py251
3 files changed, 563 insertions, 0 deletions
diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst
index 78d40315..7def2a22 100644
--- a/docs/hazmat/primitives/key-derivation-functions.rst
+++ b/docs/hazmat/primitives/key-derivation-functions.rst
@@ -324,6 +324,192 @@ Different KDFs are suitable for different tasks such as:
``key_material`` generates the same key as the ``expected_key``, and
raises an exception if they do not match.
+.. currentmodule:: cryptography.hazmat.primitives.kdf.concatkdf
+
+.. class:: ConcatKDFHash(algorithm, length, otherinfo, backend)
+
+ .. versionadded:: 1.0
+
+ ConcatKDFHash (Concatenation Key Derivation Function) is defined by the
+ NIST Special Publication `NIST SP 800-56Ar2`_ document, to be used to
+ derive keys for use after a Key Exchange negotiation operation.
+
+ .. warning::
+
+ ConcatKDFHash should not be used for password storage.
+
+ .. doctest::
+
+ >>> import os
+ >>> from cryptography.hazmat.primitives import hashes
+ >>> from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
+ >>> from cryptography.hazmat.backends import default_backend
+ >>> backend = default_backend()
+ >>> salt = os.urandom(16)
+ >>> otherinfo = b"concatkdf-example"
+ >>> ckdf = ConcatKDFHash(
+ ... algorithm=hashes.SHA256(),
+ ... length=256,
+ ... otherinfo=otherinfo,
+ ... backend=backend
+ ... )
+ >>> key = ckdf.derive(b"input key")
+ >>> ckdf = ConcatKDFHash(
+ ... algorithm=hashes.SHA256(),
+ ... length=256,
+ ... otherinfo=otherinfo,
+ ... backend=backend
+ ... )
+ >>> ckdf.verify(b"input key", key)
+
+ :param algorithm: An instance of a
+ :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
+ provider
+
+ :param int length: The desired length of the derived key in bytes.
+ Maximum is ``hashlen * (2^32 -1)``.
+
+ :param bytes otherinfo: Application specific context information.
+ If ``None`` is explicitly passed an empty byte string will be used.
+
+ :param backend: A
+ :class:`~cryptography.hazmat.backends.interfaces.HashBackend`
+ provider.
+
+ :raises cryptography.exceptions.UnsupportedAlgorithm: This is raised
+ if the provided ``backend`` does not implement
+ :class:`~cryptography.hazmat.backends.interfaces.HashBackend`
+
+ :raises TypeError: This exception is raised if ``otherinfo`` is not
+ ``bytes``.
+
+ .. method:: derive(key_material)
+
+ :param bytes key_material: The input key material.
+ :return bytes: The derived key.
+ :raises TypeError: This exception is raised if ``key_material`` is
+ not ``bytes``.
+
+ Derives a new key from the input key material by performing both the
+ extract and expand operations.
+
+ .. 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.
+
+
+.. class:: ConcatKDFHMAC(algorithm, length, salt, otherinfo, backend)
+
+ .. versionadded:: 1.0
+
+ Similar to ConcatKFDHash but uses an HMAC function instead.
+
+ .. warning::
+
+ ConcatKDFHMAC should not be used for password storage.
+
+ .. doctest::
+
+ >>> import os
+ >>> from cryptography.hazmat.primitives import hashes
+ >>> from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHMAC
+ >>> from cryptography.hazmat.backends import default_backend
+ >>> backend = default_backend()
+ >>> salt = os.urandom(16)
+ >>> otherinfo = b"concatkdf-example"
+ >>> ckdf = ConcatKDFHMAC(
+ ... algorithm=hashes.SHA256(),
+ ... length=256,
+ ... salt=salt,
+ ... otherinfo=otherinfo,
+ ... backend=backend
+ ... )
+ >>> key = ckdf.derive(b"input key")
+ >>> ckdf = ConcatKDFHMAC(
+ ... algorithm=hashes.SHA256(),
+ ... length=256,
+ ... salt=salt,
+ ... otherinfo=otherinfo,
+ ... backend=backend
+ ... )
+ >>> ckdf.verify(b"input key", key)
+
+ :param algorithm: An instance of a
+ :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
+ provider
+
+ :param int length: The desired length of the derived key in bytes. Maximum
+ is ``hashlen * (2^32 -1)``.
+
+ :param bytes salt: A salt. Randomizes the KDF's output. Optional, but
+ highly recommended. Ideally as many bits of entropy as the security
+ level of the hash: often that means cryptographically random and as
+ long as the hash output. Does not have to be secret, but may cause
+ stronger security guarantees if secret; If ``None`` is explicitly
+ passed a default salt of ``algorithm.block_size`` null bytes will be
+ used.
+
+ :param bytes otherinfo: Application specific context information.
+ If ``None`` is explicitly passed an empty byte string will be used.
+
+ :param backend: A
+ :class:`~cryptography.hazmat.backends.interfaces.HMACBackend`
+ provider.
+
+ :raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if the
+ provided ``backend`` does not implement
+ :class:`~cryptography.hazmat.backends.interfaces.HMACBackend`
+
+ :raises TypeError: This exception is raised if ``salt`` or ``otherinfo``
+ is not ``bytes``.
+
+ .. method:: derive(key_material)
+
+ :param bytes key_material: The input key material.
+ :return bytes: The derived key.
+ :raises TypeError: This exception is raised if ``key_material`` is not
+ ``bytes``.
+
+ Derives a new key from the input key material by performing both the
+ extract and expand operations.
+
+ .. 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.
+
+
Interface
~~~~~~~~~
@@ -372,6 +558,7 @@ Interface
.. _`NIST SP 800-132`: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
+.. _`NIST SP 800-56Ar2`: http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf
.. _`Password Storage Cheat Sheet`: https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet
.. _`PBKDF2`: https://en.wikipedia.org/wiki/PBKDF2
.. _`scrypt`: https://en.wikipedia.org/wiki/Scrypt
diff --git a/src/cryptography/hazmat/primitives/kdf/concatkdf.py b/src/cryptography/hazmat/primitives/kdf/concatkdf.py
new file mode 100644
index 00000000..c6399e4f
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/kdf/concatkdf.py
@@ -0,0 +1,125 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import struct
+
+from cryptography import utils
+from cryptography.exceptions import (
+ AlreadyFinalized, InvalidKey, UnsupportedAlgorithm, _Reasons
+)
+from cryptography.hazmat.backends.interfaces import HMACBackend
+from cryptography.hazmat.backends.interfaces import HashBackend
+from cryptography.hazmat.primitives import constant_time, hashes, hmac
+from cryptography.hazmat.primitives.kdf import KeyDerivationFunction
+
+
+def _int_to_u32be(n):
+ return struct.pack('>I', n)
+
+
+def _common_args_checks(algorithm, length, otherinfo):
+ max_length = algorithm.digest_size * (2 ** 32 - 1)
+ if length > max_length:
+ raise ValueError(
+ "Can not derive keys larger than {0} bits.".format(
+ max_length
+ ))
+ if not (otherinfo is None or isinstance(otherinfo, bytes)):
+ raise TypeError("otherinfo must be bytes.")
+
+
+def _concatkdf_derive(key_material, length, auxfn, otherinfo):
+ if not isinstance(key_material, bytes):
+ raise TypeError("key_material must be bytes.")
+
+ output = [b""]
+ outlen = 0
+ counter = 1
+
+ while (length > outlen):
+ h = auxfn()
+ h.update(_int_to_u32be(counter))
+ h.update(key_material)
+ h.update(otherinfo)
+ output.append(h.finalize())
+ outlen += len(output[-1])
+ counter += 1
+
+ return b"".join(output)[:length]
+
+
+@utils.register_interface(KeyDerivationFunction)
+class ConcatKDFHash(object):
+ def __init__(self, algorithm, length, otherinfo, backend):
+
+ _common_args_checks(algorithm, length, otherinfo)
+ self._algorithm = algorithm
+ self._length = length
+ self._otherinfo = otherinfo
+ if self._otherinfo is None:
+ self._otherinfo = b""
+
+ if not isinstance(backend, HashBackend):
+ raise UnsupportedAlgorithm(
+ "Backend object does not implement HashBackend.",
+ _Reasons.BACKEND_MISSING_INTERFACE
+ )
+ self._backend = backend
+ self._used = False
+
+ def _hash(self):
+ return hashes.Hash(self._algorithm, self._backend)
+
+ def derive(self, key_material):
+ if self._used:
+ raise AlreadyFinalized
+ self._used = True
+ return _concatkdf_derive(key_material, self._length,
+ self._hash, self._otherinfo)
+
+ def verify(self, key_material, expected_key):
+ if not constant_time.bytes_eq(self.derive(key_material), expected_key):
+ raise InvalidKey
+
+
+@utils.register_interface(KeyDerivationFunction)
+class ConcatKDFHMAC(object):
+ def __init__(self, algorithm, length, salt, otherinfo, backend):
+
+ _common_args_checks(algorithm, length, otherinfo)
+ self._algorithm = algorithm
+ self._length = length
+ self._otherinfo = otherinfo
+ if self._otherinfo is None:
+ self._otherinfo = b""
+
+ if not (salt is None or isinstance(salt, bytes)):
+ raise TypeError("salt must be bytes.")
+ if salt is None:
+ salt = b"\x00" * algorithm.block_size
+ self._salt = salt
+
+ if not isinstance(backend, HMACBackend):
+ raise UnsupportedAlgorithm(
+ "Backend object does not implement HMACBackend.",
+ _Reasons.BACKEND_MISSING_INTERFACE
+ )
+ self._backend = backend
+ self._used = False
+
+ def _hmac(self):
+ return hmac.HMAC(self._salt, self._algorithm, self._backend)
+
+ def derive(self, key_material):
+ if self._used:
+ raise AlreadyFinalized
+ self._used = True
+ return _concatkdf_derive(key_material, self._length,
+ self._hmac, self._otherinfo)
+
+ def verify(self, key_material, expected_key):
+ if not constant_time.bytes_eq(self.derive(key_material), expected_key):
+ raise InvalidKey
diff --git a/tests/hazmat/primitives/test_concatkdf.py b/tests/hazmat/primitives/test_concatkdf.py
new file mode 100644
index 00000000..27e5460e
--- /dev/null
+++ b/tests/hazmat/primitives/test_concatkdf.py
@@ -0,0 +1,251 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+
+import pytest
+
+from cryptography.exceptions import (
+ AlreadyFinalized, InvalidKey, _Reasons
+)
+from cryptography.hazmat.backends.interfaces import HMACBackend
+from cryptography.hazmat.backends.interfaces import HashBackend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHMAC
+from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
+
+from ...utils import raises_unsupported_algorithm
+
+
+@pytest.mark.requires_backend_interface(interface=HashBackend)
+class TestConcatKDFHash(object):
+ def test_length_limit(self, backend):
+ big_length = hashes.SHA256().digest_size * (2 ** 32 - 1) + 1
+
+ with pytest.raises(ValueError):
+ ConcatKDFHash(hashes.SHA256(), big_length, None, backend)
+
+ def test_already_finalized(self, backend):
+ ckdf = ConcatKDFHash(hashes.SHA256(), 16, None, backend)
+
+ ckdf.derive(b"\x01" * 16)
+
+ with pytest.raises(AlreadyFinalized):
+ ckdf.derive(b"\x02" * 16)
+
+ def test_derive(self, backend):
+ prk = binascii.unhexlify(
+ b"52169af5c485dcc2321eb8d26d5efa21fb9b93c98e38412ee2484cf14f0d0d23"
+ )
+
+ okm = binascii.unhexlify(b"1c3bc9e7c4547c5191c0d478cccaed55")
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e53728157e634612c12d6d5223e204aeea4341565369647bd184bcd2"
+ b"46f72971f292badaa2fe4124612cba"
+ )
+
+ ckdf = ConcatKDFHash(hashes.SHA256(), 16, oinfo, backend)
+
+ assert ckdf.derive(prk) == okm
+
+ def test_verify(self, backend):
+ prk = binascii.unhexlify(
+ b"52169af5c485dcc2321eb8d26d5efa21fb9b93c98e38412ee2484cf14f0d0d23"
+ )
+
+ okm = binascii.unhexlify(b"1c3bc9e7c4547c5191c0d478cccaed55")
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e53728157e634612c12d6d5223e204aeea4341565369647bd184bcd2"
+ b"46f72971f292badaa2fe4124612cba"
+ )
+
+ ckdf = ConcatKDFHash(hashes.SHA256(), 16, oinfo, backend)
+
+ assert ckdf.verify(prk, okm) is None
+
+ def test_invalid_verify(self, backend):
+ prk = binascii.unhexlify(
+ b"52169af5c485dcc2321eb8d26d5efa21fb9b93c98e38412ee2484cf14f0d0d23"
+ )
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e53728157e634612c12d6d5223e204aeea4341565369647bd184bcd2"
+ b"46f72971f292badaa2fe4124612cba"
+ )
+
+ ckdf = ConcatKDFHash(hashes.SHA256(), 16, oinfo, backend)
+
+ with pytest.raises(InvalidKey):
+ ckdf.verify(prk, b"wrong key")
+
+ def test_unicode_typeerror(self, backend):
+ with pytest.raises(TypeError):
+ ConcatKDFHash(
+ hashes.SHA256(),
+ 16,
+ otherinfo=u"foo",
+ backend=backend
+ )
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHash(
+ hashes.SHA256(),
+ 16,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.derive(u"foo")
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHash(
+ hashes.SHA256(),
+ 16,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.verify(u"foo", b"bar")
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHash(
+ hashes.SHA256(),
+ 16,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.verify(b"foo", u"bar")
+
+
+@pytest.mark.requires_backend_interface(interface=HMACBackend)
+class TestConcatKDFHMAC(object):
+ def test_length_limit(self, backend):
+ big_length = hashes.SHA256().digest_size * (2 ** 32 - 1) + 1
+
+ with pytest.raises(ValueError):
+ ConcatKDFHMAC(hashes.SHA256(), big_length, None, None, backend)
+
+ def test_already_finalized(self, backend):
+ ckdf = ConcatKDFHMAC(hashes.SHA256(), 16, None, None, backend)
+
+ ckdf.derive(b"\x01" * 16)
+
+ with pytest.raises(AlreadyFinalized):
+ ckdf.derive(b"\x02" * 16)
+
+ def test_derive(self, backend):
+ prk = binascii.unhexlify(
+ b"013951627c1dea63ea2d7702dd24e963eef5faac6b4af7e4"
+ b"b831cde499dff1ce45f6179f741c728aa733583b02409208"
+ b"8f0af7fce1d045edbc5790931e8d5ca79c73"
+ )
+
+ okm = binascii.unhexlify(b"64ce901db10d558661f10b6836a122a7"
+ b"605323ce2f39bf27eaaac8b34cf89f2f")
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e55e600be5f367e0e8a465f4bf2704db00c9325c"
+ b"9fbd216d12b49160b2ae5157650f43415653696421e68e"
+ )
+
+ ckdf = ConcatKDFHMAC(hashes.SHA512(), 32, None, oinfo, backend)
+
+ assert ckdf.derive(prk) == okm
+
+ def test_verify(self, backend):
+ prk = binascii.unhexlify(
+ b"013951627c1dea63ea2d7702dd24e963eef5faac6b4af7e4"
+ b"b831cde499dff1ce45f6179f741c728aa733583b02409208"
+ b"8f0af7fce1d045edbc5790931e8d5ca79c73"
+ )
+
+ okm = binascii.unhexlify(b"64ce901db10d558661f10b6836a122a7"
+ b"605323ce2f39bf27eaaac8b34cf89f2f")
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e55e600be5f367e0e8a465f4bf2704db00c9325c"
+ b"9fbd216d12b49160b2ae5157650f43415653696421e68e"
+ )
+
+ ckdf = ConcatKDFHMAC(hashes.SHA512(), 32, None, oinfo, backend)
+
+ assert ckdf.verify(prk, okm) is None
+
+ def test_invalid_verify(self, backend):
+ prk = binascii.unhexlify(
+ b"013951627c1dea63ea2d7702dd24e963eef5faac6b4af7e4"
+ b"b831cde499dff1ce45f6179f741c728aa733583b02409208"
+ b"8f0af7fce1d045edbc5790931e8d5ca79c73"
+ )
+
+ oinfo = binascii.unhexlify(
+ b"a1b2c3d4e55e600be5f367e0e8a465f4bf2704db00c9325c"
+ b"9fbd216d12b49160b2ae5157650f43415653696421e68e"
+ )
+
+ ckdf = ConcatKDFHMAC(hashes.SHA512(), 32, None, oinfo, backend)
+
+ with pytest.raises(InvalidKey):
+ ckdf.verify(prk, b"wrong key")
+
+ def test_unicode_typeerror(self, backend):
+ with pytest.raises(TypeError):
+ ConcatKDFHMAC(
+ hashes.SHA256(),
+ 16, salt=u"foo",
+ otherinfo=None,
+ backend=backend
+ )
+
+ with pytest.raises(TypeError):
+ ConcatKDFHMAC(
+ hashes.SHA256(),
+ 16, salt=None,
+ otherinfo=u"foo",
+ backend=backend
+ )
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHMAC(
+ hashes.SHA256(),
+ 16, salt=None,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.derive(u"foo")
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHMAC(
+ hashes.SHA256(),
+ 16, salt=None,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.verify(u"foo", b"bar")
+
+ with pytest.raises(TypeError):
+ ckdf = ConcatKDFHMAC(
+ hashes.SHA256(),
+ 16, salt=None,
+ otherinfo=None,
+ backend=backend
+ )
+
+ ckdf.verify(b"foo", u"bar")
+
+
+def test_invalid_backend():
+ pretend_backend = object()
+
+ with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE):
+ ConcatKDFHash(hashes.SHA256(), 16, None, pretend_backend)
+ with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE):
+ ConcatKDFHMAC(hashes.SHA256(), 16, None, None, pretend_backend)