diff options
-rw-r--r-- | docs/hazmat/primitives/key-derivation-functions.rst | 187 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/kdf/concatkdf.py | 125 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_concatkdf.py | 251 |
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) |