From af22a7a59ca918ff3226fec41d8e204ba8ca0cad Mon Sep 17 00:00:00 2001 From: Velichka Atanasova Date: Tue, 10 Aug 2021 09:13:33 +0300 Subject: [PATCH] Add an option to create TargetFile from data/file This is a repository tooling use case but also helpful when testing. It could be useful when we need to update the targets object. Signed-off-by: Velichka Atanasova --- tests/test_api.py | 40 ++++++++++++++++++++++++++ tuf/api/metadata.py | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 8a3045d44e..aff3b46531 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -50,6 +50,7 @@ import_ed25519_privatekey_from_file ) +from securesystemslib import hash as sslib_hash from securesystemslib.signer import ( SSlibSigner, Signature @@ -623,6 +624,45 @@ def test_length_and_hash_validation(self): file1_targetfile.verify_length_and_hashes, file1) + def test_targetfile_from_data(self): + data = b"Inline test content" + + # Test with valid hash algorithm(s) specified + targetfile_from_data = TargetFile.from_data(data, ['sha256']) + targetfile_from_data.verify_length_and_hashes(data) + + # Test with no algorithms specified + targetfile_from_data = TargetFile.from_data(data, []) + targetfile_from_data.verify_length_and_hashes(data) + + # Test with an unsupported algorithm + self.assertRaises( + exceptions.UnsupportedAlgorithmError, + TargetFile.from_data, + data, + ['123'] + ) + + + def test_targetfile_from_file(self): + # Test with an existing file + filepath = os.path.join(self.repo_dir, 'targets', 'file1.txt') + targetfile_from_file = TargetFile.from_file( + filepath, [sslib_hash.DEFAULT_HASH_ALGORITHM] + ) + + with open(filepath, "rb") as file: + targetfile_from_file.verify_length_and_hashes(file) + + # Test with a non-existing file + filepath = os.path.join(self.repo_dir, 'targets', 'file123.txt') + self.assertRaises( + FileNotFoundError, + TargetFile.from_file, + filepath, + [sslib_hash.DEFAULT_HASH_ALGORITHM] + ) + # Run unit test. if __name__ == '__main__': utils.configure_test_logging(sys.argv) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index a7d967d8d9..43d2c98884 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1123,6 +1123,74 @@ def to_dict(self) -> Dict[str, Any]: **self.unrecognized_fields, } + @classmethod + def from_file( + cls, filepath: str, hash_algorithms: List[str] + ) -> "TargetFile": + """Creates TargetFile object from a file. + + Attributes: + filepath: The path to the file to create the TaretFile object from. + hash_algorithms: The hash algorithms to create the hashes with. + If empty the securesystemslib default hash algorithm is used. + + Raises: + FileNotFoundError: The file doesn't exist. + """ + with open(filepath, "rb") as file: + return cls.from_data(file, hash_algorithms) + + @classmethod + def from_data( + cls, data: Union[bytes, BinaryIO], hash_algorithms: List[str] + ) -> "TargetFile": + """Creates TargetFile object from data. + + Attributes: + data: The data to create the TargetFile object from. + hash_algorithms: The hash algorithms to create the hashes with. + If empty the securesystemslib default hash algorithm is used. + + Raises: + UnsupportedAlgorithmError: The hash algorithms list + contains an unsupported algorithm. + """ + # Calculate the length + is_bytes = isinstance(data, bytes) + if is_bytes: + length = len(data) + else: + # If data is not bytes, assume it is a file object. + data.seek(0, io.SEEK_END) + length = data.tell() + + # Calculate the hashes for the given algorithms. + if len(hash_algorithms) == 0: + hash_algorithms.append(sslib_hash.DEFAULT_HASH_ALGORITHM) + + hashes = {} + for algorithm in hash_algorithms: + try: + if is_bytes: + digest_object = sslib_hash.digest(algorithm) + digest_object.update(data) + else: + # if data is not bytes, assume it is a file object + digest_object = sslib_hash.digest_fileobject( + data, algorithm + ) + except ( + sslib_exceptions.UnsupportedAlgorithmError, + sslib_exceptions.FormatError, + ) as e: + raise exceptions.UnsupportedAlgorithmError( + f"Unsupported algorithm '{algorithm}'" + ) from e + + hashes[algorithm] = digest_object.hexdigest() + + return cls(length, hashes) + def verify_length_and_hashes(self, data: Union[bytes, BinaryIO]) -> None: """Verifies that length and hashes of "data" match expected values.