Skip to content

Commit 14c7124

Browse files
committed
examples: Update client example
* Support any repository (that serves /targets/ and /metadata/) with --url * Support multiple repositories by aking the local cache repository-specific * Add "tofu" command to initialize with Trust-On-First-Use * Update README so it uses the new repository application example Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent 2073d8c commit 14c7124

File tree

4 files changed

+92
-123
lines changed

4 files changed

+92
-123
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Usage examples
22

3+
* [repository](repository)
34
* [client](client_example)
45
* [repository built with low-level Metadata API](manual_repo)

examples/client_example/1.root.json

Lines changed: 0 additions & 87 deletions
This file was deleted.

examples/client_example/README.md

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,43 @@
44
TUF Client Example, using ``python-tuf``.
55

66
This TUF Client Example implements the following actions:
7-
- Client Infrastructure Initialization
8-
- Download target files from TUF Repository
7+
- Client Initialization
8+
- Target file download
99

10-
The example client expects to find a TUF repository running on localhost. We
11-
can use the static metadata files in ``tests/repository_data/repository``
12-
to set one up.
10+
The client can be used against any TUF repository that serves metadata and
11+
targets under the same URL (in _/metadata/_ and _/targets/_ directories, respectively). The
12+
used TUF repository can be set with `--url` (default repository is "http://127.0.0.1:8001"
13+
which is also the default for the repository example).
1314

14-
Run the repository using the Python3 built-in HTTP module, and keep this
15-
session running.
1615

16+
### Example with the repository example
17+
18+
In one terminal, run the repository example and leave it running:
1719
```console
18-
$ python3 -m http.server -d tests/repository_data/repository
19-
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
20+
examples/repository/repo
2021
```
2122

22-
How to use the TUF Client Example to download a target file.
23+
In another terminal, run the client:
2324

2425
```console
25-
$ ./client_example.py download file1.txt
26+
# initialize the client with Trust-On-First-Use
27+
./client tofu
28+
29+
# Then download example files from the repository:
30+
./client download file1.txt
31+
```
32+
33+
Note that unlike normal repositories, the example repository only exists in
34+
memory and is re-generated from scratch at every startup: This means your
35+
client needs to run `tofu` everytime you restart the repository application.
36+
37+
38+
### Example with a repository on the internet
39+
40+
```console
41+
# On first use only, initialize the client with Trust-On-First-Use
42+
./client --url https://jku.github.io/tuf-demo tofu
43+
44+
# Then download example files from the repository:
45+
./client --url https://jku.github.io/tuf-demo download demo/succinctly-delegated-1.txt
2646
```

examples/client_example/client_example.py renamed to examples/client_example/client

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,48 @@
77
import argparse
88
import logging
99
import os
10-
import shutil
10+
import sys
11+
from hashlib import sha256
1112
from pathlib import Path
13+
from urllib import request
1214

1315
from tuf.api.exceptions import DownloadError, RepositoryError
1416
from tuf.ngclient import Updater
1517

1618
# constants
17-
BASE_URL = "http://127.0.0.1:8000"
1819
DOWNLOAD_DIR = "./downloads"
19-
METADATA_DIR = f"{Path.home()}/.local/share/python-tuf-client-example"
2020
CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__))
2121

22+
def build_metadata_dir(base_url: str) -> str:
23+
"""build a unique and reproducible directory name for the repository url"""
24+
name = sha256(base_url.encode()).hexdigest()[:8]
25+
# TODO: Make this not windows hostile?
26+
return f"{Path.home()}/.local/share/tuf-example/{name}"
2227

23-
def init() -> None:
24-
"""Initialize local trusted metadata and create a directory for downloads"""
28+
29+
def init_tofu(base_url: str) -> bool:
30+
"""Initialize local trusted metadata (Trust-On-First-Use) and create a
31+
directory for downloads"""
32+
metadata_dir = build_metadata_dir(base_url)
2533

2634
if not os.path.isdir(DOWNLOAD_DIR):
2735
os.mkdir(DOWNLOAD_DIR)
2836

