Repository: pyca/cryptography

Issue:
aes keywrap with padding does properly handle key to wrap length % 8 == 0
@SalusaSecondus and I found a bug in the new `aes_key_unwrap_with_padding` for wrapped keys where the plaintext is multiples of 8 bytes. Strangely, it looks like the NIST test vectors used do include cases like this, but they are passing.

The problem appears to be with the padding check[1], where if `mli` is a multiple of 8, `b` is 0, making the final check `not bytes_eq(data[-b:], b"\x00" * b)` becomes `not bytes_eq(data, b"")`

[1] https://github.com/pyca/cryptography/blob/master/src/cryptography/hazmat/primitives/keywrap.py#L120-L121

## Results of testing on Amazon Linux 2017.09.1

https://gist.github.com/mattsb42-aws/0f535c5827b96022879bc169897cf3cf

## To duplicate
```
#Amazon Linux does not have 3.6 by default
#sudo yum install python36

virtualenv test_keywrap -p python3.6
source test_keywrap/bin/activate
pip install cryptography==2.2 pytest

echo "
import os

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import keywrap
import pytest

def test_unpadded():
    key = os.urandom(32)
    key_to_wrap = os.urandom(32)

    wrapped_key = keywrap.aes_key_wrap(wrapping_key=key, key_to_wrap=key_to_wrap, backend=default_backend())
    assert keywrap.aes_key_unwrap(wrapping_key=key, wrapped_key=wrapped_key, backend=default_backend()) == key_to_wrap

@pytest.mark.parametrize('key_to_wrap_length', (11, 16, 32, 72, 99))
def test_padded_with_padding(key_to_wrap_length):
    key = os.urandom(32)
    key_to_wrap = os.urandom(key_to_wrap_length)

    wrapped_key_with_padding = keywrap.aes_key_wrap_with_padding(wrapping_key=key, key_to_wrap=key_to_wrap, backend=default_backend())
    print(wrapped_key_with_padding)
    assert keywrap.aes_key_unwrap_with_padding(wrapping_key=key, wrapped_key=wrapped_key_with_padding, backend=default_backend()) == key_to_wrap

" > test_this.py

pytest test_this.py -l -vv

pip freeze

deactivate
```




Discussion:
Ah crap.

How are we failing? Is it an exception on unwrap, or are we wrapping this incorrectly, or unwrapping them incorrectly?
I guess from your description we raise an `InvalidUnwrap` exception?
Sorry, I meant to add the test results.

https://gist.github.com/mattsb42-aws/0f535c5827b96022879bc169897cf3cf
Ok, of all the bad outcomes, this is the least bad one.

I'd still like to have a fix out ASAP.
Plaintext length is never actually a multiple of 8 bytes in the vectors. NIST provides the following lengths: 1 byte, 9 bytes, 4 bytes, 12 bytes, and 101 bytes. They list them by bit length in the files though so it's kind of confusing.

We'll be adding some n % 8 == 0 vectors I guess...
This line[1] seemed to indicate that there were vectors with 72-byte plaintexts, but looking through the vectors following it, I'm not actually seeing any that are...

[1] https://github.com/pyca/cryptography/blob/master/vectors/cryptography_vectors/keywrap/kwtestvectors/KWP_AD_128.txt#L509
...nvm, reading fail; 72 bits == 9 bytes, which is what you said
Yeah those are 72-bit, so 9 byte 😢 
@mattsb42-aws I don't suppose you know of a source of vectors we can use to test the fix here?

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
@@ -118,11 +118,16 @@ def aes_key_unwrap_with_padding(wrapping_key, wrapped_key, backend):
     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)
+        8 * (n - 1) < mli <= 8 * n or (
+            b != 0 and not bytes_eq(data[-b:], b"\x00" * b)
+        )
     ):
         raise InvalidUnwrap()
 
-    return data[:-b]
+    if b == 0:
+        return data
+    else:
+        return data[:-b]
 
 
 def aes_key_unwrap(wrapping_key, wrapped_key, backend):


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
@@ -141,6 +141,18 @@ def test_wrap(self, backend, params):
         )
         assert params["c"] == binascii.hexlify(wrapped_key)
 
+    @pytest.mark.parametrize(
+        "params",
+        _load_all_params("keywrap", ["kwp_botan.txt"], load_nist_vectors)
+    )
+    def test_wrap_additional_vectors(self, backend, params):
+        wrapping_key = binascii.unhexlify(params["key"])
+        key_to_wrap = binascii.unhexlify(params["input"])
+        wrapped_key = keywrap.aes_key_wrap_with_padding(
+            wrapping_key, key_to_wrap, backend
+        )
+        assert wrapped_key == binascii.unhexlify(params["output"])
+
     @pytest.mark.parametrize(
         "params",
         _load_all_params(
@@ -163,6 +175,18 @@ def test_unwrap(self, backend, params):
             )
             assert params["p"] == binascii.hexlify(unwrapped_key)
 
+    @pytest.mark.parametrize(
+        "params",
+        _load_all_params("keywrap", ["kwp_botan.txt"], load_nist_vectors)
+    )
+    def test_unwrap_additional_vectors(self, backend, params):
+        wrapping_key = binascii.unhexlify(params["key"])
+        wrapped_key = binascii.unhexlify(params["output"])
+        unwrapped_key = keywrap.aes_key_unwrap_with_padding(
+            wrapping_key, wrapped_key, backend
+        )
+        assert unwrapped_key == binascii.unhexlify(params["input"])
+
     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'):