Repository: pyca/cryptography

Issue:
Make it harder to allow truncation on a GCM tag
`GCM()` should take a `min_tag_length` parameter, which defaults to `GCM.MAX_TAG_LENGTH`, then we can make it error out on truncated tags by default, while still allowing truncation for use cases that need it.

This is technically a backwards compatibility break, but I argue that it should be allowed as a security hardening measure. Thoughts?



Discussion:
I'm :+1: on this. It allows the bad behavior while forcing it to be explicit.

We could comply with our backwards compatibility policy if we're willing to add this option but only emit deprecation warnings. I'm neutral on whether that's worth it.


Resolving patch:
diff --git a/cryptography/__about__.py b/cryptography/__about__.py
--- a/cryptography/__about__.py
+++ b/cryptography/__about__.py
@@ -28,4 +28,4 @@
 __email__ = "cryptography-dev@python.org"
 
 __license__ = "Apache License, Version 2.0"
-__copyright__ = "Copyright 2013-2014 %s" % __author__
+__copyright__ = "Copyright 2013-2014 {0}".format(__author__)
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
@@ -97,13 +97,16 @@ def validate_for_algorithm(self, algorithm):
 class GCM(object):
     name = "GCM"
 
-    def __init__(self, initialization_vector, tag=None):
+    def __init__(self, initialization_vector, tag=None, min_tag_length=16):
         # 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:
+        if min_tag_length < 4:
+            raise ValueError("min_tag_length must be >= 4")
+        if tag is not None and len(tag) < min_tag_length:
             raise ValueError(
-                "Authentication tag must be 4 bytes or longer."
+                "Authentication tag must be {0} bytes or longer.".format(
+                    min_tag_length)
             )
 
         self.initialization_vector = initialization_vector


Test patch:
diff --git a/tests/hazmat/primitives/test_aes.py b/tests/hazmat/primitives/test_aes.py
--- a/tests/hazmat/primitives/test_aes.py
+++ b/tests/hazmat/primitives/test_aes.py
@@ -225,6 +225,6 @@ class TestAESModeGCM(object):
             "gcmEncryptExtIV192.rsp",
             "gcmEncryptExtIV256.rsp",
         ],
-        lambda key: algorithms.AES(key),
-        lambda iv, tag: modes.GCM(iv, tag),
+        algorithms.AES,
+        modes.GCM,
     )
diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py
--- a/tests/hazmat/primitives/utils.py
+++ b/tests/hazmat/primitives/utils.py
@@ -90,7 +90,8 @@ def aead_test(backend, cipher_factory, mode_factory, params):
         cipher = Cipher(
             cipher_factory(binascii.unhexlify(params["key"])),
             mode_factory(binascii.unhexlify(params["iv"]),
-                         binascii.unhexlify(params["tag"])),
+                         binascii.unhexlify(params["tag"]),
+                         len(binascii.unhexlify(params["tag"]))),
             backend
         )
         decryptor = cipher.decryptor()
@@ -108,12 +109,13 @@ def aead_test(backend, cipher_factory, mode_factory, params):
         encryptor.authenticate_additional_data(binascii.unhexlify(aad))
         actual_ciphertext = encryptor.update(binascii.unhexlify(plaintext))
         actual_ciphertext += encryptor.finalize()
-        tag_len = len(params["tag"])
-        assert binascii.hexlify(encryptor.tag)[:tag_len] == params["tag"]
+        tag_len = len(binascii.unhexlify(params["tag"]))
+        assert binascii.hexlify(encryptor.tag[:tag_len]) == params["tag"]
         cipher = Cipher(
             cipher_factory(binascii.unhexlify(params["key"])),
             mode_factory(binascii.unhexlify(params["iv"]),
-                         binascii.unhexlify(params["tag"])),
+                         binascii.unhexlify(params["tag"]),
+                         min_tag_length=tag_len),
             backend
         )
         decryptor = cipher.decryptor()
@@ -309,6 +311,9 @@ def aead_tag_exception_test(backend, cipher_factory, mode_factory):
     with pytest.raises(ValueError):
         mode_factory(binascii.unhexlify(b"0" * 24), b"000")
 
+    with pytest.raises(ValueError):
+        mode_factory(binascii.unhexlify(b"0" * 24), b"000000", 2)
+
     cipher = Cipher(
         cipher_factory(binascii.unhexlify(b"0" * 32)),
         mode_factory(binascii.unhexlify(b"0" * 24), b"0" * 16),