29-
if not os.path.isdir(METADATA_DIR):
30-
os.makedirs(METADATA_DIR)
37+
if not os.path.isdir(metadata_dir):
38+
os.makedirs(metadata_dir)
3139

32-
if not os.path.isfile(f"{METADATA_DIR}/root.json"):
33-
shutil.copy(
34-
f"{CLIENT_EXAMPLE_DIR}/1.root.json", f"{METADATA_DIR}/root.json"
35-
)
36-
print(f"Added trusted root in {METADATA_DIR}")
40+
root_url = f"{base_url}/metadata/1.root.json"
41+
try:
42+
request.urlretrieve(root_url, f"{metadata_dir}/root.json")
43+
except OSError:
44+
print(f"Failed to download initial root from {root_url}")
45+
return False
3746

38-
else:
39-
print(f"Found trusted root in {METADATA_DIR}")
47+
print(f"Trust-on-First-Use: Initialized new root in {metadata_dir}")
48+
return True
4049

4150

42-
def download(target: str) -> bool:
51+
def download(base_url: str, target: str) -> bool:
4352
"""
4453
Download the target file using ``ngclient`` Updater.
4554
@@ -50,11 +59,23 @@ def download(target: str) -> bool:
5059
Returns:
5160
A boolean indicating if process was successful
5261
"""
62+
metadata_dir = build_metadata_dir(base_url)
63+
64+
if not os.path.isfile(f"{metadata_dir}/root.json"):
65+
print(
66+
"Trusted local root not found. Use 'tofu' command to "
67+
"Trust-On-First-Use or copy trusted root metadata to "
68+
f"{metadata_dir}/root.json"
69+
)
70+
return False
71+
72+
print(f"Using trusted root in {metadata_dir}")
73+
5374
try:
5475
updater = Updater(
55-
metadata_dir=METADATA_DIR,
56-
metadata_base_url=f"{BASE_URL}/metadata/",
57-
target_base_url=f"{BASE_URL}/targets/",
76+
metadata_dir=metadata_dir,
77+
metadata_base_url=f"{base_url}/metadata/",
78+
target_base_url=f"{base_url}/targets/",
5879
target_dir=DOWNLOAD_DIR,
5980
)
6081
updater.refresh()
@@ -74,7 +95,7 @@ def download(target: str) -> bool:
7495
print(f"Target downloaded and available in {path}")
7596

7697
except (OSError, RepositoryError, DownloadError) as e:
77-
print(f"Failed to download target {target}: {e}")
98+
print(f"Failed: {e}")
7899
return False
79100

80101
return True
@@ -94,9 +115,22 @@ def main() -> None:
94115
default=0,
95116
)
96117

118+
client_args.add_argument(
119+
"-u",
120+
"--url",
121+
help="Base repository URL",
122+
default="http://127.0.0.1:8001",
123+
)
124+
97125
# Sub commands
98126
sub_command = client_args.add_subparsers(dest="sub_command")
99127

128+
# Trust-On-First-Use
129+
sub_command.add_parser(
130+
"tofu",
131+
help="Initialize client with Trust-On-First-Use",
132+
)
133+
100134
# Download
101135
download_parser = sub_command.add_parser(
102136
"download",
@@ -123,14 +157,15 @@ def main() -> None:
123157
logging.basicConfig(level=loglevel)
124158

125159
# initialize the TUF Client Example infrastructure
126-
init()
127-
128-
if command_args.sub_command == "download":
129-
download(command_args.target)
130-
160+
if command_args.sub_command == "tofu":
161+
if not init_tofu(command_args.url):
162+
return "Failed to initialize local repository"
163+
elif command_args.sub_command == "download":
164+
if not download(command_args.url, command_args.target):
165+
return f"Failed to download {command_args.target}"
131166
else:
132167
client_args.print_help()
133168

134169

135170
if __name__ == "__main__":
136-
main()
171+
sys.exit(main())

0 commit comments

Comments
 (0)