Repository: pyca/cryptography

Issue:
RFC 5649 support
RFC 3394 (AES Key Wrap) was added a while back. I'd like to request support for RFC 5649 (AES Key Wrap with Padding), since it builds off of RFC 3394. It looks like OpenSSL handled this back in 2015:

https://rt.openssl.org/Ticket/Display.html?id=3675&user=guest&pass=guest

Is this feasible for cryptography in the not-too-distant future?

Thanks,
Peter


Discussion:
Our AES KW implementation actually doesn't use OpenSSL due to historical bugs in implementation (although now that we support 1.0.1+ only we could probably drop that code). That said, adding support this should be straight forward. Out of curiosity, can you share your use case?
Huh, I thought the AES and cipher functionality was backed in general by the OpenSSL backend. My mistake.

I'm working on adding key wrapping and key derivation support to PyKMIP and one of my requirements includes support for RFC 5649, given its improved flexibility with key sizes. RFC 3394 works as a stopgap for now, but I figured I'd ask to see if you were willing to add 5649.
Sorry, my previous statement was somewhat confusing. We do use the AES functionality from OpenSSL, we just don't use its AES keywrap APIs (we compose our own out of the AES primitive). At any rate, that's not hugely relevant in this case. I'll go ahead and tentatively put this in the next release milestone.
Ok, that makes sense regarding OpenSSL. Thanks for considering the feature.
It looks like RFC 5649 support was added in OpenSSL 1.1.0. Since we now ship manylinux1 wheels in addition to our mac and windows wheels we could just use that implementation (all our wheels are 1.1.0), but it does mean users on non-manylinux1 compatible distributions or alternative OSes will not be able to use it. Alternately we could implement it ourselves. I'll think a bit more on that, but we'll definitely need to add NIST CAVP's keywrap test vectors (the `KWP` files in http://csrc.nist.gov/groups/STM/cavp/documents/mac/kwtestvectors.zip are AESKW with padding)

Resolving patch:
diff --git a/src/cryptography/hazmat/primitives/keywrap.py b/src/cryptography/hazmat/primitives/keywrap.py
--- 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")


Test patch:
diff --git a/tests/hazmat/primitives/test_keywrap.py b/tests/hazmat/primitives/test_keywrap.py
--- a/tests/hazmat/primitives/test_keywrap.py
+++ b/tests/hazmat/primitives/test_keywrap.py
@@ -114,3 +114,68 @@ def test_unwrap_invalid_wrapped_key_length(self, backend):
         # 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
+            )