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