Skip to content

Commit 216ae64

Browse files
author
Lukas Pühringer
authored
Merge pull request #2193 from jku/repository-lib
Repository module and example
2 parents c6f8b58 + fd02226 commit 216ae64

13 files changed

Lines changed: 548 additions & 129 deletions

File tree

examples/README.md

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

3+
* [repository](repository)
34
* [client](client_example)
4-
* [repository](repo_example)
5-
5+
* [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+
### Usage 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` every time you restart the repository application.
36+
37+
38+
### Usage 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
```
Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,49 @@
77
import argparse
88
import logging
99
import os
10-
import shutil
10+
import sys
1111
import traceback
12+
from hashlib import sha256
1213
from pathlib import Path
14+
from urllib import request
1315

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

1719
# constants
18-
BASE_URL = "http://127.0.0.1:8000"
1920
DOWNLOAD_DIR = "./downloads"
20-
METADATA_DIR = f"{Path.home()}/.local/share/python-tuf-client-example"
2121
CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__))
2222

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

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

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

30-
if not os.path.isdir(METADATA_DIR):
31-
os.makedirs(METADATA_DIR)
38+
if not os.path.isdir(metadata_dir):
39+
os.makedirs(metadata_dir)
3240

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

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

4251

43-
def download(target: str) -> bool:
52+
def download(base_url: str, target: str) -> bool:
4453
"""
4554
Download the target file using ``ngclient`` Updater.
4655
@@ -51,11 +60,23 @@ def download(target: str) -> bool:
5160
Returns:
5261
A boolean indicating if process was successful
5362
"""
63+
metadata_dir = build_metadata_dir(base_url)
64+
65+
if not os.path.isfile(f"{metadata_dir}/root.json"):
66+
print(
67+
"Trusted local root not found. Use 'tofu' command to "
68+
"Trust-On-First-Use or copy trusted root metadata to "
69+
f"{metadata_dir}/root.json"
70+
)
71+
return False
72+
73+
print(f"Using trusted root in {metadata_dir}")
74+
5475
try:
5576
updater = Updater(
56-
metadata_dir=METADATA_DIR,
57-
metadata_base_url=f"{BASE_URL}/metadata/",
58-
target_base_url=f"{BASE_URL}/targets/",
77+
metadata_dir=metadata_dir,
78+
metadata_base_url=f"{base_url}/metadata/",
79+
target_base_url=f"{base_url}/targets/",
5980
target_dir=DOWNLOAD_DIR,
6081
)
6182
updater.refresh()
@@ -97,9 +118,22 @@ def main() -> None:
97118
default=0,
98119
)
99120

121+
client_args.add_argument(
122+
"-u",
123+
"--url",
124+
help="Base repository URL",
125+
default="http://127.0.0.1:8001",
126+
)
127+
100128
# Sub commands
101129
sub_command = client_args.add_subparsers(dest="sub_command")
102130

131+
# Trust-On-First-Use
132+
sub_command.add_parser(
133+
"tofu",
134+
help="Initialize client with Trust-On-First-Use",
135+
)
136+
103137
# Download
104138
download_parser = sub_command.add_parser(
105139
"download",
@@ -126,14 +160,15 @@ def main() -> None:
126160
logging.basicConfig(level=loglevel)
127161

128162
# initialize the TUF Client Example infrastructure
129-
init()
130-
131-
if command_args.sub_command == "download":
132-
download(command_args.target)
133-
163+
if command_args.sub_command == "tofu":
164+
if not init_tofu(command_args.url):
165+
return "Failed to initialize local repository"
166+
elif command_args.sub_command == "download":
167+
if not download(command_args.url, command_args.target):
168+
return f"Failed to download {command_args.target}"
134169
else:
135170
client_args.print_help()
136171

137172

138173
if __name__ == "__main__":
139-
main()
174+
sys.exit(main())
File renamed without changes.

examples/repo_example/succinct_hash_bin_delegations.py renamed to examples/manual_repo/succinct_hash_bin_delegations.py

File renamed without changes.

examples/repository/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TUF Repository Application Example
2+
3+
:warning: This example uses the repository module which is not considered
4+
part of the python-tuf stable API quite yet.
5+
6+
This TUF Repository Application Example has the following features:
7+
- Initializes a completely new repository on startup
8+
- Stores everything (metadata, targets, signing keys) in-memory
9+
- Serves metadata and targets on localhost (default port 8001)
10+
- Simulates a live repository by automatically adding a new target
11+
file every 10 seconds.
12+
13+
14+
### Usage
15+
16+
```console
17+
./repo
18+
```
19+
Your repository is now running and is accessible on localhost, See e.g.
20+
http://127.0.0.1:8001/metadata/1.root.json. The
21+
[client example](../client_example/README.md) uses this address by default.

0 commit comments

Comments
 (0)