Redundantly verify file length in updater.

You may argue that the redundancy is unnecessary (pun intended), but it
is there because redundancy means one safety check will work where
another fails. I introduced this redundant file length check because the
updater unit test is mocking the download functions, which means that
file length checks in the download functions are being bypassed.
Redundancy is a good thing for safety.
This commit is contained in:
dachshund 2013-09-09 21:21:32 -04:00
parent a7f14e5334
commit 9ddd2617f5
6 changed files with 153 additions and 43 deletions

View file

@ -192,7 +192,14 @@ class DownloadError(Error):
class DownloadLengthMismatchError(DownloadError):
"""Indicate that a mismatch of lengths was seen while downloading a file."""
pass
def __init__(self, expected_length, observed_length):
self.expected_length = expected_length #bytes
self.observed_length = observed_length #bytes
def __str__(self):
return 'Observed length ('+str(self.observed_length)+\
') <= expected length ('+str(self.expected_length)+')'

View file

@ -592,17 +592,17 @@ def refresh(self):
def __check_hashes(self, input_file, trusted_hashes):
def __check_hashes(self, file_object, trusted_hashes):
"""
<Purpose>
A helper function that verifies multiple secure hashes of the downloaded
file. If any of these fail it raises an exception. This is to conform
with the TUF specs, which support clients with different hashing
algorithms. The 'hash.py' module is used to compute the hashes of the
'input_file'.
'file_object'.
<Arguments>
input_file:
file_object:
A file-like object.
trusted_hashes:
@ -624,7 +624,7 @@ def __check_hashes(self, input_file, trusted_hashes):
# any of the hashes are incorrect and return if all are correct.
for algorithm, trusted_hash in trusted_hashes.items():
digest_object = tuf.hash.digest(algorithm)
digest_object.update(input_file.read())
digest_object.update(file_object.read())
computed_hash = digest_object.hexdigest()
if trusted_hash != computed_hash:
raise tuf.BadHashError('Hashes do not match! Expected '+
@ -636,6 +636,74 @@ def __check_hashes(self, input_file, trusted_hashes):
def __hard_check_length(self, file_object, trusted_length):
"""
<Purpose>
A helper function that checks the expected length of a file-like object.
The length of the file must be strictly equal to the expected length.
This is a deliberately redundant implementation designed to complement
tuf.download._check_downloaded_length().
<Arguments>
file_object:
A file-like object.
trusted_length:
A nonnegative integer that is the expected length of the file.
<Exceptions>
tuf.DownloadLengthMismatchError, if the lengths don't match.
<Side Effects>
None.
<Returns>
None.
"""
observed_length = len(file_object)
if observed_length != trusted_length:
raise tuf.DownloadLengthMismatchError(trusted_length, observed_length)
def __soft_check_length(self, file_object, trusted_length):
"""
<Purpose>
A helper function that checks the expected length of a file-like object.
The length of the file must be less than or equal to the expected length.
This is a deliberately redundant implementation designed to complement
tuf.download._check_downloaded_length().
<Arguments>
file_object:
A file-like object.
trusted_length:
A nonnegative integer that is the expected length of the file.
<Exceptions>
tuf.DownloadLengthMismatchError, if the lengths don't match.
<Side Effects>
None.
<Returns>
None.
"""
observed_length = len(file_object)
if observed_length > trusted_length:
raise tuf.DownloadLengthMismatchError(trusted_length, observed_length)
def get_target_file(self, target_filepath, file_length, file_hashes):
"""
<Purpose>
@ -667,21 +735,25 @@ def get_target_file(self, target_filepath, file_length, file_hashes):
"""
def verify_target_file(target_file_object):
# Every target file must have its hashes inspected.
def verify_decompressed_target_file(target_file_object):
# Every target file must have its length and hashes inspected.
self.__hard_check_length(target_file_object, file_length)
self.__check_hashes(target_file_object, file_hashes)
return self.__get_file(target_filepath, verify_target_file, 'target',
file_length, download_safely=True, compression=None)
return self.__get_file(target_filepath, verify_decompressed_target_file,
'target', file_length, download_safely=True,
compression=None)
def __verify_metadata_file(self, metadata_file_object, metadata_role):
def __verify_decompressed_metadata_file(self, metadata_file_object,
metadata_role):
"""
<Purpose>
A private helpe function to verify a downloaded metadata file.
A private helper function to verify a decompressed downloaded metadata
file.
<Arguments>
metadata_file_object:
@ -787,11 +859,14 @@ def unsafely_get_metadata_file(self, metadata_role, metadata_filepath,
"""
def unsafely_verify_metadata_file(metadata_file_object):
self.__verify_metadata_file(metadata_file_object, metadata_role)
def unsafely_verify_decompressed_metadata_file(metadata_file_object):
self.__soft_check_length(metadata_file_object, file_length)
self.__verify_decompressed_metadata_file(metadata_file_object,
metadata_role)
return self.__get_file(metadata_filepath, unsafely_verify_metadata_file,
'meta', file_length, download_safely=False,
return self.__get_file(metadata_filepath,
unsafely_verify_decompressed_metadata_file, 'meta',
file_length, download_safely=False,
compression=None)
@ -836,12 +911,15 @@ def safely_get_metadata_file(self, metadata_role, metadata_filepath,
"""
def safely_verify_metadata_file(metadata_file_object):
def safely_verify_decompressed_metadata_file(metadata_file_object):
self.__hard_check_length(metadata_file_object, file_length)
self.__check_hashes(metadata_file_object, file_hashes)
self.__verify_metadata_file(metadata_file_object, metadata_role)
self.__verify_decompressed_metadata_file(metadata_file_object,
metadata_role)
return self.__get_file(metadata_filepath, safely_verify_metadata_file,
'meta', file_length, download_safely=True,
return self.__get_file(metadata_filepath,
safely_verify_decompressed_metadata_file, 'meta',
file_length, download_safely=True,
compression=compression)
@ -851,7 +929,7 @@ def safely_verify_metadata_file(metadata_file_object):
# TODO: Instead of the more fragile 'download_safely' switch, unroll the
# function into two separate ones: one for "safe" download, and the other one
# for "unsafe" download? This should induce safer and more readable code.
def __get_file(self, filepath, verify_file, file_type,
def __get_file(self, filepath, verify_decompressed_file, file_type,
file_length, download_safely, compression):
"""
<Purpose>
@ -863,9 +941,9 @@ def __get_file(self, filepath, verify_file, file_type,
filepath:
The relative metadata or target filepath.
verify_file:
A function which expects a file-like object and which will raise an
exception in case the file is not valid for any reason.
verify_decompressed_file:
A function which expects a decompressed file-like object and which will
raise an exception in case the file is not valid for any reason.
file_type:
Type of data needed for download, must correspond to one of the strings
@ -911,9 +989,12 @@ def __get_file(self, filepath, verify_file, file_type,
file_object = tuf.download.unsafe_download(file_mirror, file_length)
if compression:
logger.debug('Decompressing '+str(file_mirror))
file_object.decompress_temp_file_object(compression)
else:
logger.debug('Not decompressing '+str(file_mirror))
verify_file(file_object)
verify_decompressed_file(file_object)
except Exception, exception:
# Remember the error from this mirror, and "reset" the target file.
@ -2155,10 +2236,10 @@ def _visit_child_role(self, child_role, target_filepath):
Ensure that we explore only delegated roles trusted with the target. We
assume conservation of delegated paths in the complete tree of
delegations. Note that the call to _ensure_all_targets_allowed in
__verify_metadata_file should already ensure that all targets metadata is
valid; i.e. that the targets signed by a delegatee is a proper subset of
the targets delegated to it by the delegator. Nevertheless, we check it
again here for performance and safety reasons.
__verify_decompressed_metadata_file should already ensure that all
targets metadata is valid; i.e. that the targets signed by a delegatee is
a proper subset of the targets delegated to it by the delegator.
Nevertheless, we check it again here for performance and safety reasons.
TODO: Should the TUF spec restrict the repository to one particular
algorithm? Should we allow the repository to specify in the role

View file

@ -647,7 +647,7 @@ def _check_downloaded_length(total_downloaded, required_length,
if STRICT_REQUIRED_LENGTH:
# This must be due to a programming error, and must never happen!
logger.error(message)
raise tuf.DownloadLengthMismatchError(message)
raise tuf.DownloadLengthMismatchError(required_length, total_downloaded)
else:
# We specifically disabled strict checking of required length, but we
# will log a warning anyway. This is useful when we wish to download the

View file

@ -534,15 +534,15 @@ def test_3__update_metadata(self):
added_target_2 = self._add_target_to_targets_dir(targets_keyids)
# To test compressed file handling, compress targets metadata file.
targets_filepath_compressed = self._compress_file(self.targets_filepath)
targets_filepath_compressed = self._compress_file(self.targets_filepath)
# Re-patch 'download.download_url_to_tempfileobj' function.
self._mock_download_url_to_tempfileobj(targets_filepath_compressed)
# TODO: Not convinced this is actually being tested correctly.
# See how we get fileinfo in tuf.client.updater._update_metadata_if_changed
# FIXME: The length (but not the hash) passed to this function is
# incorrect. The length must be that of the compressed file, whereas the
# hash must be that of the uncompressed file.
_update_metadata('targets',
#signerlib.get_metadata_file_info(self.targets_filepath),
None,
signerlib.get_metadata_file_info(self.targets_filepath),
compression='gzip')
list_of_targets = self.Repository.metadata['current']['targets']['targets']
@ -696,12 +696,12 @@ def test_3__update_metadata_if_changed(self):
# Patch 'download.download_url_to_tempfileobj' and update targets.
self._mock_download_url_to_tempfileobj(self.root_filepath)
# TODO: Is this the original intent of this test?
# FIXME: What is the original intent of this test?
try:
update_if_changed('targets')
except tuf.NoWorkingMirrorError, exception:
for mirror_url, mirror_error in exception.mirror_errors.iteritems():
assert isinstance(mirror_error, tuf.BadHashError)
assert isinstance(mirror_error, tuf.DownloadLengthMismatchError)
# Restoring repositories to the initial state.
os.remove(release_filepath_compressed)

View file

@ -1,3 +1,5 @@
#!/usr/bin/env python
"""
<Program Name>
test_util.py
@ -37,7 +39,6 @@ class TestUtil(unittest_toolbox.Modified_TestCase):
def setUp(self):
unittest_toolbox.Modified_TestCase.setUp(self)
util.tempfile.TemporaryFile = tempfile.TemporaryFile
self.temp_fileobj = util.TempFile()
@ -71,10 +72,10 @@ def _extract_tempfile_directory(self, config_temp_dir=None):
# Patching 'tempfile.TemporaryFile()' (by substituting
# temfile.TemporaryFile() with tempfile.mkstemp()) in order to get the
# directory of the stored tempfile object.
saved_tempfile_TemporaryFile = util.tempfile.TemporaryFile
util.tempfile.TemporaryFile = tempfile.mkstemp
saved_tempfile_TemporaryFile = util.tempfile.NamedTemporaryFile
util.tempfile.NamedTemporaryFile = tempfile.mkstemp
_temp_fileobj = util.TempFile()
util.tempfile.TemporaryFile = saved_tempfile_TemporaryFile
util.tempfile.NamedTemporaryFile = saved_tempfile_TemporaryFile
junk, _tempfilepath = _temp_fileobj.temporary_file
_tempfile_dir = os.path.dirname(_tempfilepath)

View file

@ -52,7 +52,7 @@ class TempFile(object):
def _default_temporary_directory(self, prefix):
"""__init__ helper."""
try:
self.temporary_file = tempfile.TemporaryFile(prefix=prefix)
self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix)
except OSError, err:
logger.critical('Temp file in '+temp_dir+'failed: '+repr(err))
raise tuf.Error(err)
@ -66,7 +66,7 @@ def __init__(self, prefix='tuf_temp_'):
<Arguments>
prefix:
A string argument to be used with tempfile.TemporaryFile function.
A string argument to be used with tempfile.NamedTemporaryFile function.
<Exceptions>
tuf.Error on failure to load temp dir.
@ -82,7 +82,8 @@ def __init__(self, prefix='tuf_temp_'):
temp_dir = tuf.conf.temporary_directory
if temp_dir is not None and isinstance(temp_dir, str):
try:
self.temporary_file = tempfile.TemporaryFile(prefix=prefix, dir=temp_dir)
self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix,
dir=temp_dir)
except OSError, err:
logger.error('Temp file in '+temp_dir+' failed: '+repr(err))
logger.error('Will attempt to use system default temp dir.')
@ -92,6 +93,26 @@ def __init__(self, prefix='tuf_temp_'):
def __len__(self):
"""
<Purpose>
Initializes TempFile.
<Arguments>
None.
<Exceptions>
OSError.
<Return>
Nonnegative integer representing file size.
"""
return os.stat(self.temporary_file.name).st_size
def flush(self):
"""
<Purpose>