Repository: pyca/cryptography

Issue:
Check IV lengths are correct for cipher
OpenSSL can't be bothered to check these itself, because crypto is real easy and not important.



Discussion:
If openssl doesn't have an inbuilt mechanism for this, then it can't go down to api bindings...

A) Same check need to happen for a nonce?

B) This couples the mode to information of the cipher. Anyone have any opinion on whether functionality should be located in Mode classes, or in BlockCipher?

C) Any reason not to assume IV = block_size?

So we've got two directions here:

a) Put this logic in the backend, it's trivial for OpenSSL to interogate a cipher to see how long it's IV should be. This is a bit unfortunate since it means every backend needs this logic. On the other hand, it's easy to write a test that'll automatically verify this for a given backend.
b) Put the logic in each mode class. For example:

``` python
class CBC(object):
    def verify_for_algorithm(self, algorithm):
        assert len(self.iv) == algorithm.block_Size
```

I don't have a strong opinion on this.

So unfortunately recommended IV size does not necessarily equal the block size of the algorithm. For example, NIST recommendation for the IV of GCM (with AES) is 96-bit (for performance), not 128-bit. However, it actually accepts IVs of up to 2^64 - 1 bits (modulo 8). So any check would need to support a flexible range that we define on a per algorithm+mode basis. The vast majority of the time block size is fine though, so maybe it just needs to be a list of overrides?


Resolving patch:
diff --git a/cryptography/hazmat/primitives/ciphers/base.py b/cryptography/hazmat/primitives/ciphers/base.py
--- a/cryptography/hazmat/primitives/ciphers/base.py
+++ b/cryptography/hazmat/primitives/ciphers/base.py
@@ -25,6 +25,9 @@ def __init__(self, algorithm, mode, backend):
         if not isinstance(algorithm, interfaces.CipherAlgorithm):
             raise TypeError("Expected interface of interfaces.CipherAlgorithm")
 
+        if mode is not None:
+            mode.validate_for_algorithm(algorithm)
+
         self.algorithm = algorithm
         self.mode = mode
         self._backend = backend
diff --git a/cryptography/hazmat/primitives/ciphers/modes.py b/cryptography/hazmat/primitives/ciphers/modes.py
--- a/cryptography/hazmat/primitives/ciphers/modes.py
+++ b/cryptography/hazmat/primitives/ciphers/modes.py
@@ -25,11 +25,20 @@ class CBC(object):
     def __init__(self, initialization_vector):
         self.initialization_vector = initialization_vector
 
+    def validate_for_algorithm(self, algorithm):
+        if len(self.initialization_vector) * 8 != algorithm.block_size:
+            raise ValueError("Invalid iv size ({0}) for {1}".format(
+                len(self.initialization_vector), self.name
+            ))
+
 
 @utils.register_interface(interfaces.Mode)
 class ECB(object):
     name = "ECB"
 
+    def validate_for_algorithm(self, algorithm):
+        pass
+
 
 @utils.register_interface(interfaces.Mode)
 @utils.register_interface(interfaces.ModeWithInitializationVector)
@@ -39,6 +48,12 @@ class OFB(object):
     def __init__(self, initialization_vector):
         self.initialization_vector = initialization_vector
 
+    def validate_for_algorithm(self, algorithm):
+        if len(self.initialization_vector) * 8 != algorithm.block_size:
+            raise ValueError("Invalid iv size ({0}) for {1}".format(
+                len(self.initialization_vector), self.name
+            ))
+
 
 @utils.register_interface(interfaces.Mode)
 @utils.register_interface(interfaces.ModeWithInitializationVector)
@@ -48,6 +63,12 @@ class CFB(object):
     def __init__(self, initialization_vector):
         self.initialization_vector = initialization_vector
 
