diff --git a/examples/repo/repo_workflow_example.py b/examples/repo/repo_workflow_example.py index 61e5ed5..dca5117 100644 --- a/examples/repo/repo_workflow_example.py +++ b/examples/repo/repo_workflow_example.py @@ -1,5 +1,6 @@ import copy import logging +import os import pathlib import secrets # from python 3.9+ we can use random.randbytes import shutil @@ -16,17 +17,23 @@ """ +This script was based on the python-tuf basic repo example [1]. The script generates a +complete example repository, including key pairs and example data. It illustrates +some common repository operations. + +NOTE: This script creates subdirectories and files in the tufup/examples/repo directory. + +NOTE: This script was also used to generate the test data in tests/data. + NOTE: The repo content can be served for local testing as follows: python -m http.server -d examples/repo/repository -NOTE: This script creates subdirectories and files in the -tufup/examples/repo directory. - NOTE: When running this script in PyCharm, ensure "Emulate terminal in output -console" is enabled in the run configuration, otherwise the encryption -passwords cannot be entered. +console" is enabled in the run configuration, otherwise the encryption passwords +cannot be entered. +[1]: https://github.com/theupdateframework/python-tuf/blob/develop/examples/manual_repo/basic_repo.py """ logger = logging.getLogger(__name__) @@ -45,12 +52,23 @@ TARGETS_DIR = REPO_DIR / DEFAULT_TARGETS_DIR_NAME # Settings -EXPIRATION_DAYS = dict(root=365, targets=100, snapshot=7, timestamp=1) +_TEST_EXPIRATION = int(os.getenv('TEST_EXPIRATION', 0)) # for creating test repo data +if _TEST_EXPIRATION: + logger.warning(f'using TEST_EXPIRATION: {_TEST_EXPIRATION} days') +EXPIRATION_DAYS = dict( + root=_TEST_EXPIRATION or 365, + targets=_TEST_EXPIRATION or 100, + snapshot=_TEST_EXPIRATION or 7, + timestamp=_TEST_EXPIRATION or 1, +) THRESHOLDS = dict(root=2, targets=1, snapshot=1, timestamp=1) KEY_MAP = copy.deepcopy(DEFAULT_KEY_MAP) KEY_MAP['root'].append('root_two') # use two keys for root ENCRYPTED_KEYS = ['root', 'root_two', 'targets'] +# Custom metadata +DUMMY_METADATA = dict(whatever='important') + # Create repository instance repo = Repository( app_name=APP_NAME, @@ -122,9 +140,13 @@ dummy_file_content += secrets.token_bytes(dummy_delta_size) dummy_file_path.write_bytes(dummy_file_content) - # Create archive and patch and register the new update (here we sign - # everything at once, for convenience) - repo.add_bundle(new_version=new_version, new_bundle_dir=dummy_bundle_dir) + # Create archive and patch and register the new update (here we sign everything + # at once, for convenience) + repo.add_bundle( + new_version=new_version, + new_bundle_dir=dummy_bundle_dir, + custom_metadata_for_patch=DUMMY_METADATA, # just to point out the option + ) repo.publish_changes(private_key_dirs=[OFFLINE_DIR_1, OFFLINE_DIR_2, ONLINE_DIR]) # Time goes by diff --git a/src/tufup/client.py b/src/tufup/client.py index 5ccebd2..a86c121 100644 --- a/src/tufup/client.py +++ b/src/tufup/client.py @@ -66,8 +66,8 @@ def trusted_target_metas(self) -> list: _trusted_target_metas = [] if self._trusted_set.targets: _trusted_target_metas = [ - TargetMeta(target_path=key) - for key in self._trusted_set.targets.signed.targets.keys() + TargetMeta(target_path=key, custom=target.custom) + for key, target in self._trusted_set.targets.signed.targets.items() ] logger.debug(f'{len(_trusted_target_metas)} TargetMeta objects created') else: diff --git a/src/tufup/common.py b/src/tufup/common.py index 1039626..5620dbe 100644 --- a/src/tufup/common.py +++ b/src/tufup/common.py @@ -12,6 +12,29 @@ SUFFIX_PATCH = '.patch' +def _immutable(value): + """ + Make value immutable, recursively, so the result is hashable. + + Applies to (nested) dict, list, set, and bytearray [1] mutable sequence types. + Everything else is passed through unaltered, so the more exotic mutable types are + not supported. + + [1]: https://peps.python.org/pep-3137/ + """ + # recursive cases + if isinstance(value, dict): + return tuple((k, _immutable(v)) for k, v in value.items()) + elif isinstance(value, list): + return tuple(_immutable(v) for v in value) + elif isinstance(value, set): + return frozenset(_immutable(v) for v in value) + elif isinstance(value, bytearray): + return bytes(value) + # base case + return value + + class TargetMeta(object): filename_pattern = '{name}-{version}{suffix}' filename_regex = re.compile( @@ -24,6 +47,7 @@ def __init__( name: Optional[str] = None, version: Optional[str] = None, is_archive: Optional[bool] = True, + custom: Optional[dict] = None, ): """ Initialize either with target_path, or with name, version, archive. @@ -42,6 +66,7 @@ def __init__( logger.critical( f'invalid filename "{self.filename}": whitespace not allowed' ) + self.custom = custom def __str__(self): return str(self.target_path_str) @@ -57,10 +82,10 @@ def __hash__(self): https://docs.python.org/3/glossary.html#term-hashable """ - return hash(tuple(self.__dict__.items())) + return hash(_immutable(self.__dict__)) def __eq__(self, other): - if type(other) != type(self): + if type(other) is not type(self): return NotImplemented return vars(self) == vars(other) @@ -70,7 +95,7 @@ def __lt__(self, other): without having to specify an explicit sorting key. Note this disregards app name, platform, and suffixes. """ - if type(other) != type(self): + if type(other) is not type(self): return NotImplemented return self.version < other.version diff --git a/src/tufup/repo/__init__.py b/src/tufup/repo/__init__.py index 30b8890..84f01ec 100644 --- a/src/tufup/repo/__init__.py +++ b/src/tufup/repo/__init__.py @@ -358,6 +358,7 @@ def add_or_update_target( self, local_path: Union[pathlib.Path, str], url_path_segments: Optional[List[str]] = None, + custom: Optional[dict] = None, ): # based on python-tuf basic_repo.py local_path = pathlib.Path(local_path) @@ -365,9 +366,13 @@ def add_or_update_target( url_path_segments = url_path_segments or [] url_path_segments.append(local_path.name) url_path = '/'.join(url_path_segments) + # create targetfile instance target_file_info = TargetFile.from_file( target_file_path=url_path, local_path=str(local_path) ) + if custom: + # todo: should we verify that custom is a dict? + target_file_info.unrecognized_fields['custom'] = custom # note we assume self.targets has been initialized self.targets.signed.targets[url_path] = target_file_info @@ -709,6 +714,8 @@ def add_bundle( new_bundle_dir: Union[pathlib.Path, str], new_version: Optional[str] = None, skip_patch: bool = False, + custom_metadata_for_archive: Optional[dict] = None, + custom_metadata_for_patch: Optional[dict] = None, ): """ Adds a new application bundle to the local repository. @@ -740,14 +747,18 @@ def add_bundle( latest_archive = self.roles.get_latest_archive() if not latest_archive or latest_archive.version < new_archive.version: # register new archive - self.roles.add_or_update_target(local_path=new_archive.path) + self.roles.add_or_update_target( + local_path=new_archive.path, custom=custom_metadata_for_archive + ) # create patch, if possible, and register that too if latest_archive and not skip_patch: patch_path = Patcher.create_patch( src_path=self.targets_dir / latest_archive.path, dst_path=self.targets_dir / new_archive.path, ) - self.roles.add_or_update_target(local_path=patch_path) + self.roles.add_or_update_target( + local_path=patch_path, custom=custom_metadata_for_patch + ) def remove_latest_bundle(self): """ diff --git a/tests/data/README.md b/tests/data/README.md index 1fb1581..b6456f5 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -1,3 +1,3 @@ -Large parts of the test data were copied verbatim from the `python-tuf` [repository_data][1] folder. +These test data were generated using the examples/repo/repo_workflow_example.py script. -[1]: https://github.com/theupdateframework/python-tuf/tree/develop/tests/repository_data +(expiration dates were set to some time far in the future) diff --git a/tests/data/keystore/root b/tests/data/keystore/root deleted file mode 100644 index 29d0f99..0000000 --- a/tests/data/keystore/root +++ /dev/null @@ -1 +0,0 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid": "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "0e492fadf5643a11049e2d7e59db6b8fc766945315f5bdc5648bd94fe2b427cb", "private": "b0ebbcf6d898c0d6804cf64bc5f07a40b8c417c24c17a4258c010e8cfc3311c2"}} \ No newline at end of file diff --git a/tests/data/keystore/root.pub b/tests/data/keystore/root.pub old mode 100644 new mode 100755 index 8ef2650..6a0f2d0 --- a/tests/data/keystore/root.pub +++ b/tests/data/keystore/root.pub @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "0e492fadf5643a11049e2d7e59db6b8fc766945315f5bdc5648bd94fe2b427cb"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f5033e2659886185ceedec69e2cfee0f348ea63dfffafd5f8566d001b45c470d"}} \ No newline at end of file diff --git a/tests/data/keystore/root_two.pub b/tests/data/keystore/root_two.pub new file mode 100755 index 0000000..586f098 --- /dev/null +++ b/tests/data/keystore/root_two.pub @@ -0,0 +1 @@ +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "c8eaa5bf0f26e7247c965388a7ce7d3a25113899139c3d9bd2dbbb5e95577397"}} \ No newline at end of file diff --git a/tests/data/keystore/snapshot b/tests/data/keystore/snapshot deleted file mode 100644 index 67fe10f..0000000 --- a/tests/data/keystore/snapshot +++ /dev/null @@ -1 +0,0 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid": "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f3be5f4d498ca80145c84f6ca1d443b139efcbd2dde91219396aa3a0b5d7a987", "private": "49c86c11869ed7219faf0a53c8fceac444262264dd5713506823babb16ec1adb"}} \ No newline at end of file diff --git a/tests/data/keystore/snapshot.pub b/tests/data/keystore/snapshot.pub old mode 100644 new mode 100755 index 2d39366..1636d2e --- a/tests/data/keystore/snapshot.pub +++ b/tests/data/keystore/snapshot.pub @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "f3be5f4d498ca80145c84f6ca1d443b139efcbd2dde91219396aa3a0b5d7a987"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "41bf1adabf1f564de734fa5fb584a65b943317978a4dcbe39bab03ee722ee73f"}} \ No newline at end of file diff --git a/tests/data/keystore/targets b/tests/data/keystore/targets deleted file mode 100644 index 973a143..0000000 --- a/tests/data/keystore/targets +++ /dev/null @@ -1 +0,0 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid": "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "2c12e0cd2837cfe0448d77c93c0258ba8cbc2af89351b9b0bad3aae43e6433bf", "private": "f7862bb13dd83cdf9207a5899ad8c38fe783715606fe3f86372634b6a6ca598c"}} \ No newline at end of file diff --git a/tests/data/keystore/targets.pub b/tests/data/keystore/targets.pub old mode 100644 new mode 100755 index 6287cf1..0c0fd99 --- a/tests/data/keystore/targets.pub +++ b/tests/data/keystore/targets.pub @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "2c12e0cd2837cfe0448d77c93c0258ba8cbc2af89351b9b0bad3aae43e6433bf"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "a27a0209711787a4227cbfed23735a75b5f7f5cb0cd6acbf7a239fa2c3535434"}} \ No newline at end of file diff --git a/tests/data/keystore/timestamp b/tests/data/keystore/timestamp deleted file mode 100644 index b1bf7a5..0000000 --- a/tests/data/keystore/timestamp +++ /dev/null @@ -1 +0,0 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid": "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "405dc59918fab6c3c489e781f06e8a57dd0061d4c62fc071c7fc5b17c4c70209", "private": "f6817fc4fca4b092dba977caf329cacb5acd6a0fc474eb518a8cc983e0621fd2"}} \ No newline at end of file diff --git a/tests/data/keystore/timestamp.pub b/tests/data/keystore/timestamp.pub old mode 100644 new mode 100755 index 3be80cc..5175c51 --- a/tests/data/keystore/timestamp.pub +++ b/tests/data/keystore/timestamp.pub @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "405dc59918fab6c3c489e781f06e8a57dd0061d4c62fc071c7fc5b17c4c70209"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "2ec5e87c77fe70d918d92a1d849f4ec12907a34cf208123bbbc6d1e4bd584885"}} \ No newline at end of file diff --git a/tests/data/repository/metadata/1.root.json b/tests/data/repository/metadata/1.root.json old mode 100644 new mode 100755 index 442f680..417895f --- a/tests/data/repository/metadata/1.root.json +++ b/tests/data/repository/metadata/1.root.json @@ -1,40 +1,51 @@ { "signatures": [ { - "keyid": "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce", - "sig": "db5d5329bdf0fddc9e353d882276ddddd74ce5e33d0a3f8abe451f5498e1a09af866259d82a41d7b8afa2b7c9eb2de7d9bd81b08114d6c04cb419593d3884a06" + "keyid": "104c43225506bf7637a0061775a0d23ca8693e6bb4b270bc9ee9664259eb77d8", + "sig": "aa37e6a5e46938eb7c72054f2f2ff929e949283be67149c2a4fe481e51b91d8cc16876cbce03619af1d0b331ebf1d72ec368069ca49cca8d95a96eeaa06bfc07" + }, + { + "keyid": "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "sig": "b70196c013a883d0ae5fede183e1c49556ee26fecb0798968e41a391121c39ab229ed2e1f7067760232aeac0b709ecf48a29df34f0184349c5d96f4e9be91703" } ], "signed": { "_type": "root", "consistent_snapshot": false, - "expires": "2032-05-07T15:17:51Z", + "expires": "2051-06-24T09:37:39Z", "keys": { - "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659": { + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03": { + "keytype": "ed25519", + "keyval": { + "public": "2ec5e87c77fe70d918d92a1d849f4ec12907a34cf208123bbbc6d1e4bd584885" + }, + "scheme": "ed25519" + }, + "104c43225506bf7637a0061775a0d23ca8693e6bb4b270bc9ee9664259eb77d8": { "keytype": "ed25519", "keyval": { - "public": "f3be5f4d498ca80145c84f6ca1d443b139efcbd2dde91219396aa3a0b5d7a987" + "public": "c8eaa5bf0f26e7247c965388a7ce7d3a25113899139c3d9bd2dbbb5e95577397" }, "scheme": "ed25519" }, - "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb": { + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1": { "keytype": "ed25519", "keyval": { - "public": "2c12e0cd2837cfe0448d77c93c0258ba8cbc2af89351b9b0bad3aae43e6433bf" + "public": "a27a0209711787a4227cbfed23735a75b5f7f5cb0cd6acbf7a239fa2c3535434" }, "scheme": "ed25519" }, - "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4": { + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a": { "keytype": "ed25519", "keyval": { - "public": "405dc59918fab6c3c489e781f06e8a57dd0061d4c62fc071c7fc5b17c4c70209" + "public": "41bf1adabf1f564de734fa5fb584a65b943317978a4dcbe39bab03ee722ee73f" }, "scheme": "ed25519" }, - "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce": { + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f": { "keytype": "ed25519", "keyval": { - "public": "0e492fadf5643a11049e2d7e59db6b8fc766945315f5bdc5648bd94fe2b427cb" + "public": "f5033e2659886185ceedec69e2cfee0f348ea63dfffafd5f8566d001b45c470d" }, "scheme": "ed25519" } @@ -42,30 +53,31 @@ "roles": { "root": { "keyids": [ - "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce" + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "104c43225506bf7637a0061775a0d23ca8693e6bb4b270bc9ee9664259eb77d8" ], - "threshold": 1 + "threshold": 2 }, "snapshot": { "keyids": [ - "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659" + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a" ], "threshold": 1 }, "targets": { "keyids": [ - "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb" + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1" ], "threshold": 1 }, "timestamp": { "keyids": [ - "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4" + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03" ], "threshold": 1 } }, - "spec_version": "1.0.29", + "spec_version": "1.0.31", "version": 1 } } \ No newline at end of file diff --git a/tests/data/repository/metadata/2.root.json b/tests/data/repository/metadata/2.root.json new file mode 100755 index 0000000..83a65f4 --- /dev/null +++ b/tests/data/repository/metadata/2.root.json @@ -0,0 +1,87 @@ +{ + "signatures": [ + { + "keyid": "104c43225506bf7637a0061775a0d23ca8693e6bb4b270bc9ee9664259eb77d8", + "sig": "629f1d19e28f6a217b728509222b565ca7168be8cc094fed2e9c547c35da39b127b9914b49628789abef754d0615147aada1377af2fc6355ff49a42f31253e0a" + }, + { + "keyid": "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "sig": "05cd311026a0e5cece1d6d54b6b7f957f4824a704b7a319619621448a9ccb65ad9b1068429cf813aace000887eacf7a20000bacbc52777c9b11e9641d6f78f0c" + }, + { + "keyid": "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f", + "sig": "933b34c60eeca55793e875e45993397ec2447608c3d73777f6e20b452223ab949a4c1c2b0e9a9f2374bd7ac9cdc06f397407320b1039ff256e21ea52500a6106" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": false, + "expires": "2051-06-24T09:37:39Z", + "keys": { + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03": { + "keytype": "ed25519", + "keyval": { + "public": "2ec5e87c77fe70d918d92a1d849f4ec12907a34cf208123bbbc6d1e4bd584885" + }, + "scheme": "ed25519" + }, + "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f": { + "keytype": "ed25519", + "keyval": { + "public": "8ffc27373c8e9c5e32344b16d1b7f50a44323da6df4855deb6eadf8eb744eea8" + }, + "scheme": "ed25519" + }, + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1": { + "keytype": "ed25519", + "keyval": { + "public": "a27a0209711787a4227cbfed23735a75b5f7f5cb0cd6acbf7a239fa2c3535434" + }, + "scheme": "ed25519" + }, + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a": { + "keytype": "ed25519", + "keyval": { + "public": "41bf1adabf1f564de734fa5fb584a65b943317978a4dcbe39bab03ee722ee73f" + }, + "scheme": "ed25519" + }, + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f": { + "keytype": "ed25519", + "keyval": { + "public": "f5033e2659886185ceedec69e2cfee0f348ea63dfffafd5f8566d001b45c470d" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f" + ], + "threshold": 2 + }, + "snapshot": { + "keyids": [ + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03" + ], + "threshold": 1 + } + }, + "spec_version": "1.0.31", + "version": 2 + } +} \ No newline at end of file diff --git a/tests/data/repository/metadata/root.json b/tests/data/repository/metadata/root.json old mode 100644 new mode 100755 index 442f680..83a65f4 --- a/tests/data/repository/metadata/root.json +++ b/tests/data/repository/metadata/root.json @@ -1,40 +1,55 @@ { "signatures": [ { - "keyid": "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce", - "sig": "db5d5329bdf0fddc9e353d882276ddddd74ce5e33d0a3f8abe451f5498e1a09af866259d82a41d7b8afa2b7c9eb2de7d9bd81b08114d6c04cb419593d3884a06" + "keyid": "104c43225506bf7637a0061775a0d23ca8693e6bb4b270bc9ee9664259eb77d8", + "sig": "629f1d19e28f6a217b728509222b565ca7168be8cc094fed2e9c547c35da39b127b9914b49628789abef754d0615147aada1377af2fc6355ff49a42f31253e0a" + }, + { + "keyid": "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "sig": "05cd311026a0e5cece1d6d54b6b7f957f4824a704b7a319619621448a9ccb65ad9b1068429cf813aace000887eacf7a20000bacbc52777c9b11e9641d6f78f0c" + }, + { + "keyid": "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f", + "sig": "933b34c60eeca55793e875e45993397ec2447608c3d73777f6e20b452223ab949a4c1c2b0e9a9f2374bd7ac9cdc06f397407320b1039ff256e21ea52500a6106" } ], "signed": { "_type": "root", "consistent_snapshot": false, - "expires": "2032-05-07T15:17:51Z", + "expires": "2051-06-24T09:37:39Z", "keys": { - "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659": { + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03": { + "keytype": "ed25519", + "keyval": { + "public": "2ec5e87c77fe70d918d92a1d849f4ec12907a34cf208123bbbc6d1e4bd584885" + }, + "scheme": "ed25519" + }, + "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f": { "keytype": "ed25519", "keyval": { - "public": "f3be5f4d498ca80145c84f6ca1d443b139efcbd2dde91219396aa3a0b5d7a987" + "public": "8ffc27373c8e9c5e32344b16d1b7f50a44323da6df4855deb6eadf8eb744eea8" }, "scheme": "ed25519" }, - "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb": { + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1": { "keytype": "ed25519", "keyval": { - "public": "2c12e0cd2837cfe0448d77c93c0258ba8cbc2af89351b9b0bad3aae43e6433bf" + "public": "a27a0209711787a4227cbfed23735a75b5f7f5cb0cd6acbf7a239fa2c3535434" }, "scheme": "ed25519" }, - "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4": { + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a": { "keytype": "ed25519", "keyval": { - "public": "405dc59918fab6c3c489e781f06e8a57dd0061d4c62fc071c7fc5b17c4c70209" + "public": "41bf1adabf1f564de734fa5fb584a65b943317978a4dcbe39bab03ee722ee73f" }, "scheme": "ed25519" }, - "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce": { + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f": { "keytype": "ed25519", "keyval": { - "public": "0e492fadf5643a11049e2d7e59db6b8fc766945315f5bdc5648bd94fe2b427cb" + "public": "f5033e2659886185ceedec69e2cfee0f348ea63dfffafd5f8566d001b45c470d" }, "scheme": "ed25519" } @@ -42,30 +57,31 @@ "roles": { "root": { "keyids": [ - "bd7b600ecf8443b36566a170785cb66a400fee7b5af2c6c693e60fe4f8207cce" + "eb456bc4372b9aef1aea4790911d748a741d27ad0bd0eabcfe41e7fe3c6e9a8f", + "1784b06ef8e18f906f3fd62f2fd81aa088bb58317da624e10b5a3ecb72bd662f" ], - "threshold": 1 + "threshold": 2 }, "snapshot": { "keyids": [ - "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659" + "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a" ], "threshold": 1 }, "targets": { "keyids": [ - "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb" + "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1" ], "threshold": 1 }, "timestamp": { "keyids": [ - "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4" + "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03" ], "threshold": 1 } }, - "spec_version": "1.0.29", - "version": 1 + "spec_version": "1.0.31", + "version": 2 } } \ No newline at end of file diff --git a/tests/data/repository/metadata/snapshot.json b/tests/data/repository/metadata/snapshot.json old mode 100644 new mode 100755 index d6d5ee0..309f851 --- a/tests/data/repository/metadata/snapshot.json +++ b/tests/data/repository/metadata/snapshot.json @@ -1,19 +1,19 @@ { "signatures": [ { - "keyid": "0fac4d0180fffcecd7fb6832487e314fbf3cee050e3aed64a4bf60879a053659", - "sig": "b520e2cd435b697e167b4e9edc5c1b4850942abee150851351dd2ad2938259f393a96fc888128180eb7fc257953c1f53622ca447f6c8bac63b23dc38e412960b" + "keyid": "5fcbe7c4faa87ab25bea551c0c4b0ac6e47a07caf5e7633314a784c54ad2ea8a", + "sig": "814c447447a834e1072e5a76c5b91d3cd2234c52ee63789c90f05f8ac7679a5e6e10149c1cacfd2fe92bb1f3db186374c8acc28abc9d96d1bd2afa46540a4200" } ], "signed": { "_type": "snapshot", - "expires": "2032-05-07T15:17:51Z", + "expires": "2051-06-24T09:37:39Z", "meta": { "targets.json": { - "version": 4 + "version": 6 } }, - "spec_version": "1.0.29", - "version": 4 + "spec_version": "1.0.31", + "version": 7 } } \ No newline at end of file diff --git a/tests/data/repository/metadata/targets.json b/tests/data/repository/metadata/targets.json old mode 100644 new mode 100755 index 0b1f978..24b44a4 --- a/tests/data/repository/metadata/targets.json +++ b/tests/data/repository/metadata/targets.json @@ -1,58 +1,67 @@ { "signatures": [ { - "keyid": "40e032f119d90855f540d23cbd364388de5f622cf868cf5c767df661a8678bcb", - "sig": "a89f36cc8fbf81947886f526865c3489da0e1dcde93a3e493946948b625d059cbbf5cb79759e1c5989e359f806b336605c8e56319f30d1f0ea7aa23746453c0c" + "keyid": "3515ef592c09ddb3a09da0096802afc26852dc7a1978cb1c99fbe3a6f5c0c1a1", + "sig": "dd175aeed38f8a1a844507fa465e36be5577920becd6fd44d424de3b8caac4e4d9ac9b3afd2813e12b8412ff963495d16bdff0e4035b98d7026518018aadd005" } ], "signed": { "_type": "targets", - "expires": "2032-05-07T15:17:51Z", - "spec_version": "1.0.29", + "expires": "2051-06-24T09:37:39Z", + "spec_version": "1.0.31", "targets": { "example_app-1.0.tar.gz": { "hashes": { - "sha256": "2e9b081bd1e904ffb1d6c804406044e6b8a3e645abcc8aaa7b850cf44c7b16d6" + "sha256": "223dfd468edbe36256dc119f8477ac4025279ae3705dec04a32002e81b10fd16" }, - "length": 101417 + "length": 101613 }, "example_app-2.0.patch": { + "custom": { + "whatever": "important" + }, "hashes": { - "sha256": "033a3181db239a60e99f113b8bd7365334aeba86b0cc4a8e45561d03d0133001" + "sha256": "f7ee90e00fa69d5832eeee193f9b6bb2d32ff028c413f47fbaf853b3d2add27f" }, - "length": 20475 + "length": 18709 }, "example_app-2.0.tar.gz": { "hashes": { - "sha256": "1dddab8403835e37166ef950776cd89ede0adebd5f62b1779c4a15929805827f" + "sha256": "d85f423a56427e522ac4d093a6ce94abcc2cc32f99b80a39b87832d8e4ba9ad8" }, - "length": 101537 + "length": 101744 }, "example_app-3.0rc0.patch": { + "custom": { + "whatever": "important" + }, "hashes": { - "sha256": "85519146d28c954d9263b04a959002feafaf6fbda1e15f5ecdc1e76bf06e0ce7" + "sha256": "dcf2ce8ca9fc0ccc0e541fb0fca97a7772374177197a41fa4cf17653ef850956" }, - "length": 20310 + "length": 6458 }, "example_app-3.0rc0.tar.gz": { "hashes": { - "sha256": "edbfc88b5a2ed2da2c8c949958187b99e9d58adf301b20524331bdaaaa450e41" + "sha256": "d7fa6ddd397282e8fa81924a31f340ebbcb8c082604c0549a38f5882cd3716c6" }, - "length": 101628 + "length": 101841 }, "example_app-4.0a0.patch": { + "custom": { + "whatever": "important" + }, "hashes": { - "sha256": "d4e059c936db10eff5a51e21b8a2c2ead3c27230dac1e75c4f5ed959eacbb37e" + "sha256": "0fc6918d6d0757e234fe550e26bca659503166da3d3f493ec6bdbb5281b356ce" }, - "length": 102335 + "length": 102567 }, "example_app-4.0a0.tar.gz": { "hashes": { - "sha256": "0393ca32f4081297da7e7a1b5bf87eda9c94c4e2ccaedb55a6befd539da3687c" + "sha256": "05304765a0cb4e40cbd7ca2587aeb5f0db36cd9b617921e0d141bb91d3304e2c" }, - "length": 101423 + "length": 101642 } }, - "version": 4 + "version": 6 } } \ No newline at end of file diff --git a/tests/data/repository/metadata/timestamp.json b/tests/data/repository/metadata/timestamp.json old mode 100644 new mode 100755 index 4c7b309..6d1c34e --- a/tests/data/repository/metadata/timestamp.json +++ b/tests/data/repository/metadata/timestamp.json @@ -1,19 +1,19 @@ { "signatures": [ { - "keyid": "8e7d4ee2d147b4db84a208fc1e7eac3e586916afd1e3679c9d42dc89b58438e4", - "sig": "9934f3a0e112f74dde252917c6a15da47728a2b16153726eba7b62f31fc6c3057b572343b4caefba353d45f46008bc2fb56d81807ba4f7adb02b3426e220f709" + "keyid": "0eb56770be481c3a117f0487e7b6762edd0eaac7860ba85530dba400edf7de03", + "sig": "c1a5016ddce626ded84fe28b2c4830e50865c43bb70d3986a81f61f524d31aaaa80d3abe9282f434ec95e4a82c513942ce55f8e38a8c39cdf170ee7b5aec8a06" } ], "signed": { "_type": "timestamp", - "expires": "2032-05-07T15:17:51Z", + "expires": "2051-06-24T09:37:39Z", "meta": { "snapshot.json": { - "version": 4 + "version": 7 } }, - "spec_version": "1.0.29", - "version": 4 + "spec_version": "1.0.31", + "version": 7 } } \ No newline at end of file diff --git a/tests/data/repository/targets/example_app-1.0.tar.gz b/tests/data/repository/targets/example_app-1.0.tar.gz index 7400f62..a1bca9a 100644 Binary files a/tests/data/repository/targets/example_app-1.0.tar.gz and b/tests/data/repository/targets/example_app-1.0.tar.gz differ diff --git a/tests/data/repository/targets/example_app-2.0.patch b/tests/data/repository/targets/example_app-2.0.patch index 9f1de79..1a9628a 100644 Binary files a/tests/data/repository/targets/example_app-2.0.patch and b/tests/data/repository/targets/example_app-2.0.patch differ diff --git a/tests/data/repository/targets/example_app-2.0.tar.gz b/tests/data/repository/targets/example_app-2.0.tar.gz index bf41d3f..e2695f0 100644 Binary files a/tests/data/repository/targets/example_app-2.0.tar.gz and b/tests/data/repository/targets/example_app-2.0.tar.gz differ diff --git a/tests/data/repository/targets/example_app-3.0rc0.patch b/tests/data/repository/targets/example_app-3.0rc0.patch index 1b4e086..3d0d978 100644 Binary files a/tests/data/repository/targets/example_app-3.0rc0.patch and b/tests/data/repository/targets/example_app-3.0rc0.patch differ diff --git a/tests/data/repository/targets/example_app-3.0rc0.tar.gz b/tests/data/repository/targets/example_app-3.0rc0.tar.gz index c42551c..6ecec76 100644 Binary files a/tests/data/repository/targets/example_app-3.0rc0.tar.gz and b/tests/data/repository/targets/example_app-3.0rc0.tar.gz differ diff --git a/tests/data/repository/targets/example_app-4.0a0.patch b/tests/data/repository/targets/example_app-4.0a0.patch index 4c6de28..8874b82 100644 Binary files a/tests/data/repository/targets/example_app-4.0a0.patch and b/tests/data/repository/targets/example_app-4.0a0.patch differ diff --git a/tests/data/repository/targets/example_app-4.0a0.tar.gz b/tests/data/repository/targets/example_app-4.0a0.tar.gz index 3750af6..f2b9a05 100644 Binary files a/tests/data/repository/targets/example_app-4.0a0.tar.gz and b/tests/data/repository/targets/example_app-4.0a0.tar.gz differ diff --git a/tests/test_client.py b/tests/test_client.py index ee2c0b3..984dabd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -97,6 +97,15 @@ def test_init(self): def test_trusted_target_metas(self): client = self.get_refreshed_client() self.assertTrue(client.trusted_target_metas) + # in the example data, only the patches have custom metadata, as defined in + # the repo_workflow_example.py script + for meta in client.trusted_target_metas: + with self.subTest(msg=meta): + if meta.is_patch: + self.assertTrue(meta.custom) + self.assertIsInstance(meta.custom, dict) + else: + self.assertIsNone(meta.custom) def test_get_targetinfo(self): client = self.get_refreshed_client() @@ -142,7 +151,8 @@ def test_check_for_updates(self): with patch.object(client, 'refresh', Mock()): for pre, expected in [(None, 1), ('a', 1), ('b', 2), ('rc', 2)]: with self.subTest(msg=pre): - self.assertTrue(client.check_for_updates(pre=pre)) + target_meta = client.check_for_updates(pre=pre) + self.assertTrue(target_meta) self.assertEqual(expected, len(client.new_targets)) if pre == 'a': self.assertTrue( @@ -152,6 +162,9 @@ def test_check_for_updates(self): self.assertTrue( all(item.is_patch for item in client.new_targets.keys()) ) + # verify that we can access custom metadata where needed + if target_meta.is_patch: + self.assertTrue(target_meta.custom) def test_check_for_updates_already_up_to_date(self): self.client_kwargs['current_version'] = '4.0a0' diff --git a/tests/test_common.py b/tests/test_common.py index 90e0175..90b80c6 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,11 +1,31 @@ import logging import pathlib +from typing import Hashable +from unittest import TestCase import bsdiff4 from packaging.version import Version from tests import TempDirTestCase -from tufup.common import Patcher, TargetMeta +from tufup.common import _immutable, Patcher, TargetMeta # noqa + + +class ImmutableTests(TestCase): + def test_immutable(self): + cases = [ + 'a', + b'b', + 1, + ('a', 1), + ['a', 1], + {'a', 1}, + bytearray(b'b'), + dict(a=1), + [dict(a=[dict(c={1})], b=bytearray(b'd'))], + ] + for case in cases: + with self.subTest(msg=case): + self.assertIsInstance(_immutable(case), Hashable) class TargetMetaTests(TempDirTestCase): diff --git a/tests/test_repo.py b/tests/test_repo.py index 4ed6170..5c43453 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -331,26 +331,33 @@ def test_add_or_update_target(self): # prepare roles = Roles(dir_path=self.temp_dir_path) roles.targets = Mock(signed=Mock(targets=dict())) - # test + # test (path must exist) filename = 'my_app.tar.gz' local_target_path = self.temp_dir_path / filename - # path must exist with self.assertRaises(FileNotFoundError): roles.add_or_update_target(local_path=local_target_path) + # test (path segments) local_target_path.write_bytes(b'some bytes') - # test - for segments, expected_url_path in [ + cases = [ (None, filename), - ([], filename), + ([], filename), # update (['a', 'b'], 'a/b/' + filename), - ]: - roles.add_or_update_target( - local_path=local_target_path, url_path_segments=segments - ) + (['a', 'b'], 'a/b/' + filename), # update with segments + ] + for segments, expected_url_path in cases: with self.subTest(msg=segments): + roles.add_or_update_target( + local_path=local_target_path, url_path_segments=segments + ) self.assertIsInstance( roles.targets.signed.targets[expected_url_path], TargetFile ) + # ensure update did not create new items + self.assertEqual(2, len(roles.targets.signed.targets)) + # test (custom) + custom = dict(something='whatever') + roles.add_or_update_target(local_path=local_target_path, custom=custom) + self.assertEqual(custom, roles.targets.signed.targets[filename].custom) def test_remove_target(self): # prepare @@ -698,20 +705,29 @@ def test_add_key(self): self.assertIn(new_key_name, repo.encrypted_keys) def test_add_bundle(self): + app_name = 'test' + version = '1.0' # prepare bundle_dir = self.temp_dir_path / 'dist' / 'test_app' bundle_dir.mkdir(parents=True) bundle_file = bundle_dir / 'dummy.exe' bundle_file.touch() repo = Repository( - app_name='test', + app_name=app_name, keys_dir=self.temp_dir_path / 'keystore', repo_dir=self.temp_dir_path / 'repo', ) repo.initialize() # todo: make test independent... # test - repo.add_bundle(new_version='1.0', new_bundle_dir=bundle_dir) + repo.add_bundle( + new_version=version, + new_bundle_dir=bundle_dir, + custom_metadata_for_archive=dict(whatever='something'), + custom_metadata_for_patch=None, + ) self.assertTrue((repo.metadata_dir / 'targets.json').exists()) + target_name = f'{app_name}-{version}.tar.gz' + self.assertTrue(repo.roles.targets.signed.targets[target_name].custom) def test_add_bundle_no_patch(self): # prepare