diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2018-03-18 22:06:13 -0400 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2018-03-18 22:06:13 -0400 |
commit | cd6cf4aa7567ec7e870c19eeb5c200d8bf133ed9 (patch) | |
tree | 9fc45d68d425596a18165b6c82b2f7a13317a280 | |
parent | 4a41e540b20b3b37814ec1fc042ea24723eae9da (diff) | |
download | cryptography-cd6cf4aa7567ec7e870c19eeb5c200d8bf133ed9.tar.gz cryptography-cd6cf4aa7567ec7e870c19eeb5c200d8bf133ed9.tar.bz2 cryptography-cd6cf4aa7567ec7e870c19eeb5c200d8bf133ed9.zip |
implement AES KW with padding (RFC 5649) (#3880)
* implement AES KW with padding (RFC 5649)
fixes #3791
* oops, 2.2
* make sure this is the right valueerror
* more match
* make key padding easier to read
* review feedback
* review feedback
-rw-r--r-- | CHANGELOG.rst | 5 | ||||
-rw-r--r-- | docs/hazmat/primitives/keywrap.rst | 39 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/keywrap.py | 57 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_keywrap.py | 65 |
4 files changed, 166 insertions, 0 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f8924fd..912a368f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,11 @@ Changelog :meth:`~cryptography.fernet.MultiFernet.rotate`. * Fixed a memory leak in :func:`~cryptography.hazmat.primitives.asymmetric.ec.derive_private_key`. +* Added support for AES key wrapping with padding via + :func:`~cryptography.hazmat.primitives.keywrap.aes_key_wrap_with_padding` + and + :func:`~cryptography.hazmat.primitives.keywrap.aes_key_unwrap_with_padding` + . .. _v2-1-4: diff --git a/docs/hazmat/primitives/keywrap.rst b/docs/hazmat/primitives/keywrap.rst index 95cae8d7..1c15f9d1 100644 --- a/docs/hazmat/primitives/keywrap.rst +++ b/docs/hazmat/primitives/keywrap.rst @@ -50,6 +50,45 @@ protections offered by key wrapping are also offered by using authenticated :raises cryptography.hazmat.primitives.keywrap.InvalidUnwrap: This is raised if the key is not successfully unwrapped. +.. function:: aes_key_wrap_with_padding(wrapping_key, key_to_wrap, backend) + + .. versionadded:: 2.2 + + This function performs AES key wrap with padding as specified in + :rfc:`5649`. + + :param bytes wrapping_key: The wrapping key. + + :param bytes key_to_wrap: The key to wrap. + + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.CipherBackend` + instance that supports + :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`. + + :return bytes: The wrapped key as bytes. + +.. function:: aes_key_unwrap_with_padding(wrapping_key, wrapped_key, backend) + + .. versionadded:: 2.2 + + This function performs AES key unwrap with padding as specified in + :rfc:`5649`. + + :param bytes wrapping_key: The wrapping key. + + :param bytes wrapped_key: The wrapped key. + + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.CipherBackend` + instance that supports + :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`. + + :return bytes: The unwrapped key as bytes. + + :raises cryptography.hazmat.primitives.keywrap.InvalidUnwrap: This is + raised if the key is not successfully unwrapped. + Exceptions ~~~~~~~~~~ diff --git a/src/cryptography/hazmat/primitives/keywrap.py b/src/cryptography/hazmat/primitives/keywrap.py index 702a6932..3b531318 100644 --- a/src/cryptography/hazmat/primitives/keywrap.py +++ b/src/cryptography/hazmat/primitives/keywrap.py @@ -68,6 +68,63 @@ def _unwrap_core(wrapping_key, a, r, backend): return a, r +def aes_key_wrap_with_padding(wrapping_key, key_to_wrap, backend): + if len(wrapping_key) not in [16, 24, 32]: + raise ValueError("The wrapping key must be a valid AES key length") + + aiv = b"\xA6\x59\x59\xA6" + struct.pack(">i", len(key_to_wrap)) + # pad the key to wrap if necessary + pad = (8 - (len(key_to_wrap) % 8)) % 8 + key_to_wrap = key_to_wrap + b"\x00" * pad + if len(key_to_wrap) == 8: + # RFC 5649 - 4.1 - exactly 8 octets after padding + encryptor = Cipher(AES(wrapping_key), ECB(), backend).encryptor() + b = encryptor.update(aiv + key_to_wrap) + assert encryptor.finalize() == b"" + return b + else: + r = [key_to_wrap[i:i + 8] for i in range(0, len(key_to_wrap), 8)] + return _wrap_core(wrapping_key, aiv, r, backend) + + +def aes_key_unwrap_with_padding(wrapping_key, wrapped_key, backend): + if len(wrapped_key) < 16: + raise ValueError("Must be at least 16 bytes") + + if len(wrapping_key) not in [16, 24, 32]: + raise ValueError("The wrapping key must be a valid AES key length") + + if len(wrapped_key) == 16: + # RFC 5649 - 4.2 - exactly two 64-bit blocks + decryptor = Cipher(AES(wrapping_key), ECB(), backend).decryptor() + b = decryptor.update(wrapped_key) + assert decryptor.finalize() == b"" + a = b[:8] + data = b[8:] + n = 1 + else: + r = [wrapped_key[i:i + 8] for i in range(0, len(wrapped_key), 8)] + encrypted_aiv = r.pop(0) + n = len(r) + a, r = _unwrap_core(wrapping_key, encrypted_aiv, r, backend) + data = b"".join(r) + + # 1) Check that MSB(32,A) = A65959A6. + # 2) Check that 8*(n-1) < LSB(32,A) <= 8*n. If so, let + # MLI = LSB(32,A). + # 3) Let b = (8*n)-MLI, and then check that the rightmost b octets of + # the output data are zero. + (mli,) = struct.unpack(">I", a[4:]) + b = (8 * n) - mli + if ( + not bytes_eq(a[:4], b"\xa6\x59\x59\xa6") or not + 8 * (n - 1) < mli <= 8 * n or not bytes_eq(data[-b:], b"\x00" * b) + ): + raise InvalidUnwrap() + + return data[:-b] + + def aes_key_unwrap(wrapping_key, wrapped_key, backend): if len(wrapped_key) < 24: raise ValueError("Must be at least 24 bytes") diff --git a/tests/hazmat/primitives/test_keywrap.py b/tests/hazmat/primitives/test_keywrap.py index f41baedb..8311c2a4 100644 --- a/tests/hazmat/primitives/test_keywrap.py +++ b/tests/hazmat/primitives/test_keywrap.py @@ -114,3 +114,68 @@ class TestAESKeyWrap(object): # Keys to unwrap must be a multiple of 8 bytes with pytest.raises(ValueError): keywrap.aes_key_unwrap(b"sixteen_byte_key", b"\x00" * 27, backend) + + +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES(b"\x00" * 16), modes.ECB() + ), + skip_message="Does not support AES key wrap (RFC 5649) because AES-ECB" + " is unsupported", +) +@pytest.mark.requires_backend_interface(interface=CipherBackend) +class TestAESKeyWrapWithPadding(object): + @pytest.mark.parametrize( + "params", + _load_all_params( + os.path.join("keywrap", "kwtestvectors"), + ["KWP_AE_128.txt", "KWP_AE_192.txt", "KWP_AE_256.txt"], + load_nist_vectors + ) + ) + def test_wrap(self, backend, params): + wrapping_key = binascii.unhexlify(params["k"]) + key_to_wrap = binascii.unhexlify(params["p"]) + wrapped_key = keywrap.aes_key_wrap_with_padding( + wrapping_key, key_to_wrap, backend + ) + assert params["c"] == binascii.hexlify(wrapped_key) + + @pytest.mark.parametrize( + "params", + _load_all_params( + os.path.join("keywrap", "kwtestvectors"), + ["KWP_AD_128.txt", "KWP_AD_192.txt", "KWP_AD_256.txt"], + load_nist_vectors + ) + ) + def test_unwrap(self, backend, params): + wrapping_key = binascii.unhexlify(params["k"]) + wrapped_key = binascii.unhexlify(params["c"]) + if params.get("fail") is True: + with pytest.raises(keywrap.InvalidUnwrap): + keywrap.aes_key_unwrap_with_padding( + wrapping_key, wrapped_key, backend + ) + else: + unwrapped_key = keywrap.aes_key_unwrap_with_padding( + wrapping_key, wrapped_key, backend + ) + assert params["p"] == binascii.hexlify(unwrapped_key) + + def test_unwrap_invalid_wrapped_key_length(self, backend): + # Keys to unwrap must be at least 16 bytes + with pytest.raises(ValueError, match='Must be at least 16 bytes'): + keywrap.aes_key_unwrap_with_padding( + b"sixteen_byte_key", b"\x00" * 15, backend + ) + + def test_wrap_invalid_key_length(self, backend): + with pytest.raises(ValueError, match='must be a valid AES key length'): + keywrap.aes_key_wrap_with_padding(b"badkey", b"\x00", backend) + + def test_unwrap_invalid_key_length(self, backend): + with pytest.raises(ValueError, match='must be a valid AES key length'): + keywrap.aes_key_unwrap_with_padding( + b"badkey", b"\x00" * 16, backend + ) |