forked from theupdateframework/python-tuf
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: TUF Python Client Example/Tutorial
It is a simple example of TUF ngclient implementation. This example contains a README.rst that is a 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 <[email protected]>
- Loading branch information
Kairo de Araujo
committed
Nov 12, 2021
1 parent
fa7990c
commit 317e600
Showing
3 changed files
with
327 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
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>`_. | ||
|
||
During this Client example, we will be using the source code, but is recomended | ||
that you use the installed ``python-tuf`` to run the client example | ||
|
||
Preparing | ||
========= | ||
|
||
In order to have the example working in your machine, clone the ``python-tuf`` | ||
in your system. | ||
|
||
.. code:: console | ||
$ git clone [email protected]:theupdateframework/python-tuf.git | ||
Repository | ||
========== | ||
|
||
As this example aims to demonstrate 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 availabe 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 proper create the client infrastructure. | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
#!/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) |