diff --git a/tests/test_util.py b/tests/test_util.py index 25e1e318..8896e03e 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -32,6 +32,7 @@ import logging import tempfile import unittest +import timeit import tuf import tuf.log @@ -577,6 +578,48 @@ def test_c6_get_compressed_length(self): + def test_digests_are_equal(self): + digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + + # Normal case: test for digests that are equal. + self.assertTrue(tuf.util.digests_are_equal(digest, digest)) + + # Normal case: test for digests that are unequal. + self.assertFalse(tuf.util.digests_are_equal(digest, '0a8df1')) + + # Test for invalid arguments. + self.assertRaises(tuf.FormatError, tuf.util.digests_are_equal, 7, + digest) + self.assertRaises(tuf.FormatError, tuf.util.digests_are_equal, digest, + 7) + + # Test that digests_are_equal() takes the same amount of time to compare + # equal and unequal arguments. + runtime = timeit.timeit('digests_are_equal("ab8df", "ab8df")', + setup='from tuf.util import digests_are_equal', + number=100000) + + runtime2 = timeit.timeit('digests_are_equal("ab8df", "1b8df")', + setup='from tuf.util import digests_are_equal', + number=100000) + + print('\nruntime1: ' + repr(runtime)) + print('runtime2: ' + repr(runtime2)) + + runtime3 = timeit.timeit('"ab8df" == "ab8df"', number=100000) + + runtime4 = timeit.timeit('"ab8df" == "1b8df"', number=1000000) + + # The ratio for the 'digest_are_equal' runtimes should be at or near 1. + ratio_digests_are_equal = abs(runtime2 / runtime) + + # The ratio for the variable-time runtimes should be (>1) & at or near 10? + ratio_variable_compare = abs(runtime4 / runtime3) + + self.assertTrue(ratio_digests_are_equal < ratio_variable_compare) + + + # Run unit test. if __name__ == '__main__': unittest.main() diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 85521376..de5fe796 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -931,7 +931,7 @@ def _decrypt(file_contents, password): Crypto.Hash.SHA256) generated_hmac = generated_hmac_object.hexdigest() - if generated_hmac != hmac: + if not tuf.util.digests_are_equal(generated_hmac, hmac): raise tuf.CryptoError('Decryption failed.') # The following decryption routine assumes 'ciphertext' was encrypted with diff --git a/tuf/util.py b/tuf/util.py index fa8219a6..8d8a0b46 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -950,3 +950,45 @@ def load_json_file(filepath): finally: fileobject.close() + + + +def digests_are_equal(digest1, digest2): + """ + + While protecting against timing attacks, compare the hexadecimal arguments + and determine if they are equal. + + + digest1: + The first hexadecimal string value to compare. + + digest2: + The second hexadecimal string value to compare. + + + tuf.FormatError: If the arguments are improperly formatted. + + + None. + + + Return True if 'digest1' is equal to 'digest2', False otherwise. + """ + + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.HEX_SCHEMA.check_match(digest1) + tuf.formats.HEX_SCHEMA.check_match(digest2) + + if len(digest1) != len(digest2): + return False + + are_equal = True + + for element in range(len(digest1)): + if digest1[element] != digest2[element]: + are_equal = False + + return are_equal