+    def validate_for_algorithm(self, algorithm):
+        if len(self.initialization_vector) * 8 != algorithm.block_size:
+            raise ValueError("Invalid iv size ({0}) for {1}".format(
+                len(self.initialization_vector), self.name
+            ))
+
 
 @utils.register_interface(interfaces.Mode)
 @utils.register_interface(interfaces.ModeWithNonce)
@@ -57,6 +78,12 @@ class CTR(object):
     def __init__(self, nonce):
         self.nonce = nonce
 
+    def validate_for_algorithm(self, algorithm):
+        if len(self.nonce) * 8 != algorithm.block_size:
+            raise ValueError("Invalid nonce size ({0}) for {1}".format(
+                len(self.nonce), self.name
+            ))
+
 
 @utils.register_interface(interfaces.Mode)
 @utils.register_interface(interfaces.ModeWithInitializationVector)
@@ -65,6 +92,9 @@ class GCM(object):
     name = "GCM"
 
     def __init__(self, initialization_vector, tag=None):
+        # len(initialization_vector) must in [1, 2 ** 64), but it's impossible
+        # to actually construct a bytes object that large, so we don't check
+        # for it
         if tag is not None and len(tag) < 4:
             raise ValueError(
                 "Authentication tag must be 4 bytes or longer"
@@ -72,3 +102,6 @@ def __init__(self, initialization_vector, tag=None):
 
         self.initialization_vector = initialization_vector
         self.tag = tag
+
+    def validate_for_algorithm(self, algorithm):
+        pass
diff --git a/cryptography/hazmat/primitives/interfaces.py b/cryptography/hazmat/primitives/interfaces.py
--- a/cryptography/hazmat/primitives/interfaces.py
+++ b/cryptography/hazmat/primitives/interfaces.py
@@ -47,6 +47,13 @@ def name(self):
         A string naming this mode (e.g. "ECB", "CBC").
         """
 
+    @abc.abstractmethod
+    def validate_for_algorithm(self, algorithm):
+        """
+        Checks that all the necessary invariants of this (mode, algorithm)
+        combination are met.
+        """
+
 
 class ModeWithInitializationVector(six.with_metaclass(abc.ABCMeta)):
     @abc.abstractproperty


Test patch:
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -27,6 +27,9 @@
 class DummyMode(object):
     name = "dummy-mode"
 
+    def validate_for_algorithm(self, algorithm):
+        pass
+
 
 @utils.register_interface(interfaces.CipherAlgorithm)
 class DummyCipher(object):
diff --git a/tests/hazmat/primitives/test_block.py b/tests/hazmat/primitives/test_block.py
--- a/tests/hazmat/primitives/test_block.py
+++ b/tests/hazmat/primitives/test_block.py
@@ -35,6 +35,9 @@
 class DummyMode(object):
     name = "dummy-mode"
 
+    def validate_for_algorithm(self, algorithm):
+        pass
+
 
 @utils.register_interface(interfaces.CipherAlgorithm)
 class DummyCipher(object):
@@ -152,3 +155,37 @@ class TestAEADCipherContext(object):
         algorithms.AES,
         modes.GCM,
     )
+
+
+class TestModeValidation(object):
+    def test_cbc(self, backend):
+        with pytest.raises(ValueError):
+            Cipher(
+                algorithms.AES(b"\x00" * 16),
+                modes.CBC(b"abc"),
+                backend,
+            )
+
+    def test_ofb(self, backend):
+        with pytest.raises(ValueError):
+            Cipher(
+                algorithms.AES(b"\x00" * 16),
+                modes.OFB(b"abc"),
+                backend,
+            )
+
+    def test_cfb(self, backend):
+        with pytest.raises(ValueError):
+            Cipher(
+                algorithms.AES(b"\x00" * 16),
+                modes.CFB(b"abc"),
+                backend,
+            )
+
+    def test_ctr(self, backend):
+        with pytest.raises(ValueError):
+            Cipher(
+                algorithms.AES(b"\x00" * 16),
+                modes.CTR(b"abc"),
+                backend,
+            )