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)))