Skip to content

Commit b8ea2fb

Browse files
author
Martin Vrachev
committed
Add an example script about succinct roles usage
Add a basic example script showing all features of the succinct hash bin delegations and the available API calls of SuccinctRoles. The explanations are used to promote the usage of succinct hash bin delegations by explaining it well enough so our users can understand the API limitations and how to use them and at the same time I tried not going into too many details of the SuccinctRoles math as its implementation is inside tuf/api/metadata.py and there there are explanations about that. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent bfcd3a5 commit b8ea2fb

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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 a
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 the 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+
# The right number of bins depends on the expected number of target files in a
56+
# repository. For the purpose of this example we choose:
57+
NUMBER_OF_BINS = 32
58+
#
59+
# The bit length is the number of bins that will be used to calculate the bin
60+
# for each target path. It can be calculated directly from NUMBER_OF_BINS:
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+
# Given that the primary concern of succinct hash bin delegation is to reduce
77+
# network overhead it was decided that all bins will be signed by the same
78+
# set of one or more signing keys.
79+
#
80+
# Before generating the keys a decision has to be made about the number of keys
81+
# required for signing the bins or in other words the value of the threshold.
82+
# For the purpose of this example we choose the threshold to be:
83+
THRESHOLD = 1
84+
# Note: If THRESHOLD is changed to more than 1 the example should be modified.
85+
86+
87+
def create_key() -> Tuple[Key, SSlibSigner]:
88+
"""Generates a new Key and Signer."""
89+
sslib_key = generate_ed25519_key()
90+
return Key.from_securesystemslib_key(sslib_key), SSlibSigner(sslib_key)
91+
92+
93+
key, signer = create_key()
94+
95+
# Top level targets instance with succinct hash bin delegation
96+
# ------------------------------------------------------------
97+
# NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py'
98+
# example for more details about the Targets object.
99+
#
100+
# Now we have all the ingredients needed to create a Targets instance using
101+
# succinct hash bin delegation.
102+
#
103+
# First, we create a Targets metadata instance without any delegations.
104+
105+
# We define expire as 7 days from today.
106+
expiration_date = datetime.utcnow().replace(microsecond=0) + timedelta(days=7)
107+
targets = Metadata(Targets(expires=expiration_date))
108+
109+
# Then, we want to add delegations and with it information about the succinct
110+
# hash bin delegations which are represented by SuccinctRoles instance.
111+
#
112+
# Using succinct hash bin delegations has two restrictions:
113+
# 1) no other delegated roles have to be used
114+
# 2) only one succinct hash bin delegation can exist for one targets role
115+
116+
# We have all information needed to create a SuccinctRoles instance:
117+
succinct_roles = SuccinctRoles(
118+
keyids=[],
119+
threshold=THRESHOLD,
120+
bit_length=BIT_LENGTH,
121+
name_prefix=NAME_PREFIX,
122+
)
123+
124+
# Now we will populate the keyids by using the succinct_roles_keys list.
125+
delegations_keys_info: Dict[str, Key] = {}
126+
succinct_roles.keyids.append(key.keyid)
127+
delegations_keys_info[key.keyid] = key
128+
129+
# We are ready to define the Delegations instance which we will add to targets.
130+
# As mentioned, standard roles are not allowed together with succinct_roles.
131+
delegations = Delegations(
132+
delegations_keys_info, roles=None, succinct_roles=succinct_roles
133+
)
134+
135+
targets.signed.delegations = delegations
136+
137+
# Delegated targets (bins) roles
138+
# ------------------------------
139+
# We have defined the top-level targets metadata instance which utilizes
140+
# succinct_roles. With succinct_roles we have defined the bins number, common
141+
# bin properties and keys information, but we haven't actually created the
142+
# bins targets metadata instances.
143+
144+
# mypy linter requires that we verify that succinct_roles is not None
145+
assert targets.signed.delegations.succinct_roles is not None
146+
147+
# We can get all bin names for a SuccinctRoles instance with get_roles()
148+
delegated_bins: Dict[str, Metadata[Targets]] = {}
149+
for delegated_bin_name in targets.signed.delegations.succinct_roles.get_roles():
150+
delegated_bins[delegated_bin_name] = Metadata(
151+
Targets(expires=expiration_date)
152+
)
153+
154+
# Add target file inside a delegated role (bin)
155+
# ---------------------------------------------
156+
# For the purpose of this example we will protect the integrity of this very
157+
# example script by adding its file info to the corresponding bin metadata.
158+
159+
# NOTE: See "Targets" paragraph in 'basic_repo.py' example for more details
160+
# about adding target file infos to targets metadata.
161+
local_path = Path(__file__).resolve()
162+
target_path = f"{local_path.parts[-2]}/{local_path.parts[-1]}"
163+
target_file_info = TargetFile.from_file(target_path, str(local_path))
164+
165+
# We don't know yet in which delegated role (bin) our target belongs.
166+
# With SuccinctRoles.get_role_for_target() we can get the name of the delegated
167+
# role (bin) responsible for that target_path.
168+
target_bin = targets.signed.delegations.succinct_roles.get_role_for_target(
169+
target_path
170+
)
171+
172+
# In our example with NUMBER_OF_BINS = 32 and the current file as target_path
173+
# the target_bin is "delegated_bin-0d"
174+
175+
# Now we can add the current target to the bin responsible for it.
176+
delegated_bins[target_bin].signed.targets[target_path] = target_file_info
177+
178+
# Sign and persist
179+
# ----------------
180+
# Sign all metadata and persist to a temporary directory at CWD for review using
181+
# versioned file names. Most notably see 'targets.json' and
182+
# 'delegated_bin-0d.json'. For more information on versioned file names see:
183+
# https://theupdateframework.github.io/specification/latest/#writing-consistent-snapshots
184+
185+
# NOTE: See "Persist metadata" paragraph in 'basic_repo.py' example for more
186+
# details about serialization formats and metadata file name convention.
187+
PRETTY = JSONSerializer(compact=False)
188+
TMP_DIR = tempfile.mkdtemp(dir=os.getcwd())
189+
190+
# Generate a key for targets we haven't added one up to this point.
191+
_, targets_signer = create_key()
192+
targets.sign(targets_signer)
193+
targets.to_file(os.path.join(TMP_DIR, "1.targets.json"), serializer=PRETTY)
194+
195+
for bin_name, bin_target_role in delegated_bins.items():
196+
file_name = f"1.{bin_name}.json"
197+
file_path = os.path.join(TMP_DIR, file_name)
198+
199+
bin_target_role.sign(signer, append=True)
200+
201+
bin_target_role.to_file(file_path, serializer=PRETTY)

tests/test_examples.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,46 @@ def test_hashed_bin_delegation(self) -> None:
128128
],
129129
)
130130

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",
168+
],
169+
)
170+
131171

132172
if __name__ == "__main__":
133173
utils.configure_test_logging(sys.argv)

0 commit comments

Comments
 (0)