1"""
2<Program Name>
3  util.py
4
5<Author>
6  Konstantin Andrianov
7
8<Started>
9  March 24, 2012.  Derived from original util.py written by Geremy Condra.
10
11<Copyright>
12  See LICENSE for licensing information.
13
14<Purpose>
15  Provides utility services.  This module supplies utility functions such as:
16  get_file_details() that computes the length and hash of a file, import_json
17  that tries to import a working json module, load_json_* functions, and a
18  TempFile class that generates a file-like object for temporary storage, etc.
19"""
20
21# Help with Python 3 compatibility, where the print statement is a function, an
22# implicit relative import is invalid, and the '/' operator performs true
23# division.  Example:  print 'hello world' raises a 'SyntaxError' exception.
24from __future__ import print_function
25from __future__ import absolute_import
26from __future__ import division
27from __future__ import unicode_literals
28
29import os
30import sys
31import gzip
32import shutil
33import logging
34import tempfile
35import fnmatch
36
37import securesystemslib.exceptions
38import securesystemslib.settings
39import securesystemslib.hash
40import securesystemslib.formats
41
42import six
43
44# The algorithm used by the repository to generate the digests of the
45# target filepaths, which are included in metadata files and may be prepended
46# to the filenames of consistent snapshots.
47HASH_FUNCTION = 'sha256'
48
49# See 'log.py' to learn how logging is handled in TUF.
50logger = logging.getLogger('securesystemslib_util')
51
52
53class TempFile(object):
54  """
55  <Purpose>
56    A high-level temporary file that cleans itself up or can be manually
57    cleaned up. This isn't a complete file-like object. The file functions
58    that are supported make additional common-case safe assumptions.  There
59    are additional functions that aren't part of file-like objects.  TempFile
60    is used in the download.py module to temporarily store downloaded data while
61    all security checks (file hashes/length) are performed.
62  """
63
64  def _default_temporary_directory(self, prefix):
65    """__init__ helper."""
66    try:
67      self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix)
68
69    except OSError as err: # pragma: no cover
70      logger.critical('Cannot create a system temporary directory: '+repr(err))
71      raise securesystemslib.exceptions.Error(err)
72
73
74  def __init__(self, prefix='tuf_temp_'):
75    """
76    <Purpose>
77      Initializes TempFile.
78
79    <Arguments>
80      prefix:
81        A string argument to be used with tempfile.NamedTemporaryFile function.
82
83    <Exceptions>
84      securesystemslib.exceptions.Error on failure to load temp dir.
85
86    <Return>
87      None.
88    """
89
90    self._compression = None
91
92    # If compression is set then the original file is saved in 'self._orig_file'.
93    self._orig_file = None
94    temp_dir = securesystemslib.settings.temporary_directory
95    if temp_dir is not None and securesystemslib.formats.PATH_SCHEMA.matches(temp_dir):
96      try:
97        self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix,
98                                                          dir=temp_dir)
99      except OSError as err:
100        logger.error('Temp file in ' + temp_dir + ' failed: ' +repr(err))
101        logger.error('Will attempt to use system default temp dir.')
102        self._default_temporary_directory(prefix)
103
104    else:
105      self._default_temporary_directory(prefix)
106
107
108  def get_compressed_length(self):
109    """
110    <Purpose>
111      Get the compressed length of the file. This will be correct information
112      even when the file is read as an uncompressed one.
113
114    <Arguments>
115      None.
116
117    <Exceptions>
118      OSError.
119
120    <Return>
121      Nonnegative integer representing compressed file size.
122    """
123
124    # Even if we read a compressed file with the gzip standard library module,
125    # the original file will remain compressed.
126    return os.stat(self.temporary_file.name).st_size
127
128
129  def flush(self):
130    """
131    <Purpose>
132      Flushes buffered output for the file.
133
134    <Arguments>
135      None.
136
137    <Exceptions>
138      None.
139
140    <Return>
141      None.
142    """
143
144    self.temporary_file.flush()
145
146
147  def read(self, size=None):
148    """
149    <Purpose>
150      Read specified number of bytes.  If size is not specified then the whole
151      file is read and the file pointer is placed at the beginning of the file.
152
153    <Arguments>
154      size:
155        Number of bytes to be read.
156
157    <Exceptions>
158      securesystemslib.exceptions.FormatError: if 'size' is invalid.
159
160    <Return>
161      String of data.
162    """
163
164    if size is None:
165      self.temporary_file.seek(0)
166      data = self.temporary_file.read()
167      self.temporary_file.seek(0)
168
169      return data
170
171    else:
172      if not (isinstance(size, int) and size > 0):
173        raise securesystemslib.exceptions.FormatError
174
175      return self.temporary_file.read(size)
176
177
178  def write(self, data, auto_flush=True):
179    """
180    <Purpose>
181      Writes a data string to the file.
182
183    <Arguments>
184      data:
185        A string containing some data.
186
187      auto_flush:
188        Boolean argument, if set to 'True', all data will be flushed from
189        internal buffer.
190
191    <Exceptions>
192      None.
193
194    <Return>
195      None.
196    """
197
198    self.temporary_file.write(data)
199    if auto_flush:
200      self.flush()
201
202
203  def move(self, destination_path):
204    """
205    <Purpose>
206      Copies 'self.temporary_file' to a non-temp file at 'destination_path' and
207      closes 'self.temporary_file' so that it is removed.
208
209    <Arguments>
210      destination_path:
211        Path to store the file in.
212
213    <Exceptions>
214      None.
215
216    <Return>
217      None.
218    """
219
220    self.flush()
221    self.seek(0)
222    destination_file = open(destination_path, 'wb')
223    shutil.copyfileobj(self.temporary_file, destination_file)
224    # Force the destination file to be written to disk from Python's internal
225    # and the operation system's buffers.  os.fsync() should follow flush().
226    destination_file.flush()
227    os.fsync(destination_file.fileno())
228    destination_file.close()
229
230    # 'self.close()' closes temporary file which destroys itself.
231    self.close_temp_file()
232
233
234  def seek(self, *args):
235    """
236    <Purpose>
237      Set file's current position.
238
239    <Arguments>
240      *args:
241        (*-operator): unpacking argument list is used because seek method
242        accepts two args: offset and whence.  If whence is not specified, its
243        default is 0.  Indicate offset to set the file's current position.
244        Refer to the python manual for more info.
245
246    <Exceptions>
247      None.
248
249    <Return>
250      None.
251    """
252
253    self.temporary_file.seek(*args)
254
255
256  def decompress_temp_file_object(self, compression):
257    """
258    <Purpose>
259      To decompress a compressed temp file object.  Decompression is performed
260      on a temp file object that is compressed, this occurs after downloading
261      a compressed file.  For instance if a compressed version of some meta
262      file in the repository is downloaded, the temp file containing the
263      compressed meta file will be decompressed using this function.
264      Note that after calling this method, write() can no longer be called.
265
266                            meta.json.gz
267                               |...[download]
268                        temporary_file (containing meta.json.gz)
269                        /             \
270               temporary_file          _orig_file
271          containing meta.json          containing meta.json.gz
272          (decompressed data)
273
274    <Arguments>
275      compression:
276        A string indicating the type of compression that was used to compress
277        a file.  Only gzip is allowed.
278
279    <Exceptions>
280      securesystemslib.exceptions.FormatError: If 'compression' is improperly formatted.
281
282      securesystemslib.exceptions.Error: If an invalid compression is given.
283
284      securesystemslib.exceptions.DecompressionError: If the compression failed for any reason.
285
286    <Side Effects>
287      'self._orig_file' is used to store the original data of 'temporary_file'.
288
289    <Return>
290      None.
291    """
292
293    # Does 'compression' have the correct format?
294    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
295    securesystemslib.formats.NAME_SCHEMA.check_match(compression)
296
297    if self._orig_file is not None:
298      raise securesystemslib.exceptions.Error('Can only set compression on a'
299          ' TempFile once.')
300
301    if compression != 'gzip':
302      raise securesystemslib.exceptions.Error('Only gzip compression is'
303          ' supported.')
304
305    self.seek(0)
306    self._compression = compression
307    self._orig_file = self.temporary_file
308
309    try:
310      gzip_file_object = gzip.GzipFile(fileobj=self.temporary_file, mode='rb')
311      uncompressed_content = gzip_file_object.read()
312      self.temporary_file = tempfile.NamedTemporaryFile()
313      self.temporary_file.write(uncompressed_content)
314      self.flush()
315
316    except Exception as exception:
317      raise securesystemslib.exceptions.DecompressionError(exception)
318
319
320  def close_temp_file(self):
321    """
322    <Purpose>
323      Closes the temporary file object. 'close_temp_file' mimics usual
324      file.close(), however temporary file destroys itself when
325      'close_temp_file' is called. Further if compression is set, second
326      temporary file instance 'self._orig_file' is also closed so that no open
327      temporary files are left open.
328
329    <Arguments>
330      None.
331
332    <Exceptions>
333      None.
334
335    <Side Effects>
336      Closes 'self._orig_file'.
337
338    <Return>
339      None.
340    """
341
342    self.temporary_file.close()
343    # If compression has been set, we need to explicitly close the original
344    # file object.
345    if self._orig_file is not None:
346      self._orig_file.close()
347
348
349def get_file_details(filepath, hash_algorithms=['sha256']):
350  """
351  <Purpose>
352    To get file's length and hash information.  The hash is computed using the
353    sha256 algorithm.  This function is used in the signerlib.py and updater.py
354    modules.
355
356  <Arguments>
357    filepath:
358      Absolute file path of a file.
359
360    hash_algorithms:
361
362  <Exceptions>
363    securesystemslib.exceptions.FormatError: If hash of the file does not match
364    HASHDICT_SCHEMA.
365
366    securesystemslib.exceptions.Error: If 'filepath' does not exist.
367
368  <Returns>
369    A tuple (length, hashes) describing 'filepath'.
370  """
371
372  # Making sure that the format of 'filepath' is a path string.
373  # 'securesystemslib.exceptions.FormatError' is raised on incorrect format.
374  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
375  securesystemslib.formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms)
376
377  # The returned file hashes of 'filepath'.
378  file_hashes = {}
379
380  # Does the path exists?
381  if not os.path.exists(filepath):
382    raise securesystemslib.exceptions.Error('Path ' + repr(filepath) + ' doest'
383        ' not exist.')
384
385  filepath = os.path.abspath(filepath)
386
387  # Obtaining length of the file.
388  file_length = os.path.getsize(filepath)
389
390  # Obtaining hash of the file.
391  for algorithm in hash_algorithms:
392    digest_object = securesystemslib.hash.digest_filename(filepath, algorithm)
393    file_hashes.update({algorithm: digest_object.hexdigest()})
394
395  # Performing a format check to ensure 'file_hash' corresponds HASHDICT_SCHEMA.
396  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
397  securesystemslib.formats.HASHDICT_SCHEMA.check_match(file_hashes)
398
399  return file_length, file_hashes
400
401
402def ensure_parent_dir(filename):
403  """
404  <Purpose>
405    To ensure existence of the parent directory of 'filename'.  If the parent
406    directory of 'name' does not exist, create it.
407
408    Example: If 'filename' is '/a/b/c/d.txt', and only the directory '/a/b/'
409    exists, then directory '/a/b/c/d/' will be created.
410
411  <Arguments>
412    filename:
413      A path string.
414
415  <Exceptions>
416    securesystemslib.exceptions.FormatError: If 'filename' is improperly
417    formatted.
418
419  <Side Effects>
420    A directory is created whenever the parent directory of 'filename' does not
421    exist.
422
423  <Return>
424    None.
425  """
426
427  # Ensure 'filename' corresponds to 'PATH_SCHEMA'.
428  # Raise 'securesystemslib.exceptions.FormatError' on a mismatch.
429  securesystemslib.formats.PATH_SCHEMA.check_match(filename)
430
431  # Split 'filename' into head and tail, check if head exists.
432  directory = os.path.split(filename)[0]
433
434  if directory and not os.path.exists(directory):
435    # mode = 'rwx------'. 448 (decimal) is 700 in octal.
436    os.makedirs(directory, 448)
437
438
439def file_in_confined_directories(filepath, confined_directories):
440  """
441  <Purpose>
442    Check if the directory containing 'filepath' is in the list/tuple of
443    'confined_directories'.
444
445  <Arguments>
446    filepath:
447      A string representing the path of a file.  The following example path
448      strings are viewed as files and not directories: 'a/b/c', 'a/b/c.txt'.
449
450    confined_directories:
451      A list, or a tuple, of directory strings.
452
453  <Exceptions>
454   securesystemslib.exceptions.FormatError: On incorrect format of the input.
455
456  <Return>
457    Boolean.  True, if path is either the empty string
458    or in 'confined_paths'; False, otherwise.
459  """
460
461  # Do the arguments have the correct format?
462  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
463  securesystemslib.formats.RELPATH_SCHEMA.check_match(filepath)
464  securesystemslib.formats.RELPATHS_SCHEMA.check_match(confined_directories)
465
466  for confined_directory in confined_directories:
467    # The empty string (arbitrarily chosen) signifies the client is confined
468    # to all directories and subdirectories.  No need to check 'filepath'.
469    if confined_directory == '':
470      return True
471
472    # Normalized paths needed, to account for up-level references, etc.
473    # TUF clients have the option of setting the list of directories in
474    # 'confined_directories'.
475    filepath = os.path.normpath(filepath)
476    confined_directory = os.path.normpath(confined_directory)
477
478    # A TUF client may restrict himself to specific directories on the
479    # remote repository.  The list of paths in 'confined_path', not including
480    # each path's subdirectories, are the only directories the client will
481    # download targets from.
482    if os.path.dirname(filepath) == confined_directory:
483      return True
484
485  return False
486
487
488def find_delegated_role(roles, delegated_role):
489  """
490  <Purpose>
491    Find the index, if any, of a role with a given name in a list of roles.
492
493  <Arguments>
494    roles:
495      The list of roles, each of which must have a 'name' attribute.
496
497    delegated_role:
498      The name of the role to be found in the list of roles.
499
500  <Exceptions>
501    securesystemslib.exceptions.RepositoryError, if the list of roles has
502    invalid data.
503
504  <Side Effects>
505    No known side effects.
506
507  <Returns>
508    The unique index, an interger, in the list of roles.  if 'delegated_role'
509    does not exist, 'None' is returned.
510  """
511
512  # Do the arguments have the correct format?
513  # Ensure the arguments have the appropriate number of objects and object
514  # types, and that all dict keys are properly named.  Raise
515  # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
516  securesystemslib.formats.ROLELIST_SCHEMA.check_match(roles)
517  securesystemslib.formats.ROLENAME_SCHEMA.check_match(delegated_role)
518
519  # The index of a role, if any, with the same name.
520  role_index = None
521
522  for index in six.moves.xrange(len(roles)):
523    role = roles[index]
524    name = role.get('name')
525
526    # This role has no name.
527    if name is None:
528      no_name_message = 'Role with no name.'
529      raise securesystemslib.exceptions.RepositoryError(no_name_message)
530
531    # Does this role have the same name?
532    else:
533      # This role has the same name, and...
534      if name == delegated_role:
535        # ...it is the only known role with the same name.
536        if role_index is None:
537          role_index = index
538
539        # ...there are at least two roles with the same name.
540        else:
541          duplicate_role_message = 'Duplicate role (' + str(delegated_role) + ').'
542          raise securesystemslib.exceptions.RepositoryError(
543              'Duplicate role (' + str(delegated_role) + ').')
544
545      # This role has a different name.
546      else:
547        logger.debug('Skipping delegated role: ' + repr(delegated_role))
548
549  return role_index
550
551
552def ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations):
553  """
554  <Purpose>
555    Ensure that the list of targets specified by 'rolename' are allowed; this
556    is determined by inspecting the 'delegations' field of the parent role of
557    'rolename'.  If a target specified by 'rolename' is not found in the
558    delegations field of 'metadata_object_of_parent', raise an exception.  The
559    top-level role 'targets' is allowed to list any target file, so this
560    function does not raise an exception if 'rolename' is 'targets'.
561
562    Targets allowed are either exlicitly listed under the 'paths' field, or
563    implicitly exist under a subdirectory of a parent directory listed under
564    'paths'.  A parent role may delegate trust to all files under a particular
565    directory, including files in subdirectories, by simply listing the
566    directory (e.g., '/packages/source/Django/', the equivalent of
567    '/packages/source/Django/*').  Targets listed in hashed bins are also
568    validated (i.e., its calculated path hash prefix must be delegated by the
569    parent role).
570
571    TODO: Should the TUF spec restrict the repository to one particular
572    algorithm when calcutating path hash prefixes (currently restricted to
573    SHA256)?  Should we allow the repository to specify in the role dictionary
574    the algorithm used for these generated hashed paths?
575
576  <Arguments>
577    rolename:
578      The name of the role whose targets must be verified. This is a
579      role name and should not end in '.json'.  Examples: 'root', 'targets',
580      'targets/linux/x86'.
581
582    list_of_targets:
583      The targets of 'rolename', as listed in targets field of the 'rolename'
584      metadata.  'list_of_targets' are target paths relative to the targets
585      directory of the repository.  The delegations of the parent role are
586      checked to verify that the targets of 'list_of_targets' are valid.
587
588    parent_delegations:
589      The parent delegations of 'rolename'.  The metadata object stores
590      the allowed paths and path hash prefixes of child delegations in its
591      'delegations' attribute.
592
593  <Exceptions>
594    securesystemslib.exceptions.FormatError:
595      If any of the arguments are improperly formatted.
596
597    securesystemslib.exceptions.ForbiddenTargetError:
598      If the targets of 'metadata_role' are not allowed according to
599      the parent's metadata file.  The 'paths' and 'path_hash_prefixes'
600      attributes are verified.
601
602    securesystemslib.exceptions.RepositoryError:
603      If the parent of 'rolename' has not made a delegation to 'rolename'.
604
605  <Side Effects>
606    None.
607
608  <Returns>
609    None.
610  """
611
612  # Do the arguments have the correct format?
613  # Ensure the arguments have the appropriate number of objects and object
614  # types, and that all dict keys are properly named.  Raise
615  # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
616  securesystemslib.formats.ROLENAME_SCHEMA.check_match(rolename)
617  securesystemslib.formats.RELPATHS_SCHEMA.check_match(list_of_targets)
618  securesystemslib.formats.DELEGATIONS_SCHEMA.check_match(parent_delegations)
619
620  # Return if 'rolename' is 'targets'.  'targets' is not a delegated role.  Any
621  # target file listed in 'targets' is allowed.
622  if rolename == 'targets':
623    return
624
625  # The allowed targets of delegated roles are stored in the parent's metadata
626  # file.  Iterate 'list_of_targets' and confirm they are trusted, or their
627  # root parent directory exists in the role delegated paths, or path hash
628  # prefixes, of the parent role.  First, locate 'rolename' in the 'roles'
629  # attribute of 'parent_delegations'.
630  roles = parent_delegations['roles']
631  role_index = find_delegated_role(roles, rolename)
632
633  # Ensure the delegated role exists prior to extracting trusted paths from
634  # the parent's 'paths', or trusted path hash prefixes from the parent's
635  # 'path_hash_prefixes'.
636  if role_index is not None:
637    role = roles[role_index]
638    allowed_child_paths = role.get('paths')
639    allowed_child_path_hash_prefixes = role.get('path_hash_prefixes')
640    actual_child_targets = list_of_targets
641
642    if allowed_child_path_hash_prefixes is not None:
643      consistent = paths_are_consistent_with_hash_prefixes
644
645      # 'actual_child_tarets' (i.e., 'list_of_targets') should have lenth
646      # greater than zero due to the format check above.
647      if not consistent(actual_child_targets,
648                        allowed_child_path_hash_prefixes):
649        message =  repr(rolename) + ' specifies a target that does not' + \
650          ' have a path hash prefix listed in its parent role.'
651        raise securesystemslib.exceptions.ForbiddenTargetError(message)
652
653    elif allowed_child_paths is not None:
654      # Check that each delegated target is either explicitly listed or a
655      # parent directory is found under role['paths'], otherwise raise an
656      # exception.  If the parent role explicitly lists target file paths in
657      # 'paths', this loop will run in O(n^2), the worst-case.  The repository
658      # maintainer will likely delegate entire directories, and opt for
659      # explicit file paths if the targets in a directory are delegated to
660      # different roles/developers.
661      for child_target in actual_child_targets:
662        for allowed_child_path in allowed_child_paths:
663          if fnmatch.fnmatch(child_target, allowed_child_path):
664            break
665
666        else:
667          raise securesystemslib.exceptions.ForbiddenTargetError(
668              'Role ' + repr(rolename) + ' specifies'
669              ' target' + repr(child_target) + ',' + ' which is not an allowed'
670              ' path according to the delegations set by its parent role.')
671
672    else:
673      # 'role' should have been validated when it was downloaded.
674      # The 'paths' or 'path_hash_prefixes' attributes should not be missing,
675      # so raise an error in case this clause is reached.
676      raise securesystemslib.exceptions.FormatError(repr(role) + ' did not'
677          ' contain one of the required fields ("paths" or'
678          ' "path_hash_prefixes").')
679
680  # Raise an exception if the parent has not delegated to the specified
681  # 'rolename' child role.
682  else:
683    raise securesystemslib.exceptions.RepositoryError('The parent role has'
684        ' not delegated to ' + repr(rolename) + '.')
685
686
687def paths_are_consistent_with_hash_prefixes(paths, path_hash_prefixes):
688  """
689  <Purpose>
690    Determine whether a list of paths are consistent with their alleged path
691    hash prefixes. By default, the SHA256 hash function is used.
692
693  <Arguments>
694    paths:
695      A list of paths for which their hashes will be checked.
696
697    path_hash_prefixes:
698      The list of path hash prefixes with which to check the list of paths.
699
700  <Exceptions>
701    securesystemslib.exceptions.FormatError:
702      If the arguments are improperly formatted.
703
704  <Side Effects>
705    No known side effects.
706
707  <Returns>
708    A Boolean indicating whether or not the paths are consistent with the
709    hash prefix.
710  """
711
712  # Do the arguments have the correct format?
713  # Ensure the arguments have the appropriate number of objects and object
714  # types, and that all dict keys are properly named.  Raise
715  # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
716  securesystemslib.formats.RELPATHS_SCHEMA.check_match(paths)
717  securesystemslib.formats.PATH_HASH_PREFIXES_SCHEMA.check_match(path_hash_prefixes)
718
719  # Assume that 'paths' and 'path_hash_prefixes' are inconsistent until
720  # proven otherwise.
721  consistent = False
722
723  # The format checks above ensure the 'paths' and 'path_hash_prefix' lists
724  # have lengths greater than zero.
725  for path in paths:
726    path_hash = get_target_hash(path)
727
728    # Assume that every path is inconsistent until proven otherwise.
729    consistent = False
730
731    for path_hash_prefix in path_hash_prefixes:
732      if path_hash.startswith(path_hash_prefix):
733        consistent = True
734        break
735
736    # This path has no matching path_hash_prefix. Stop looking further.
737    if not consistent:
738      break
739
740  return consistent
741
742
743def get_target_hash(target_filepath):
744  """
745  <Purpose>
746    Compute the hash of 'target_filepath'. This is useful in conjunction with
747    the "path_hash_prefixes" attribute in a delegated targets role, which tells
748    us which paths it is implicitly responsible for.
749
750    The repository may optionally organize targets into hashed bins to ease
751    target delegations and role metadata management.  The use of consistent
752    hashing allows for a uniform distribution of targets into bins.
753
754  <Arguments>
755    target_filepath:
756      The path to the target file on the repository. This will be relative to
757      the 'targets' (or equivalent) directory on a given mirror.
758
759  <Exceptions>
760    None.
761
762  <Side Effects>
763    None.
764
765  <Returns>
766    The hash of 'target_filepath'.
767  """
768
769  # Does 'target_filepath' have the correct format?
770  # Ensure the arguments have the appropriate number of objects and object
771  # types, and that all dict keys are properly named.
772  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
773  securesystemslib.formats.RELPATH_SCHEMA.check_match(target_filepath)
774
775  # Calculate the hash of the filepath to determine which bin to find the
776  # target.  The client currently assumes the repository uses
777  # 'HASH_FUNCTION' to generate hashes and 'utf-8'.
778  digest_object = securesystemslib.hash.digest(HASH_FUNCTION)
779  encoded_target_filepath = target_filepath.encode('utf-8')
780  digest_object.update(encoded_target_filepath)
781  target_filepath_hash = digest_object.hexdigest()
782
783  return target_filepath_hash
784
785
786_json_module = None
787
788def import_json():
789  """
790  <Purpose>
791    Tries to import json module. We used to fall back to the simplejson module,
792    but we have dropped support for that module. We are keeping this interface
793    intact for backwards compatibility.
794
795  <Arguments>
796    None.
797
798  <Exceptions>
799    ImportError: on failure to import the json module.
800
801  <Side Effects>
802    None.
803
804  <Return>
805    json module
806  """
807
808  global _json_module
809
810  if _json_module is not None:
811    return _json_module
812
813  else:
814    try:
815      module = __import__('json')
816
817    # The 'json' module is available in Python > 2.6, and thus this exception
818    # should not occur in all supported Python installations (> 2.6) of TUF.
819    except ImportError: #pragma: no cover
820      raise ImportError('Could not import the json module')
821
822    else:
823      _json_module = module
824      return module
825
826json = import_json()
827
828
829def load_json_string(data):
830  """
831  <Purpose>
832    Deserialize 'data' (JSON string) to a Python object.
833
834  <Arguments>
835    data:
836      A JSON string.
837
838  <Exceptions>
839    securesystemslib.exceptions.Error, if 'data' cannot be deserialized to a
840    Python object.
841
842  <Side Effects>
843    None.
844
845  <Returns>
846    Deserialized object.  For example, a dictionary.
847  """
848
849  deserialized_object = None
850
851  try:
852    deserialized_object = json.loads(data)
853
854  except TypeError:
855    message = 'Invalid JSON string: ' + repr(data)
856    raise securesystemslib.exceptions.Error(message)
857
858  except ValueError:
859    message = 'Cannot deserialize to a Python object: ' + repr(data)
860    raise securesystemslib.exceptions.Error(message)
861
862  else:
863    return deserialized_object
864
865
866def load_json_file(filepath):
867  """
868  <Purpose>
869    Deserialize a JSON object from a file containing the object.
870
871  <Arguments>
872    filepath:
873      Absolute path of JSON file.
874
875  <Exceptions>
876    securesystemslib.exceptions.FormatError: If 'filepath' is improperly
877    formatted.
878
879    securesystemslib.exceptions.Error: If 'filepath' cannot be deserialized to
880    a Python object.
881
882    IOError in case of runtime IO exceptions.
883
884  <Side Effects>
885    None.
886
887  <Return>
888    Deserialized object.  For example, a dictionary.
889  """
890
891  # Making sure that the format of 'filepath' is a path string.
892  # securesystemslib.exceptions.FormatError is raised on incorrect format.
893  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
894
895  deserialized_object = None
896
897  # The file is mostly likely gzipped.
898  if filepath.endswith('.gz'):
899    logger.debug('gzip.open(' + str(filepath) + ')')
900    fileobject = six.StringIO(gzip.open(filepath).read().decode('utf-8'))
901
902  else:
903    logger.debug('open(' + str(filepath) + ')')
904    fileobject = open(filepath)
905
906  try:
907    deserialized_object = json.load(fileobject)
908
909  except (ValueError, TypeError) as e:
910    raise securesystemslib.exceptions.Error('Cannot deserialize to a'
911      ' Python object: ' + repr(filepath))
912
913  else:
914    fileobject.close()
915    return deserialized_object
916
917  finally:
918    fileobject.close()
919
920
921def digests_are_equal(digest1, digest2):
922  """
923  <Purpose>
924    While protecting against timing attacks, compare the hexadecimal arguments
925    and determine if they are equal.
926
927  <Arguments>
928    digest1:
929      The first hexadecimal string value to compare.
930
931    digest2:
932      The second hexadecimal string value to compare.
933
934  <Exceptions>
935    securesystemslib.exceptions.FormatError: If the arguments are improperly
936    formatted.
937
938  <Side Effects>
939    None.
940
941  <Return>
942    Return True if 'digest1' is equal to 'digest2', False otherwise.
943  """
944
945  # Ensure the arguments have the appropriate number of objects and object
946  # types, and that all dict keys are properly named.
947  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
948  securesystemslib.formats.HEX_SCHEMA.check_match(digest1)
949  securesystemslib.formats.HEX_SCHEMA.check_match(digest2)
950
951  if len(digest1) != len(digest2):
952    return False
953
954  are_equal = True
955
956  for element in range(len(digest1)):
957    if digest1[element] != digest2[element]:
958      are_equal = False
959
960  return are_equal
961