From ffe2c11fcfed5e2a137e01ffaef2585e23c4dc95 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Mon, 15 Nov 2021 09:11:00 +0100 Subject: [PATCH 1/8] WIP: TUF Python Client Example/Tutorial It is a simple example of TUF ngclient implementation. This example contains a README.rst that is a tutorial/how-to-use this simple client using static test data from TUF repository. The code aims to be straightforward implementation, using basic concepts from Python and Command Line Interface. This is part of theupdateframework#1518 Signed-off-by: Kairo de Araujo --- examples/client_example/1.root.json | 87 +++++++++++++ examples/client_example/README.rst | 92 ++++++++++++++ examples/client_example/client_example.py | 145 ++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 examples/client_example/1.root.json create mode 100644 examples/client_example/README.rst create mode 100755 examples/client_example/client_example.py 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.rst b/examples/client_example/README.rst new file mode 100644 index 0000000000..5fc1c815d4 --- /dev/null +++ b/examples/client_example/README.rst @@ -0,0 +1,92 @@ +Python Client Example +##################### + +Introduction +============ + +Python Client Example, using ``python-tuf``. + +For information about installing ``python-tuf``, please refer to the +`Installation documentation `_. + + +Preparing +========= + +To have the example working in your machine, clone the ``python-tuf`` in your +system. + +.. code:: console + + $ git clone git@github.com:theupdateframework/python-tuf.git + + +Repository +========== + +As this example demonstrates how to use the ``python-tuf`` to build a +client application, the repository will use static files. + +The static files are available in the ``python-tuf`` repository, same as this. +The static repository files are in +``tests/repository_data/repository``. + +Run the repository using the Python3 built-in HTTP module, and keep this +session running. + +.. code:: console + + $ python3 -m http.server -d tests/repository_data/repository + Serving HTTP on :: port 8000 (http://[::]:8000/) ... + + +Client Example +============== + +The `source code is available entirely <./client_example.py>`_ in this +repository. + +How to use the Client Example: + +1. Initialize the Client + + .. code:: console + + $ ./client_example.py --init + + + This action is to create the client infrastructure properly. + + This infrastructure consists in: + - Metadata repository + - Download folder for targets + - Bootstrap 1.root.json + + +2. Download the ``file1.txt`` + + .. code:: console + + $ ./client_example.py download file1.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [INFO] File downloaded available in ./downloads/file2.txt. + + +3. Download a not available ``file_na.txt`` + + .. code:: console + + $ ./client_example.py download file_na.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [ERROR] Target file not found. + +4. Download again ``file1.txt`` + + .. code:: console + + $ ./client_example.py download file1.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [INFO] File is already available in ./downloads/file1.txt. diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py new file mode 100755 index 0000000000..2a59b8539d --- /dev/null +++ b/examples/client_example/client_example.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +import argparse +import os +import shutil +import sys +from logging import exception +from pathlib import Path + +from requests.exceptions import ConnectionError + +from tuf.ngclient import Updater + +# define directory constants +HOME_DIR = Path.home() # user home dir +DOWNLOAD_DIR = "./downloads" # download dir +METADATA_DIR = f"{HOME_DIR}/.local/share/tuf_metadata_example" # metadata dir +CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__)) # example dir + + +def init(): + """ + Initialize the TUF Client infrastructure + + This function initializes the creation of the download and TUF metadata + directory. + """ + + if not os.path.isdir(DOWNLOAD_DIR): + os.mkdir(DOWNLOAD_DIR) + + print(f"[INFO] Download directory [{DOWNLOAD_DIR}] is created.") + + if not os.path.isdir(METADATA_DIR): + os.makedirs(METADATA_DIR) + + print(f"[INFO] Metadata folder [{METADATA_DIR}] is created.") + + 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"[INFO] Bootstrap initial root metadata.") + + +def tuf_updater(): + """ + This function implement the ``tuf.ngclient.Updater`` and returns + the updater. + """ + url = "http://127.0.0.1:8000" + + try: + updater = Updater( + repository_dir=METADATA_DIR, + metadata_base_url=f"{url}/metadata/", + target_base_url=f"{url}/targets/", + target_dir=DOWNLOAD_DIR, + ) + + except FileNotFoundError: + print("[ERROR] The Example Client not initiated. Try using '--init'.") + sys.exit(1) + + return updater + + +def download(target): + """ + Download the target file using the TUF ``nglcient`` Updater process. + + 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. + """ + + try: + updater = tuf_updater() + + except ConnectionError: + print("[ERROR] Failed to connect http://127.0.0.1:8000") + sys.exit(1) + + updater.refresh() + print("[INFO] Top-level metadata is refreshed.") + + info = updater.get_targetinfo(target) + print("[INFO] Target info gotten.") + + if info is None: + print("[ERROR] Target file not found.") + sys.exit(1) + + path = updater.find_cached_target(info) + if path: + print( + f"[INFO] File is already available in {DOWNLOAD_DIR}/{info.path}." + ) + sys.exit(0) + + path = updater.download_target(info) + + print(f"[INFO] File downloaded available in {DOWNLOAD_DIR}/{info.path}.") + + +if __name__ == "__main__": + + client_args = argparse.ArgumentParser( + description="TUF Python Client Example" + ) + + # Global arguments + client_args.add_argument( + "--init", + default=False, + help="Force register a new Engine.", + action="store_true", + ) + + # Sub commands + sub_commands = client_args.add_subparsers(dest="sub_commands") + + # Download + download_parser = sub_commands.add_parser( + "download", + help="Download a target file", + ) + + download_parser.add_argument( + "target", + metavar="TARGET", + help="Target file", + ) + + command_args = vars(client_args.parse_args()) + sub_commands_args = command_args.get("sub_commands") + + if command_args.get("init") is True: + init() + + elif not sub_commands_args: + client_args.print_help() + + if sub_commands_args == "download": + target = command_args.get("target") + download(target) From 9b4fcdb5649843eb1ba7f701356b66281c5d1cf4 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Tue, 16 Nov 2021 08:42:21 +0100 Subject: [PATCH 2/8] Added lint, rst to md, output - Added the lint to examples - README format moved from Restructuredtext to Markdown - Removed the [INFO] and [ERROR] from output, to avoid confundint with logging structure Signed-off-by: Kairo de Araujo --- examples/client_example/README.md | 88 ++++++++++++++++++++++ examples/client_example/README.rst | 92 ----------------------- examples/client_example/client_example.py | 51 ++++++------- 3 files changed, 111 insertions(+), 120 deletions(-) create mode 100644 examples/client_example/README.md delete mode 100644 examples/client_example/README.rst diff --git a/examples/client_example/README.md b/examples/client_example/README.md new file mode 100644 index 0000000000..216ec8466c --- /dev/null +++ b/examples/client_example/README.md @@ -0,0 +1,88 @@ +# Python Client Example + +Introduction +============ + +Python Client Example, using ``python-tuf``. + +For information about installing ``python-tuf``, please refer to the +[Installation documentation](https://theupdateframework.readthedocs.io/en/latest/INSTALLATION.html). + + +Preparing +========= + +To have the example working in your machine, clone the ``python-tuf`` in your +system. + +```console +$ git clone git@github.com:theupdateframework/python-tuf.git +``` + + +Repository +========== + +This example demonstrates how to use the ``python-tuf`` to build a client +application. + +The repository will use static files. +The static files are available in the ``python-tuf`` source code repository in +``tests/repository_data/repository``. + +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/) ... +``` + +Client Example +============== + +The [Client Example source code](./client_example.py>) is available entirely +in this source code repository. + +How to use the Client Example: + +1. Initialize the Client + + ```console + $ ./client_example.py --init + ``` + + This action is to create the client infrastructure properly. + + This infrastructure consists in: + - Metadata repository + - Download folder for targets + - Bootstrap 1.root.json + + +2. Download the ``file1.txt`` + + ```console + $ ./client_example.py download file1.txt + Top-level metadata is refreshed. + Target info gotten. + File downloaded available in ./downloads/file2.txt. + ``` + +3. Download a not available ``file_na.txt`` + + ```console + $ ./client_example.py download file_na.txt + Top-level metadata is refreshed. + Target info gotten. + Target file not found. + ``` + +4. Download again ``file1.txt`` + + ```console + $ ./client_example.py download file1.txt + Top-level metadata is refreshed. + Target info gotten. + File is already available in ./downloads/file1.txt. + ``` diff --git a/examples/client_example/README.rst b/examples/client_example/README.rst deleted file mode 100644 index 5fc1c815d4..0000000000 --- a/examples/client_example/README.rst +++ /dev/null @@ -1,92 +0,0 @@ -Python Client Example -##################### - -Introduction -============ - -Python Client Example, using ``python-tuf``. - -For information about installing ``python-tuf``, please refer to the -`Installation documentation `_. - - -Preparing -========= - -To have the example working in your machine, clone the ``python-tuf`` in your -system. - -.. code:: console - - $ git clone git@github.com:theupdateframework/python-tuf.git - - -Repository -========== - -As this example demonstrates how to use the ``python-tuf`` to build a -client application, the repository will use static files. - -The static files are available in the ``python-tuf`` repository, same as this. -The static repository files are in -``tests/repository_data/repository``. - -Run the repository using the Python3 built-in HTTP module, and keep this -session running. - -.. code:: console - - $ python3 -m http.server -d tests/repository_data/repository - Serving HTTP on :: port 8000 (http://[::]:8000/) ... - - -Client Example -============== - -The `source code is available entirely <./client_example.py>`_ in this -repository. - -How to use the Client Example: - -1. Initialize the Client - - .. code:: console - - $ ./client_example.py --init - - - This action is to create the client infrastructure properly. - - This infrastructure consists in: - - Metadata repository - - Download folder for targets - - Bootstrap 1.root.json - - -2. Download the ``file1.txt`` - - .. code:: console - - $ ./client_example.py download file1.txt - [INFO] Top-level metadata is refreshed. - [INFO] Target info gotten. - [INFO] File downloaded available in ./downloads/file2.txt. - - -3. Download a not available ``file_na.txt`` - - .. code:: console - - $ ./client_example.py download file_na.txt - [INFO] Top-level metadata is refreshed. - [INFO] Target info gotten. - [ERROR] Target file not found. - -4. Download again ``file1.txt`` - - .. code:: console - - $ ./client_example.py download file1.txt - [INFO] Top-level metadata is refreshed. - [INFO] Target info gotten. - [INFO] File is already available in ./downloads/file1.txt. diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index 2a59b8539d..d44e48df89 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -1,13 +1,15 @@ #!/usr/bin/env python +"""Python Client Example.""" + +# Copyright 2012 - 2017, New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + import argparse import os import shutil import sys -from logging import exception from pathlib import Path -from requests.exceptions import ConnectionError - from tuf.ngclient import Updater # define directory constants @@ -28,18 +30,18 @@ def init(): if not os.path.isdir(DOWNLOAD_DIR): os.mkdir(DOWNLOAD_DIR) - print(f"[INFO] Download directory [{DOWNLOAD_DIR}] is created.") + print(f"Download directory [{DOWNLOAD_DIR}] is created.") if not os.path.isdir(METADATA_DIR): os.makedirs(METADATA_DIR) - print(f"[INFO] Metadata folder [{METADATA_DIR}] is created.") + print(f"Metadata folder [{METADATA_DIR}] is created.") 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"[INFO] Bootstrap initial root metadata.") + print("Bootstrap initial root metadata.") def tuf_updater(): @@ -49,17 +51,12 @@ def tuf_updater(): """ url = "http://127.0.0.1:8000" - try: - updater = Updater( - repository_dir=METADATA_DIR, - metadata_base_url=f"{url}/metadata/", - target_base_url=f"{url}/targets/", - target_dir=DOWNLOAD_DIR, - ) - - except FileNotFoundError: - print("[ERROR] The Example Client not initiated. Try using '--init'.") - sys.exit(1) + updater = Updater( + repository_dir=METADATA_DIR, + metadata_base_url=f"{url}/metadata/", + target_base_url=f"{url}/targets/", + target_dir=DOWNLOAD_DIR, + ) return updater @@ -77,29 +74,27 @@ def download(target): updater = tuf_updater() except ConnectionError: - print("[ERROR] Failed to connect http://127.0.0.1:8000") + print("Failed to connect http://127.0.0.1:8000") sys.exit(1) updater.refresh() - print("[INFO] Top-level metadata is refreshed.") + print("Top-level metadata is refreshed.") info = updater.get_targetinfo(target) - print("[INFO] Target info gotten.") + print("Target info gotten.") if info is None: - print("[ERROR] Target file not found.") + print("Target file not found.") sys.exit(1) path = updater.find_cached_target(info) if path: - print( - f"[INFO] File is already available in {DOWNLOAD_DIR}/{info.path}." - ) + print(f"File is already available in {DOWNLOAD_DIR}/{info.path}.") sys.exit(0) path = updater.download_target(info) - print(f"[INFO] File downloaded available in {DOWNLOAD_DIR}/{info.path}.") + print(f"File downloaded available in {DOWNLOAD_DIR}/{info.path}.") if __name__ == "__main__": @@ -112,7 +107,7 @@ def download(target): client_args.add_argument( "--init", default=False, - help="Force register a new Engine.", + help="Initializes the Client structure.", action="store_true", ) @@ -141,5 +136,5 @@ def download(target): client_args.print_help() if sub_commands_args == "download": - target = command_args.get("target") - download(target) + target_download = command_args.get("target") + download(target_download) From 46629c4547bb925e4c09625bd4b9770f1374a62d Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Mon, 22 Nov 2021 11:27:28 +0100 Subject: [PATCH 3/8] Implement a combination of print and logging Using ``print`` for high-level client output Option to see ``tuf.ngclient`` logging output Update the README. Signed-off-by: Kairo de Araujo --- examples/client_example/README.md | 36 +++++--------- examples/client_example/client_example.py | 60 ++++++++++++++++------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/examples/client_example/README.md b/examples/client_example/README.md index 216ec8466c..b835f23d38 100644 --- a/examples/client_example/README.md +++ b/examples/client_example/README.md @@ -5,19 +5,9 @@ Introduction Python Client Example, using ``python-tuf``. -For information about installing ``python-tuf``, please refer to the -[Installation documentation](https://theupdateframework.readthedocs.io/en/latest/INSTALLATION.html). - - -Preparing -========= - -To have the example working in your machine, clone the ``python-tuf`` in your -system. - -```console -$ git clone git@github.com:theupdateframework/python-tuf.git -``` +This Python Client Example implements the following actions: + - Client Infrastructure Initialization + - Download target files from TUF Repository Repository @@ -46,7 +36,7 @@ in this source code repository. How to use the Client Example: -1. Initialize the Client +1. Initialize the Client (optional) ```console $ ./client_example.py --init @@ -63,26 +53,24 @@ How to use the Client Example: 2. Download the ``file1.txt`` ```console - $ ./client_example.py download file1.txt - Top-level metadata is refreshed. - Target info gotten. - File downloaded available in ./downloads/file2.txt. + Target file1.txt information fetched + Cached target file1.txt verified + Target is available in ./downloads/file1.txt ``` 3. Download a not available ``file_na.txt`` ```console $ ./client_example.py download file_na.txt - Top-level metadata is refreshed. - Target info gotten. - Target file not found. + Target file_na.txt information fetched + Target file_na.txt not found ``` 4. Download again ``file1.txt`` ```console $ ./client_example.py download file1.txt - Top-level metadata is refreshed. - Target info gotten. - File is already available in ./downloads/file1.txt. + Target file1.txt information fetched + Cached target file1.txt verified + Target is already available in ./downloads/file1.txt ``` diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index d44e48df89..dec8f605ac 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -5,6 +5,7 @@ # SPDX-License-Identifier: MIT OR Apache-2.0 import argparse +import logging import os import shutil import sys @@ -21,33 +22,31 @@ def init(): """ - Initialize the TUF Client infrastructure + The function that initializes the TUF client infrastructure. - This function initializes the creation of the download and TUF metadata - directory. + It creates the metadata directory and downloads the ``root.json``. """ if not os.path.isdir(DOWNLOAD_DIR): os.mkdir(DOWNLOAD_DIR) - print(f"Download directory [{DOWNLOAD_DIR}] is created.") + print(f"Download directory [{DOWNLOAD_DIR}] is created") if not os.path.isdir(METADATA_DIR): os.makedirs(METADATA_DIR) - print(f"Metadata folder [{METADATA_DIR}] is created.") + print(f"Metadata folder [{METADATA_DIR}] is created") 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("Bootstrap initial root metadata.") + print("Bootstrap initial root metadata") def tuf_updater(): """ - This function implement the ``tuf.ngclient.Updater`` and returns - the updater. + The function that creates the TUF updater using ``tuf.ngclient``. """ url = "http://127.0.0.1:8000" @@ -63,7 +62,8 @@ def tuf_updater(): def download(target): """ - Download the target file using the TUF ``nglcient`` Updater process. + The function that downloads the target file using the TUF ``nglcient`` + 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, @@ -73,28 +73,31 @@ def download(target): try: updater = tuf_updater() - except ConnectionError: - print("Failed to connect http://127.0.0.1:8000") - sys.exit(1) + # if the metadata is not available, it will initialize the TUF client + except FileNotFoundError: + print("Client infrastructure not found") + print("Initializing the Client Infrastructure") + init() + updater = tuf_updater() updater.refresh() - print("Top-level metadata is refreshed.") info = updater.get_targetinfo(target) - print("Target info gotten.") + print(f"Target {target} information fetched") if info is None: - print("Target file not found.") + print(f"Target {target} not found") sys.exit(1) path = updater.find_cached_target(info) + print(f"Cached target {target} verified") if path: - print(f"File is already available in {DOWNLOAD_DIR}/{info.path}.") + print(f"Target is already available in {DOWNLOAD_DIR}/{info.path}") sys.exit(0) path = updater.download_target(info) - print(f"File downloaded available in {DOWNLOAD_DIR}/{info.path}.") + print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") if __name__ == "__main__": @@ -107,10 +110,18 @@ def download(target): client_args.add_argument( "--init", default=False, - help="Initializes the Client structure.", + help="Initializes the Client structure", action="store_true", ) + client_args.add_argument( + "-v", + "--verbose", + help="Output verbosity level (-v, -vv, ...)", + action="count", + default=0, + ) + # Sub commands sub_commands = client_args.add_subparsers(dest="sub_commands") @@ -129,6 +140,19 @@ def download(target): command_args = vars(client_args.parse_args()) sub_commands_args = command_args.get("sub_commands") + verbose = command_args.get("verbose") + + if verbose <= 1: + loglevel = logging.ERROR + elif verbose == 2: + loglevel = logging.WARNING + elif verbose == 3: + loglevel = logging.INFO + else: + loglevel = logging.DEBUG + + logging.basicConfig(level=loglevel) + if command_args.get("init") is True: init() From 4c5581a8000e3aaef4d63b01e0c34dcbe7015f8b Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Mon, 22 Nov 2021 11:54:30 +0100 Subject: [PATCH 4/8] Add a simple example handling TUF exceptions. To highlight TUF ngclient exceptions. Signed-off-by: Kairo de Araujo --- examples/client_example/client_example.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index dec8f605ac..8f7005525f 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -11,6 +11,7 @@ import sys from pathlib import Path +from tuf.exceptions import RepositoryError from tuf.ngclient import Updater # define directory constants @@ -80,6 +81,12 @@ def download(target): init() updater = tuf_updater() + # handle specific TUF error (root.json corrupted in the client metadata) + # check out ``tuf.exceptions`` for more information + except RepositoryError as e: + print(e) + sys.exit(1) + updater.refresh() info = updater.get_targetinfo(target) From 4c37f1bc35677b96b7463c5c27d7308f124533b4 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Fri, 26 Nov 2021 10:24:16 +0100 Subject: [PATCH 5/8] Removes the Python Client Example as WIP This commit adds many comments from the WIP stage Signed-off-by: Kairo de Araujo --- examples/client_example/README.md | 39 ++------ examples/client_example/client_example.py | 109 +++++++++------------- 2 files changed, 53 insertions(+), 95 deletions(-) diff --git a/examples/client_example/README.md b/examples/client_example/README.md index b835f23d38..348d20b874 100644 --- a/examples/client_example/README.md +++ b/examples/client_example/README.md @@ -36,41 +36,22 @@ in this source code repository. How to use the Client Example: -1. Initialize the Client (optional) - ```console - $ ./client_example.py --init - ``` - - This action is to create the client infrastructure properly. - - This infrastructure consists in: - - Metadata repository - - Download folder for targets - - Bootstrap 1.root.json - - -2. Download the ``file1.txt`` +1. Download the ``file1.txt`` ```console - Target file1.txt information fetched - Cached target file1.txt verified - Target is available in ./downloads/file1.txt - ``` - -3. Download a not available ``file_na.txt`` - - ```console - $ ./client_example.py download file_na.txt - Target file_na.txt information fetched - Target file_na.txt not found + $ ./client_example.py download file1.txt + Download directory [./downloads] was created + Metadata folder [] was created + Added trusted root in /Users/kdearaujo/.local/share/python-tuf-client-example + Found trusted root in + Target downloaded and available in ./downloads/file1.txt ``` -4. Download again ``file1.txt`` +2. Download again ``file1.txt`` ```console $ ./client_example.py download file1.txt - Target file1.txt information fetched - Cached target file1.txt verified - Target is already available in ./downloads/file1.txt + Found trusted root in + Target is available in ./downloads/file1.txt ``` diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index 8f7005525f..2327d7ec50 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -8,57 +8,41 @@ import logging import os import shutil -import sys from pathlib import Path from tuf.exceptions import RepositoryError from tuf.ngclient import Updater -# define directory constants -HOME_DIR = Path.home() # user home dir -DOWNLOAD_DIR = "./downloads" # download dir -METADATA_DIR = f"{HOME_DIR}/.local/share/tuf_metadata_example" # metadata dir -CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__)) # example dir +# 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(): """ The function that initializes the TUF client infrastructure. - It creates the metadata directory and downloads the ``root.json``. + It creates the metadata directory and adds a trusted ``root.json``. """ if not os.path.isdir(DOWNLOAD_DIR): os.mkdir(DOWNLOAD_DIR) - - print(f"Download directory [{DOWNLOAD_DIR}] is created") + print(f"Download directory [{DOWNLOAD_DIR}] was created") if not os.path.isdir(METADATA_DIR): os.makedirs(METADATA_DIR) - - print(f"Metadata folder [{METADATA_DIR}] is created") + print(f"Metadata folder [{METADATA_DIR}] was created") 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("Bootstrap initial root metadata") - - -def tuf_updater(): - """ - The function that creates the TUF updater using ``tuf.ngclient``. - """ - url = "http://127.0.0.1:8000" - - updater = Updater( - repository_dir=METADATA_DIR, - metadata_base_url=f"{url}/metadata/", - target_base_url=f"{url}/targets/", - target_dir=DOWNLOAD_DIR, - ) + print(f"Added trusted root in {METADATA_DIR}") - return updater + else: + print(f"Found trusted root in {METADATA_DIR}") def download(target): @@ -69,58 +53,58 @@ def download(target): 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 = tuf_updater() + updater = Updater( + repository_dir=METADATA_DIR, + metadata_base_url=f"{BASE_URL}/metadata/", + target_base_url=f"{BASE_URL}/targets/", + target_dir=DOWNLOAD_DIR, + ) - # if the metadata is not available, it will initialize the TUF client - except FileNotFoundError: - print("Client infrastructure not found") - print("Initializing the Client Infrastructure") - init() - updater = tuf_updater() + # if the metadata is not available + except FileNotFoundError as e: + print(str(e)) + return False # handle specific TUF error (root.json corrupted in the client metadata) # check out ``tuf.exceptions`` for more information except RepositoryError as e: - print(e) - sys.exit(1) + print(str(e)) + return False updater.refresh() info = updater.get_targetinfo(target) - print(f"Target {target} information fetched") if info is None: print(f"Target {target} not found") - sys.exit(1) + return True path = updater.find_cached_target(info) - print(f"Cached target {target} verified") if path: - print(f"Target is already available in {DOWNLOAD_DIR}/{info.path}") - sys.exit(0) + print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") + return True path = updater.download_target(info) - print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") + print(f"Target downloaded and available in {DOWNLOAD_DIR}/{info.path}") + return True if __name__ == "__main__": + # initialize the Python Client Example infrastructure + init() + client_args = argparse.ArgumentParser( description="TUF Python Client Example" ) # Global arguments - client_args.add_argument( - "--init", - default=False, - help="Initializes the Client structure", - action="store_true", - ) - client_args.add_argument( "-v", "--verbose", @@ -130,10 +114,10 @@ def download(target): ) # Sub commands - sub_commands = client_args.add_subparsers(dest="sub_commands") + sub_command = client_args.add_subparsers(dest="sub_command") # Download - download_parser = sub_commands.add_parser( + download_parser = sub_command.add_parser( "download", help="Download a target file", ) @@ -144,28 +128,21 @@ def download(target): help="Target file", ) - command_args = vars(client_args.parse_args()) - sub_commands_args = command_args.get("sub_commands") - - verbose = command_args.get("verbose") + command_args = client_args.parse_args() - if verbose <= 1: + if command_args.verbose <= 1: loglevel = logging.ERROR - elif verbose == 2: + elif command_args.verbose == 2: loglevel = logging.WARNING - elif verbose == 3: + elif command_args.verbose == 3: loglevel = logging.INFO else: loglevel = logging.DEBUG logging.basicConfig(level=loglevel) - if command_args.get("init") is True: - init() + if command_args.sub_command == "download": + download(command_args.target) - elif not sub_commands_args: + else: client_args.print_help() - - if sub_commands_args == "download": - target_download = command_args.get("target") - download(target_download) From ca13d059b674e1954f440bafe3de1ba6592dabef Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Mon, 29 Nov 2021 08:51:49 +0100 Subject: [PATCH 6/8] Single error handling for updater To simple exemplify, the updater calls are added to a single block that handles the exceptions Signed-off-by: Kairo de Araujo --- examples/client_example/client_example.py | 38 +++++++++-------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index 2327d7ec50..228cbb50ac 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -64,37 +64,29 @@ def download(target): target_base_url=f"{BASE_URL}/targets/", target_dir=DOWNLOAD_DIR, ) + updater.refresh() - # if the metadata is not available - except FileNotFoundError as e: - print(str(e)) - return False - - # handle specific TUF error (root.json corrupted in the client metadata) - # check out ``tuf.exceptions`` for more information - except RepositoryError as e: - print(str(e)) - return False + info = updater.get_targetinfo(target) - updater.refresh() + if info is None: + print(f"Target {target} not found") + return True - info = updater.get_targetinfo(target) + path = updater.find_cached_target(info) + if path: + print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") + return True - if info is None: - print(f"Target {target} not found") - return True + path = updater.download_target(info) + print(f"Target downloaded and available in {DOWNLOAD_DIR}/{info.path}") - path = updater.find_cached_target(info) - if path: - print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") - return True - - path = updater.download_target(info) - - print(f"Target downloaded and available in {DOWNLOAD_DIR}/{info.path}") + except (FileNotFoundError, RepositoryError) as e: + print(str(e)) + return False return True + if __name__ == "__main__": # initialize the Python Client Example infrastructure From 183223e59c3b687984fd918e7ca6d52b72318bd5 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Mon, 29 Nov 2021 14:21:17 +0100 Subject: [PATCH 7/8] Simplify README, fix some flows and output - Simplify README and better keywords - Fix the verbosity - Better docstrings - Client flow for init and main are clear Signed-off-by: Kairo de Araujo --- examples/client_example/README.md | 51 +++++------------------ examples/client_example/client_example.py | 42 +++++++++---------- 2 files changed, 29 insertions(+), 64 deletions(-) diff --git a/examples/client_example/README.md b/examples/client_example/README.md index 348d20b874..399c6d6b42 100644 --- a/examples/client_example/README.md +++ b/examples/client_example/README.md @@ -1,24 +1,15 @@ -# Python Client Example +# TUF Client Example -Introduction -============ -Python Client Example, using ``python-tuf``. +TUF Client Example, using ``python-tuf``. -This Python Client Example implements the following actions: +This TUF Client Example implements the following actions: - Client Infrastructure Initialization - Download target files from TUF Repository - -Repository -========== - -This example demonstrates how to use the ``python-tuf`` to build a client -application. - -The repository will use static files. -The static files are available in the ``python-tuf`` source code repository in -``tests/repository_data/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. @@ -28,30 +19,8 @@ session running. Serving HTTP on :: port 8000 (http://[::]:8000/) ... ``` -Client Example -============== - -The [Client Example source code](./client_example.py>) is available entirely -in this source code repository. - -How to use the Client Example: - +How to use the TUF Client Example to download a target file. -1. Download the ``file1.txt`` - - ```console - $ ./client_example.py download file1.txt - Download directory [./downloads] was created - Metadata folder [] was created - Added trusted root in /Users/kdearaujo/.local/share/python-tuf-client-example - Found trusted root in - Target downloaded and available in ./downloads/file1.txt - ``` - -2. Download again ``file1.txt`` - - ```console - $ ./client_example.py download file1.txt - Found trusted root in - Target is available in ./downloads/file1.txt - ``` +```console +$ ./client_example.py download file1.txt +``` diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index 228cbb50ac..227db96a18 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Python Client Example.""" +"""TUF Client Example""" # Copyright 2012 - 2017, New York University and the TUF contributors # SPDX-License-Identifier: MIT OR Apache-2.0 @@ -20,20 +20,14 @@ CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__)) -def init(): - """ - The function that initializes the TUF client infrastructure. - - It creates the metadata directory and adds a trusted ``root.json``. - """ +def init() -> None: + """Initialize local trusted metadata and create a directory for downloads""" if not os.path.isdir(DOWNLOAD_DIR): os.mkdir(DOWNLOAD_DIR) - print(f"Download directory [{DOWNLOAD_DIR}] was created") if not os.path.isdir(METADATA_DIR): os.makedirs(METADATA_DIR) - print(f"Metadata folder [{METADATA_DIR}] was created") if not os.path.isfile(f"{METADATA_DIR}/root.json"): shutil.copy( @@ -45,10 +39,9 @@ def init(): print(f"Found trusted root in {METADATA_DIR}") -def download(target): +def download(target: str) -> bool: """ - The function that downloads the target file using the TUF ``nglcient`` - Updater. + 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, @@ -74,27 +67,23 @@ def download(target): path = updater.find_cached_target(info) if path: - print(f"Target is available in {DOWNLOAD_DIR}/{info.path}") + print(f"Target is available in {path}") return True path = updater.download_target(info) - print(f"Target downloaded and available in {DOWNLOAD_DIR}/{info.path}") + print(f"Target downloaded and available in {path}") - except (FileNotFoundError, RepositoryError) as e: + except (OSError, RepositoryError) as e: print(str(e)) return False return True -if __name__ == "__main__": - - # initialize the Python Client Example infrastructure - init() +def main() -> None: + """Main TUF Client Example function""" - client_args = argparse.ArgumentParser( - description="TUF Python Client Example" - ) + client_args = argparse.ArgumentParser(description="TUF Client Example") # Global arguments client_args.add_argument( @@ -122,7 +111,7 @@ def download(target): command_args = client_args.parse_args() - if command_args.verbose <= 1: + if command_args.verbose == 1: loglevel = logging.ERROR elif command_args.verbose == 2: loglevel = logging.WARNING @@ -133,8 +122,15 @@ def download(target): 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() From 134441019c8cdf71b7221da207dcaa0ac811c8cc Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Wed, 1 Dec 2021 15:02:19 +0100 Subject: [PATCH 8/8] This commit fixes the verbosity (-v) levels. without -v is ERROR logging, -v WARNINGS logging, -vv INFO logging and -vvv (+) DEBUG logging. Signed-off-by: Kairo de Araujo --- examples/client_example/client_example.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py index 227db96a18..4d6fc1c6fb 100755 --- a/examples/client_example/client_example.py +++ b/examples/client_example/client_example.py @@ -111,11 +111,11 @@ def main() -> None: command_args = client_args.parse_args() - if command_args.verbose == 1: + if command_args.verbose == 0: loglevel = logging.ERROR - elif command_args.verbose == 2: + elif command_args.verbose == 1: loglevel = logging.WARNING - elif command_args.verbose == 3: + elif command_args.verbose == 2: loglevel = logging.INFO else: loglevel = logging.DEBUG