diff --git a/examples/client_example/1.root.json b/examples/client_example/1.root.json new file mode 100644 index 0000000000..214d8db01b --- /dev/null +++ b/examples/client_example/1.root.json @@ -0,0 +1,87 @@ +{ + "signatures": [ + { + "keyid": "4e777de0d275f9d28588dd9a1606cc748e548f9e22b6795b7cb3f63f98035fcb", + "sig": "a337d6375fedd2eabfcd6c2ef6c8a9c3bb85dc5a857715f6a6bd41123e7670c4972d8548bcd7248154f3d864bf25f1823af59d74c459f41ea09a02db057ca1245612ebbdb97e782c501dc3e094f7fa8aa1402b03c6ed0635f565e2a26f9f543a89237e15a2faf0c267e2b34c3c38f2a43a28ddcdaf8308a12ead8c6dc47d1b762de313e9ddda8cc5bc25aea1b69d0e5b9199ca02f5dda48c3bff615fd12a7136d00634b9abc6e75c3256106c4d6f12e6c43f6195071355b2857bbe377ce028619b58837696b805040ce144b393d50a472531f430fadfb68d3081b6a8b5e49337e328c9a0a3f11e80b0bc8eb2dc6e78d1451dd857e6e6e6363c3fd14c590aa95e083c9bfc77724d78af86eb7a7ef635eeddaa353030c79f66b3ba9ea11fab456cfe896a826fdfb50a43cd444f762821aada9bcd7b022c0ee85b8768f960343d5a1d3d76374cc0ac9e12a500de0bf5d48569e5398cadadadab045931c398e3bcb6cec88af2437ba91959f956079cbed159fed3938016e6c3b5e446131f81cc5981" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": false, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "4e777de0d275f9d28588dd9a1606cc748e548f9e22b6795b7cb3f63f98035fcb": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "rsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0GjPoVrjS9eCqzoQ8VRe\nPkC0cI6ktiEgqPfHESFzyxyjC490Cuy19nuxPcJuZfN64MC48oOkR+W2mq4pM51i\nxmdG5xjvNOBRkJ5wUCc8fDCltMUTBlqt9y5eLsf/4/EoBU+zC4SW1iPU++mCsity\nfQQ7U6LOn3EYCyrkH51hZ/dvKC4o9TPYMVxNecJ3CL1q02Q145JlyjBTuM3Xdqsa\nndTHoXSRPmmzgB/1dL/c4QjMnCowrKW06mFLq9RAYGIaJWfM/0CbrOJpVDkATmEc\nMdpGJYDfW/sRQvRdlHNPo24ZW7vkQUCqdRxvnTWkK5U81y7RtjLt1yskbWXBIbOV\nz94GXsgyzANyCT9qRjHXDDz2mkLq+9I2iKtEqaEePcWRu3H6RLahpM/TxFzw684Y\nR47weXdDecPNxWyiWiyMGStRFP4Cg9trcwAGnEm1w8R2ggmWphznCd5dXGhPNjfA\na82yNFY8ubnOUVJOf0nXGg3Edw9iY3xyjJb2+nrsk5f3AgMBAAE=\n-----END PUBLIC KEY-----" + }, + "scheme": "rsassa-pss-sha256" + }, + "59a4df8af818e9ed7abe0764c0b47b4240952aa0d179b5b78346c470ac30278d": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd" + }, + "scheme": "ed25519" + }, + "65171251a9aff5a8b3143a813481cb07f6e0de4eb197c767837fe4491b739093": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "89f28bd4ede5ec3786ab923fd154f39588d20881903e69c7b08fb504c6750815" + }, + "scheme": "ed25519" + }, + "8a1c4a3ac2d515dec982ba9910c5fd79b91ae57f625b9cff25d06bf0a61c1758": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "82ccf6ac47298ff43bfa0cd639868894e305a99c723ff0515ae2e9856eb5bbf4" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "4e777de0d275f9d28588dd9a1606cc748e548f9e22b6795b7cb3f63f98035fcb" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "59a4df8af818e9ed7abe0764c0b47b4240952aa0d179b5b78346c470ac30278d" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "65171251a9aff5a8b3143a813481cb07f6e0de4eb197c767837fe4491b739093" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "8a1c4a3ac2d515dec982ba9910c5fd79b91ae57f625b9cff25d06bf0a61c1758" + ], + "threshold": 1 + } + }, + "spec_version": "1.0.0", + "version": 1 + } +} \ No newline at end of file diff --git a/examples/client_example/README.md b/examples/client_example/README.md new file mode 100644 index 0000000000..399c6d6b42 --- /dev/null +++ b/examples/client_example/README.md @@ -0,0 +1,26 @@ +# TUF Client Example + + +TUF Client Example, using ``python-tuf``. + +This TUF Client Example implements the following actions: + - Client Infrastructure Initialization + - Download target files from TUF Repository + +The example client expects to find a TUF repository running on localhost. We +can use the static metadata files in ``tests/repository_data/repository`` +to set one up. + +Run the repository using the Python3 built-in HTTP module, and keep this +session running. + +```console + $ python3 -m http.server -d tests/repository_data/repository + Serving HTTP on :: port 8000 (http://[::]:8000/) ... +``` + +How to use the TUF Client Example to download a target file. + +```console +$ ./client_example.py download file1.txt +``` diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py new file mode 100755 index 0000000000..4d6fc1c6fb --- /dev/null +++ b/examples/client_example/client_example.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +"""TUF Client Example""" + +# Copyright 2012 - 2017, New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +import argparse +import logging +import os +import shutil +from pathlib import Path + +from tuf.exceptions import RepositoryError +from tuf.ngclient import Updater + +# constants +BASE_URL = "http://127.0.0.1:8000" +DOWNLOAD_DIR = "./downloads" +METADATA_DIR = f"{Path.home()}/.local/share/python-tuf-client-example" +CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def init() -> None: + """Initialize local trusted metadata and create a directory for downloads""" + + if not os.path.isdir(DOWNLOAD_DIR): + os.mkdir(DOWNLOAD_DIR) + + if not os.path.isdir(METADATA_DIR): + os.makedirs(METADATA_DIR) + + if not os.path.isfile(f"{METADATA_DIR}/root.json"): + shutil.copy( + f"{CLIENT_EXAMPLE_DIR}/1.root.json", f"{METADATA_DIR}/root.json" + ) + print(f"Added trusted root in {METADATA_DIR}") + + else: + print(f"Found trusted root in {METADATA_DIR}") + + +def download(target: str) -> bool: + """ + Download the target file using ``ngclient`` Updater. + + The Updater refreshes the top-level metadata, get the target information, + verifies if the target is already cached, and in case it is not cached, + downloads the target file. + + Returns: + A boolean indicating if process was successful + """ + try: + updater = Updater( + repository_dir=METADATA_DIR, + metadata_base_url=f"{BASE_URL}/metadata/", + target_base_url=f"{BASE_URL}/targets/", + target_dir=DOWNLOAD_DIR, + ) + updater.refresh() + + info = updater.get_targetinfo(target) + + if info is None: + print(f"Target {target} not found") + return True + + path = updater.find_cached_target(info) + if path: + print(f"Target is available in {path}") + return True + + path = updater.download_target(info) + print(f"Target downloaded and available in {path}") + + except (OSError, RepositoryError) as e: + print(str(e)) + return False + + return True + + +def main() -> None: + """Main TUF Client Example function""" + + client_args = argparse.ArgumentParser(description="TUF Client Example") + + # Global arguments + client_args.add_argument( + "-v", + "--verbose", + help="Output verbosity level (-v, -vv, ...)", + action="count", + default=0, + ) + + # Sub commands + sub_command = client_args.add_subparsers(dest="sub_command") + + # Download + download_parser = sub_command.add_parser( + "download", + help="Download a target file", + ) + + download_parser.add_argument( + "target", + metavar="TARGET", + help="Target file", + ) + + command_args = client_args.parse_args() + + if command_args.verbose == 0: + loglevel = logging.ERROR + elif command_args.verbose == 1: + loglevel = logging.WARNING + elif command_args.verbose == 2: + loglevel = logging.INFO + else: + loglevel = logging.DEBUG + + logging.basicConfig(level=loglevel) + + # initialize the TUF Client Example infrastructure + init() + + if command_args.sub_command == "download": + download(command_args.target) + + else: + client_args.print_help() + + +if __name__ == "__main__": + main()