Skip to content

Commit a773e8f

Browse files
author
Lukas Pühringer
authored
Merge pull request #2038 from MVrachev/tap15-example
Add an example script about succinct roles usage
2 parents d9153ee + e9ef5b6 commit a773e8f

File tree

4 files changed

+257
-42
lines changed

4 files changed

+257
-42
lines changed

examples/repo_example/hashed_bin_delegation.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from securesystemslib.signer import SSlibSigner
2727

2828
from tuf.api.metadata import (
29-
SPECIFICATION_VERSION,
3029
DelegatedRole,
3130
Delegations,
3231
Key,
@@ -42,8 +41,7 @@ def _in(days: float) -> datetime:
4241
return datetime.utcnow().replace(microsecond=0) + timedelta(days=days)
4342

4443

45-
SPEC_VERSION = ".".join(SPECIFICATION_VERSION)
46-
roles: Dict[str, Metadata] = {}
44+
roles: Dict[str, Metadata[Targets]] = {}
4745
keys: Dict[str, Dict[str, Any]] = {}
4846

4947
# Hash bin delegation
@@ -200,11 +198,11 @@ def find_hash_bin(path: str) -> str:
200198

201199
# Sign and persist
202200
# ----------------
203-
# Sign all metadata and persist to temporary directory at CWD for review
204-
# (most notably see 'bins.json' and '80-87.json').
201+
# Sign all metadata and write to temporary directory at CWD for review using
202+
# versioned file names. Most notably see '1.bins.json' and '1.80-87.json'.
205203

206204
# NOTE: See "Persist metadata" paragraph in 'basic_repo.py' example for more
207-
# details about serialization formats and metadata file name convention.
205+
# details about serialization formats and metadata file name conventions.
208206
PRETTY = JSONSerializer(compact=False)
209207
TMP_DIR = tempfile.mkdtemp(dir=os.getcwd())
210208

@@ -213,6 +211,6 @@ def find_hash_bin(path: str) -> str:
213211
signer = SSlibSigner(key)
214212
role.sign(signer)
215213

216-
filename = f"{role_name}.json"
214+
filename = f"1.{role_name}.json"
217215
filepath = os.path.join(TMP_DIR, filename)
218216
role.to_file(filepath, serializer=PRETTY)
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Copyright New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
"""
4+
A TUF succinct hash bin delegation example using the low-level TUF Metadata API.
5+
6+
The example code in this file demonstrates how to perform succinct hash bin
7+
delegation using the low-level Metadata API.
8+
Succinct hash bin delegation achieves a similar result as using a standard hash
9+
bin delegation, but the delegating metadata is smaller, resulting in fewer bytes
10+
to transfer and parse.
11+
12+
See 'basic_repo.py' for a more comprehensive TUF metadata API example.
13+
14+
For a comprehensive explanation of succinct hash bin delegation and the
15+
difference between succinct and standard hash bin delegation read:
16+
https://github.com/theupdateframework/taps/blob/master/tap15.md
17+
18+
NOTE: Metadata files will be written to a 'tmp*'-directory in CWD.
19+
"""
20+
import math
21+
import os
22+
import tempfile
23+
from datetime import datetime, timedelta
24+
from pathlib import Path
25+
from typing import Dict, Tuple
26+
27+
from securesystemslib.keys import generate_ed25519_key
28+
from securesystemslib.signer import SSlibSigner
29+
30+
from tuf.api.metadata import (
31+
Delegations,
32+
Key,
33+
Metadata,
34+
SuccinctRoles,
35+
TargetFile,
36+
Targets,
37+
)
38+
from tuf.api.serialization.json import JSONSerializer
39+
40+
# Succinct hash bin delegation
41+
# ============================
42+
# Succinct hash bin delegation aims to distribute a large number of target files
43+
# over multiple delegated targets metadata roles (bins). The consequence is
44+
# smaller metadata files and thus a lower network overhead for repository-client
45+
# communication.
46+
#
47+
# The assignment of target files to a target's metadata is done automatically,
48+
# based on the byte digest of the target file name.
49+
#
50+
# The number of bins, name prefix for all bins and key threshold are all
51+
# attributes that need to be configured.
52+
53+
# Number of bins, bit length and bin number computation
54+
# -----------------------------------------------------
55+
# Determining the correct number of bins is dependent on the expected number of
56+
# target files in a repository. For the purpose of this example we choose:
57+
NUMBER_OF_BINS = 32
58+
#
59+
# The number of bins will determine the number of bits in a target path
60+
# considered in assigning the target to a bin.
61+
BIT_LENGTH = int(math.log2(NUMBER_OF_BINS))
62+
63+
# Delegated role (bin) name format
64+
# --------------------------------
65+
# Each bin has a name in the format of f"{NAME_PREFIX}-{bin_number}".
66+
#
67+
# Name prefix is the common prefix of all delegated target roles (bins).
68+
# For our example it will be:
69+
NAME_PREFIX = "delegated_bin"
70+
#
71+
# The suffix "bin_number" is a zero-padded hexadecimal number of that
72+
# particular bin.
73+
74+
# Keys and threshold
75+
# ------------------
76+
# Succinct hash bin delegation uses the same key(s) to sign all bins. This is
77+
# acceptable because the primary concern of this type of delegation is to reduce
78+
# network overhead. For the purpose of this example only one key is required.
79+
THRESHOLD = 1
80+
81+
82+
def create_key() -> Tuple[Key, SSlibSigner]:
83+
"""Generates a new Key and Signer."""
84+
sslib_key = generate_ed25519_key()
85+
return Key.from_securesystemslib_key(sslib_key), SSlibSigner(sslib_key)
86+
87+
88+
# Create one signing key for all bins, and one for the delegating targets role.
89+
bins_key, bins_signer = create_key()
90+
_, targets_signer = create_key()
91+
92+
# Delegating targets role
93+
# -----------------------
94+
# Akin to regular targets delegation, the delegating role ships the public keys
95+
# of the delegated roles. However, instead of providing individual delegation
96+
# information about each role, one single `SuccinctRoles` object is used to
97+
# provide the information for all delegated roles (bins).
98+
99+
# NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py'
100+
# example for more details about the Targets object.
101+
102+
expiration_date = datetime.utcnow().replace(microsecond=0) + timedelta(days=7)
103+
targets = Metadata(Targets(expires=expiration_date))
104+
105+
succinct_roles = SuccinctRoles(
106+
keyids=[bins_key.keyid],
107+
threshold=THRESHOLD,
108+
bit_length=BIT_LENGTH,
109+
name_prefix=NAME_PREFIX,
110+
)
111+
delegations_keys_info: Dict[str, Key] = {}
112+
delegations_keys_info[bins_key.keyid] = bins_key
113+
114+
targets.signed.delegations = Delegations(
115+
delegations_keys_info, roles=None, succinct_roles=succinct_roles
116+
)
117+
118+
# Delegated targets roles (bins)
119+
# ------------------------------
120+
# We can use the SuccinctRoles object from the delegating role above to iterate
121+
# over all bin names in the delegation and create the corresponding metadata.
122+
123+
assert targets.signed.delegations.succinct_roles is not None # make mypy happy
124+
125+
delegated_bins: Dict[str, Metadata[Targets]] = {}
126+
for delegated_bin_name in targets.signed.delegations.succinct_roles.get_roles():
127+
delegated_bins[delegated_bin_name] = Metadata(
128+
Targets(expires=expiration_date)
129+
)
130+
131+
# Add target file inside a delegated role (bin)
132+
# ---------------------------------------------
133+
# For the purpose of this example we will protect the integrity of this
134+
# example script by adding its file info to the corresponding bin metadata.
135+
136+
# NOTE: See "Targets" paragraph in 'basic_repo.py' example for more details
137+
# about adding target file infos to targets metadata.
138+
local_path = Path(__file__).resolve()
139+
target_path = f"{local_path.parts[-2]}/{local_path.parts[-1]}"
140+
target_file_info = TargetFile.from_file(target_path, str(local_path))
141+
142+
# We don't know yet in which delegated role (bin) our target belongs.
143+
# With SuccinctRoles.get_role_for_target() we can get the name of the delegated
144+
# role (bin) responsible for that target_path.
145+
target_bin = targets.signed.delegations.succinct_roles.get_role_for_target(
146+
target_path
147+
)
148+
149+
# In our example with NUMBER_OF_BINS = 32 and the current file as target_path
150+
# the target_bin is "delegated_bin-0d"
151+
152+
# Now we can add the current target to the bin responsible for it.
153+
delegated_bins[target_bin].signed.targets[target_path] = target_file_info
154+
155+
# Sign and persist
156+
# ----------------
157+
# Sign all metadata and write to a temporary directory at CWD for review using
158+
# versioned file names. Most notably see '1.targets.json' and
159+
# '1.delegated_bin-0d.json'.
160+
161+
# NOTE: See "Persist metadata" paragraph in 'basic_repo.py' example for more
162+
# details about serialization formats and metadata file name convention.
163+
PRETTY = JSONSerializer(compact=False)
164+
TMP_DIR = tempfile.mkdtemp(dir=os.getcwd())
165+
166+
167+
targets.sign(targets_signer)
168+
targets.to_file(os.path.join(TMP_DIR, "1.targets.json"), serializer=PRETTY)
169+
170+
for bin_name, bin_target_role in delegated_bins.items():
171+
file_name = f"1.{bin_name}.json"
172+
file_path = os.path.join(TMP_DIR, file_name)
173+
174+
bin_target_role.sign(bins_signer, append=True)
175+
176+
bin_target_role.to_file(file_path, serializer=PRETTY)

tests/test_examples.py

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,39 +92,79 @@ def test_hashed_bin_delegation(self) -> None:
9292
self._run_script_and_assert_files(
9393
"hashed_bin_delegation.py",
9494
[
95-
"bins.json",
96-
"00-07.json",
97-
"08-0f.json",
98-
"10-17.json",
99-
"18-1f.json",
100-
"20-27.json",
101-
"28-2f.json",
102-
"30-37.json",
103-
"38-3f.json",
104-
"40-47.json",
105-
"48-4f.json",
106-
"50-57.json",
107-
"58-5f.json",
108-
"60-67.json",
109-
"68-6f.json",
110-
"70-77.json",
111-
"78-7f.json",
112-
"80-87.json",
113-
"88-8f.json",
114-
"90-97.json",
115-
"98-9f.json",
116-
"a0-a7.json",
117-
"a8-af.json",
118-
"b0-b7.json",
119-
"b8-bf.json",
120-
"c0-c7.json",
121-
"c8-cf.json",
122-
"d0-d7.json",
123-
"d8-df.json",
124-
"e0-e7.json",
125-
"e8-ef.json",
126-
"f0-f7.json",
127-
"f8-ff.json",
95+
"1.bins.json",
96+
"1.00-07.json",
97+
"1.08-0f.json",
98+
"1.10-17.json",
99+
"1.18-1f.json",
100+
"1.20-27.json",
101+
"1.28-2f.json",
102+
"1.30-37.json",
103+
"1.38-3f.json",
104+
"1.40-47.json",
105+
"1.48-4f.json",
106+
"1.50-57.json",
107+
"1.58-5f.json",
108+
"1.60-67.json",
109+
"1.68-6f.json",
110+
"1.70-77.json",
111+
"1.78-7f.json",
112+
"1.80-87.json",
113+
"1.88-8f.json",
114+
"1.90-97.json",
115+
"1.98-9f.json",
116+
"1.a0-a7.json",
117+
"1.a8-af.json",
118+
"1.b0-b7.json",
119+
"1.b8-bf.json",
120+
"1.c0-c7.json",
121+
"1.c8-cf.json",
122+
"1.d0-d7.json",
123+
"1.d8-df.json",
124+
"1.e0-e7.json",
125+
"1.e8-ef.json",
126+
"1.f0-f7.json",
127+
"1.f8-ff.json",
128+
],
129+
)
130+
131+
def test_succinct_hash_bin_delegation(self) -> None:
132+
self._run_script_and_assert_files(
133+
"succinct_hash_bin_delegations.py",
134+
[
135+
"1.targets.json",
136+
"1.delegated_bin-00.json",
137+
"1.delegated_bin-01.json",
138+
"1.delegated_bin-02.json",
139+
"1.delegated_bin-03.json",
140+
"1.delegated_bin-04.json",
141+
"1.delegated_bin-05.json",
142+
"1.delegated_bin-06.json",
143+
"1.delegated_bin-07.json",
144+
"1.delegated_bin-08.json",
145+
"1.delegated_bin-09.json",
146+
"1.delegated_bin-0a.json",
147+
"1.delegated_bin-0b.json",
148+
"1.delegated_bin-0c.json",
149+
"1.delegated_bin-0d.json",
150+
"1.delegated_bin-0e.json",
151+
"1.delegated_bin-0f.json",
152+
"1.delegated_bin-10.json",
153+
"1.delegated_bin-11.json",
154+
"1.delegated_bin-12.json",
155+
"1.delegated_bin-13.json",
156+
"1.delegated_bin-14.json",
157+
"1.delegated_bin-15.json",
158+
"1.delegated_bin-16.json",
159+
"1.delegated_bin-17.json",
160+
"1.delegated_bin-18.json",
161+
"1.delegated_bin-19.json",
162+
"1.delegated_bin-1a.json",
163+
"1.delegated_bin-1b.json",
164+
"1.delegated_bin-1c.json",
165+
"1.delegated_bin-1d.json",
166+
"1.delegated_bin-1e.json",
167+
"1.delegated_bin-1f.json",
128168
],
129169
)
130170

tuf/api/metadata.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,8 +1495,9 @@ def __init__(
14951495
self.name_prefix = name_prefix
14961496

14971497
# Calculate the suffix_len value based on the total number of bins in
1498-
# hex. If bit_length = 8 then number_of_bins = 256 or 100 in hex
1499-
# and suffix_len = 3 meaning the third bin will have a suffix of "003"
1498+
# hex. If bit_length = 10 then number_of_bins = 1024 or bin names will
1499+
# have a suffix between "000" and "3ff" in hex and suffix_len will be 3
1500+
# meaning the third bin will have a suffix of "003".
15001501
self.number_of_bins = 2**bit_length
15011502
# suffix_len is calculated based on "number_of_bins - 1" as the name
15021503
# of the last bin contains the number "number_of_bins -1" as a suffix.

0 commit comments

Comments
 (0)