From d7b288316bca7bcdd082e6ccff5491e241305233 Mon Sep 17 00:00:00 2001 From: Hubert Kario Date: Sun, 5 Jul 2015 14:42:56 +0200 Subject: [PATCH] constant time implementation of CBC mac and pad check to protect against Lucky 13 attacks, we need to check both the hash and padding in constant time, independent of the length of padding --- tlslite/utils/constanttime.py | 201 ++++++++ unit_tests/test_tlslite_utils_constanttime.py | 435 ++++++++++++++++++ 2 files changed, 636 insertions(+) create mode 100644 tlslite/utils/constanttime.py create mode 100644 unit_tests/test_tlslite_utils_constanttime.py diff --git a/tlslite/utils/constanttime.py b/tlslite/utils/constanttime.py new file mode 100644 index 00000000..c68077fb --- /dev/null +++ b/tlslite/utils/constanttime.py @@ -0,0 +1,201 @@ +# Copyright (c) 2015, Hubert Kario +# +# See the LICENSE file for legal information regarding use of this file. +"""Various constant time functions for processing sensitive data""" + +from __future__ import division + +from .compat import compatHMAC +import hmac + +def ct_lt_u32(val_a, val_b): + """ + Returns 1 if val_a < val_b, 0 otherwise. Constant time. + + @type val_a: int + @type val_b: int + @param val_a: an unsigned integer representable as a 32 bit value + @param val_b: an unsigned integer representable as a 32 bit value + @rtype: int + """ + val_a &= 0xffffffff + val_b &= 0xffffffff + + return (val_a^((val_a^val_b)|(((val_a-val_b)&0xffffffff)^val_b)))>>31 + +def ct_gt_u32(val_a, val_b): + """ + Return 1 if val_a > val_b, 0 otherwise. Constant time. + + @type val_a: int + @type val_b: int + @param val_a: an unsigned integer representable as a 32 bit value + @param val_b: an unsigned integer representable as a 32 bit value + @rtype: int + """ + return ct_lt_u32(val_b, val_a) + +def ct_le_u32(val_a, val_b): + """ + Return 1 if val_a <= val_b, 0 otherwise. Constant time. + + @type val_a: int + @type val_b: int + @param val_a: an unsigned integer representable as a 32 bit value + @param val_b: an unsigned integer representable as a 32 bit value + @rtype: int + """ + return 1 ^ ct_gt_u32(val_a, val_b) + +def ct_lsb_prop_u8(val): + """Propagate LSB to all 8 bits of the returned byte. Constant time.""" + val &= 0x01 + val |= val << 1 + val |= val << 2 + val |= val << 4 + return val + +def ct_isnonzero_u32(val): + """ + Returns 1 if val is != 0, 0 otherwise. Constant time. + + @type val: int + @param val: an unsigned integer representable as a 32 bit value + @rtype: int + """ + val &= 0xffffffff + return (val|(-val&0xffffffff)) >> 31 + +def ct_neq_u32(val_a, val_b): + """ + Return 1 if val_a != val_b, 0 otherwise. Constant time. + + @type val_a: int + @type val_b: int + @param val_a: an unsigned integer representable as a 32 bit value + @param val_b: an unsigned integer representable as a 32 bit value + @rtype: int + """ + val_a &= 0xffffffff + val_b &= 0xffffffff + + return (((val_a-val_b)&0xffffffff) | ((val_b-val_a)&0xffffffff)) >> 31 + +def ct_eq_u32(val_a, val_b): + """ + Return 1 if val_a == val_b, 0 otherwise. Constant time. + + @type val_a: int + @type val_b: int + @param val_a: an unsigned integer representable as a 32 bit value + @param val_b: an unsigned integer representable as a 32 bit value + @rtype: int + """ + return 1 ^ ct_neq_u32(val_a, val_b) + +def ct_check_cbc_mac_and_pad(data, mac, seqnumBytes, contentType, version): + """ + Check CBC cipher HMAC and padding. Close to constant time. + + @type data: bytearray + @param data: data with HMAC value to test and padding + + @type mac: hashlib mac + @param mac: empty HMAC, initialised with a key + + @type seqnumBytes: bytearray + @param seqnumBytes: TLS sequence number, used as input to HMAC + + @type contentType: int + @param contentType: a single byte, used as input to HMAC + + @type version: tuple of int + @param version: a tuple of two ints, used as input to HMAC and to guide + checking of padding + + @rtype: boolean + @return: True if MAC and pad is ok, False otherwise + """ + assert version in ((3, 0), (3, 1), (3, 2), (3, 3)) + + data_len = len(data) + if mac.digest_size + 1 > data_len: # data_len is public + return False + + # 0 - OK + result = 0x00 + + # + # check padding + # + pad_length = data[data_len-1] + pad_start = data_len - pad_length - 1 + pad_start = max(0, pad_start) + + if version == (3, 0): # version is public + # in SSLv3 we can only check if pad is not longer than overall length + + # subtract 1 for the pad length byte + mask = ct_lsb_prop_u8(ct_lt_u32(data_len-1, pad_length)) + result |= mask + else: + start_pos = max(0, data_len - 256) + for i in range(start_pos, data_len): + # if pad_start < i: mask = 0xff; else: mask = 0x00 + mask = ct_lsb_prop_u8(ct_le_u32(pad_start, i)) + # if data[i] != pad_length and "inside_pad": result = False + result |= (data[i] ^ pad_length) & mask + + # + # check MAC + # + + # real place where mac starts and data ends + mac_start = pad_start - mac.digest_size + mac_start = max(0, mac_start) + + # place to start processing + start_pos = max(0, data_len - (256 + mac.digest_size)) // mac.block_size + start_pos *= mac.block_size + + # add start data + data_mac = mac.copy() + data_mac.update(compatHMAC(seqnumBytes)) + data_mac.update(compatHMAC(bytearray([contentType]))) + if version != (3, 0): # version is public + data_mac.update(compatHMAC(bytearray([version[0]]))) + data_mac.update(compatHMAC(bytearray([version[1]]))) + data_mac.update(compatHMAC(bytearray([mac_start >> 8]))) + data_mac.update(compatHMAC(bytearray([mac_start & 0xff]))) + data_mac.update(compatHMAC(data[:start_pos])) + + # don't check past the array end (already checked to be >= zero) + end_pos = data_len - 1 - mac.digest_size + + # calculate all possible + for i in range(start_pos, end_pos): # constant for given overall length + cur_mac = data_mac.copy() + cur_mac.update(compatHMAC(data[start_pos:i])) + mac_compare = bytearray(cur_mac.digest()) + # compare the hash for real only if it's the place where mac is + # supposed to be + mask = ct_lsb_prop_u8(ct_eq_u32(i, mac_start)) + for j in range(0, mac.digest_size): # digest_size is public + result |= (data[i+j] ^ mac_compare[j]) & mask + + # return python boolean + return result == 0 + +if hasattr(hmac, 'compare_digest'): + ct_compare_digest = hmac.compare_digest +else: + def ct_compare_digest(val_a, val_b): + """Compares if string like objects are equal. Constant time.""" + if len(val_a) != len(val_b): + return False + + result = 0 + for x, y in zip(val_a, val_b): + result |= x ^ y + + return result == 0 diff --git a/unit_tests/test_tlslite_utils_constanttime.py b/unit_tests/test_tlslite_utils_constanttime.py new file mode 100644 index 00000000..e30ecf29 --- /dev/null +++ b/unit_tests/test_tlslite_utils_constanttime.py @@ -0,0 +1,435 @@ +# Copyright (c) 2015, Hubert Kario +# +# See the LICENSE file for legal information regarding use of this file. + +# compatibility with Python 2.6, for that we need unittest2 package, +# which is not available on 3.3 or 3.4 +try: + import unittest2 as unittest +except ImportError: + import unittest + +from tlslite.utils.constanttime import ct_lt_u32, ct_gt_u32, ct_le_u32, \ + ct_lsb_prop_u8, ct_isnonzero_u32, ct_neq_u32, ct_eq_u32, \ + ct_check_cbc_mac_and_pad, ct_compare_digest + +from tlslite.utils.compat import compatHMAC +from tlslite.recordlayer import RecordLayer +import hashlib +import hmac + +class TestContanttime(unittest.TestCase): + def test_ct_lt_u32(self): + for i in range(0, 256): + for j in range(0, 256): + self.assertEqual((i < j), (ct_lt_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(2**32-256, 2**32): + self.assertEqual((i < j), (ct_lt_u32(i, j) == 1)) + + for i in range(0, 256): + for j in range(2**32-256, 2**32): + self.assertEqual((i < j), (ct_lt_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(0, 256): + self.assertEqual((i < j), (ct_lt_u32(i, j) == 1)) + + def test_ct_gt_u32(self): + for i in range(0, 256): + for j in range(0, 256): + self.assertEqual((i > j), (ct_gt_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(2**32-256, 2**32): + self.assertEqual((i > j), (ct_gt_u32(i, j) == 1)) + + for i in range(0, 256): + for j in range(2**32-256, 2**32): + self.assertEqual((i > j), (ct_gt_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(0, 256): + self.assertEqual((i > j), (ct_gt_u32(i, j) == 1)) + + def test_ct_le_u32(self): + for i in range(0, 256): + for j in range(0, 256): + self.assertEqual((i <= j), (ct_le_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(2**32-256, 2**32): + self.assertEqual((i <= j), (ct_le_u32(i, j) == 1)) + + for i in range(0, 256): + for j in range(2**32-256, 2**32): + self.assertEqual((i <= j), (ct_le_u32(i, j) == 1)) + + for i in range(2**32-256, 2**32): + for j in range(0, 256): + self.assertEqual((i <= j), (ct_le_u32(i, j) == 1)) + + def test_ct_lsb_prop_u8(self): + for i in range(0, 256): + self.assertEqual(((i & 0x1) == 1), (ct_lsb_prop_u8(i) == 0xff)) + self.assertEqual(((i & 0x1) == 0), (ct_lsb_prop_u8(i) == 0x00)) + + def test_ct_isnonzero_u32(self): + for i in range(0, 256): + self.assertEqual((i != 0), (ct_isnonzero_u32(i) == 1)) + + def test_ct_neq_u32(self): + for i in range(0, 256): + for j in range(0, 256): + self.assertEqual((i != j), (ct_neq_u32(i, j) == 1)) + + for i in range(2**32-128, 2**32): + for j in range(2**32-128, 2**32): + self.assertEqual((i != j), (ct_neq_u32(i, j) == 1)) + + def test_ct_eq_u32(self): + for i in range(0, 256): + for j in range(0, 256): + self.assertEqual((i == j), (ct_eq_u32(i, j) == 1)) + + for i in range(2**32-128, 2**32): + for j in range(2**32-128, 2**32): + self.assertEqual((i == j), (ct_eq_u32(i, j) == 1)) + +class TestContanttimeCBCCheck(unittest.TestCase): + + @staticmethod + def data_prepare(application_data, seqnum_bytes, content_type, version, + mac, key): + r_layer = RecordLayer(None) + r_layer.version = version + + h = hmac.new(key, digestmod=mac) + + digest = r_layer._calculateMAC(h, seqnum_bytes, content_type, + application_data) + + return application_data + digest + + def test_with_empty_data_and_minimum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(0) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x00') + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_empty_data_and_maximum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(0) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\xff'*256) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_little_data_and_minimum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*32) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x00') + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_little_data_and_maximum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*32) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\xff'*256) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_lots_of_data_and_minimum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*1024) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x00') + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_lots_of_data_and_maximum_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*1024) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\xff'*256) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_lots_of_data_and_small_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*1024) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x0a'*11) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_too_little_data(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + mac = hashlib.sha1 + + data = bytearray(mac().digest_size) + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertFalse(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_invalid_hash(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*1024) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + data[-1] ^= 0xff + + padding = bytearray(b'\xff'*256) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertFalse(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_invalid_pad(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*1024) + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x00' + b'\xff'*255) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertFalse(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_pad_longer_than_data(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01') + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\xff') + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertFalse(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_pad_longer_than_data_in_SSLv3(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 0) + application_data = bytearray(b'\x01') + mac = hashlib.sha1 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray([len(application_data) + mac().digest_size + 1]) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertFalse(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_null_pad_in_SSLv3(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 0) + application_data = bytearray(b'\x01'*10) + mac = hashlib.md5 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x00'*10 + b'\x0a') + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_MD5(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 1) + application_data = bytearray(b'\x01'*10) + mac = hashlib.md5 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x0a'*11) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_SHA256(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 3) + application_data = bytearray(b'\x01'*10) + mac = hashlib.sha256 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x0a'*11) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + + def test_with_SHA384(self): + key = compatHMAC(bytearray(20)) + seqnum_bytes = bytearray(16) + content_type = 0x14 + version = (3, 3) + application_data = bytearray(b'\x01'*10) + mac = hashlib.sha384 + + data = self.data_prepare(application_data, seqnum_bytes, content_type, + version, mac, key) + + padding = bytearray(b'\x0a'*11) + data += padding + + h = hmac.new(key, digestmod=mac) + h.block_size = mac().block_size # python2 workaround + self.assertTrue(ct_check_cbc_mac_and_pad(data, h, seqnum_bytes, + content_type, version)) + +class TestCompareDigest(unittest.TestCase): + def test_with_equal_length(self): + self.assertTrue(ct_compare_digest(bytearray(10), bytearray(10))) + + self.assertTrue(ct_compare_digest(bytearray(b'\x02'*8), + bytearray(b'\x02'*8))) + + def test_different_lengths(self): + self.assertFalse(ct_compare_digest(bytearray(10), bytearray(12))) + + self.assertFalse(ct_compare_digest(bytearray(20), bytearray(12))) + + def test_different(self): + self.assertFalse(ct_compare_digest(bytearray(b'\x01'), + bytearray(b'\x03'))) + + self.assertFalse(ct_compare_digest(bytearray(b'\x01'*10 + b'\x02'), + bytearray(b'\x01'*10 + b'\x03'))) + + self.assertFalse(ct_compare_digest(bytearray(b'\x02' + b'\x01'*10), + bytearray(b'\x03' + b'\x01'*10)))