Skip to content

Commit

Permalink
Merge pull request #2038 from MVrachev/tap15-example
Browse files Browse the repository at this point in the history
Add an example script about succinct roles usage
  • Loading branch information
lukpueh committed Aug 12, 2022
2 parents d9153ee + e9ef5b6 commit a773e8f
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 42 deletions.
12 changes: 5 additions & 7 deletions examples/repo_example/hashed_bin_delegation.py
Expand Up @@ -26,7 +26,6 @@
from securesystemslib.signer import SSlibSigner

from tuf.api.metadata import (
SPECIFICATION_VERSION,
DelegatedRole,
Delegations,
Key,
Expand All @@ -42,8 +41,7 @@ def _in(days: float) -> datetime:
return datetime.utcnow().replace(microsecond=0) + timedelta(days=days)


SPEC_VERSION = ".".join(SPECIFICATION_VERSION)
roles: Dict[str, Metadata] = {}
roles: Dict[str, Metadata[Targets]] = {}
keys: Dict[str, Dict[str, Any]] = {}

# Hash bin delegation
Expand Down Expand Up @@ -200,11 +198,11 @@ def find_hash_bin(path: str) -> str:

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

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

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

filename = f"{role_name}.json"
filename = f"1.{role_name}.json"
filepath = os.path.join(TMP_DIR, filename)
role.to_file(filepath, serializer=PRETTY)
176 changes: 176 additions & 0 deletions examples/repo_example/succinct_hash_bin_delegations.py
@@ -0,0 +1,176 @@
# Copyright New York University and the TUF contributors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""
A TUF succinct hash bin delegation example using the low-level TUF Metadata API.
The example code in this file demonstrates how to perform succinct hash bin
delegation using the low-level Metadata API.
Succinct hash bin delegation achieves a similar result as using a standard hash
bin delegation, but the delegating metadata is smaller, resulting in fewer bytes
to transfer and parse.
See 'basic_repo.py' for a more comprehensive TUF metadata API example.
For a comprehensive explanation of succinct hash bin delegation and the
difference between succinct and standard hash bin delegation read:
https://github.com/theupdateframework/taps/blob/master/tap15.md
NOTE: Metadata files will be written to a 'tmp*'-directory in CWD.
"""
import math
import os
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Tuple

from securesystemslib.keys import generate_ed25519_key
from securesystemslib.signer import SSlibSigner

from tuf.api.metadata import (
Delegations,
Key,
Metadata,
SuccinctRoles,
TargetFile,
Targets,
)
from tuf.api.serialization.json import JSONSerializer

# Succinct hash bin delegation
# ============================
# Succinct hash bin delegation aims to distribute a large number of target files
# over multiple delegated targets metadata roles (bins). The consequence is
# smaller metadata files and thus a lower network overhead for repository-client
# communication.
#
# The assignment of target files to a target's metadata is done automatically,
# based on the byte digest of the target file name.
#
# The number of bins, name prefix for all bins and key threshold are all
# attributes that need to be configured.

# Number of bins, bit length and bin number computation
# -----------------------------------------------------
# Determining the correct number of bins is dependent on the expected number of
# target files in a repository. For the purpose of this example we choose:
NUMBER_OF_BINS = 32
#
# The number of bins will determine the number of bits in a target path
# considered in assigning the target to a bin.
BIT_LENGTH = int(math.log2(NUMBER_OF_BINS))

# Delegated role (bin) name format
# --------------------------------
# Each bin has a name in the format of f"{NAME_PREFIX}-{bin_number}".
#
# Name prefix is the common prefix of all delegated target roles (bins).
# For our example it will be:
NAME_PREFIX = "delegated_bin"
#
# The suffix "bin_number" is a zero-padded hexadecimal number of that
# particular bin.

# Keys and threshold
# ------------------
# Succinct hash bin delegation uses the same key(s) to sign all bins. This is
# acceptable because the primary concern of this type of delegation is to reduce
# network overhead. For the purpose of this example only one key is required.
THRESHOLD = 1


def create_key() -> Tuple[Key, SSlibSigner]:
"""Generates a new Key and Signer."""
sslib_key = generate_ed25519_key()
return Key.from_securesystemslib_key(sslib_key), SSlibSigner(sslib_key)


# Create one signing key for all bins, and one for the delegating targets role.
bins_key, bins_signer = create_key()
_, targets_signer = create_key()

# Delegating targets role
# -----------------------
# Akin to regular targets delegation, the delegating role ships the public keys
# of the delegated roles. However, instead of providing individual delegation
# information about each role, one single `SuccinctRoles` object is used to
# provide the information for all delegated roles (bins).

# NOTE: See "Targets" and "Targets delegation" paragraphs in 'basic_repo.py'
# example for more details about the Targets object.

expiration_date = datetime.utcnow().replace(microsecond=0) + timedelta(days=7)
targets = Metadata(Targets(expires=expiration_date))

succinct_roles = SuccinctRoles(
keyids=[bins_key.keyid],
threshold=THRESHOLD,
bit_length=BIT_LENGTH,
name_prefix=NAME_PREFIX,
)
delegations_keys_info: Dict[str, Key] = {}
delegations_keys_info[bins_key.keyid] = bins_key

targets.signed.delegations = Delegations(
delegations_keys_info, roles=None, succinct_roles=succinct_roles
)

# Delegated targets roles (bins)
# ------------------------------
# We can use the SuccinctRoles object from the delegating role above to iterate
# over all bin names in the delegation and create the corresponding metadata.

assert targets.signed.delegations.succinct_roles is not None # make mypy happy

delegated_bins: Dict[str, Metadata[Targets]] = {}
for delegated_bin_name in targets.signed.delegations.succinct_roles.get_roles():
delegated_bins[delegated_bin_name] = Metadata(
Targets(expires=expiration_date)
)

# Add target file inside a delegated role (bin)
# ---------------------------------------------
# For the purpose of this example we will protect the integrity of this
# example script by adding its file info to the corresponding bin metadata.

# NOTE: See "Targets" paragraph in 'basic_repo.py' example for more details
# about adding target file infos to targets metadata.
local_path = Path(__file__).resolve()
target_path = f"{local_path.parts[-2]}/{local_path.parts[-1]}"
target_file_info = TargetFile.from_file(target_path, str(local_path))

# We don't know yet in which delegated role (bin) our target belongs.
# With SuccinctRoles.get_role_for_target() we can get the name of the delegated
# role (bin) responsible for that target_path.
target_bin = targets.signed.delegations.succinct_roles.get_role_for_target(
target_path
)

# In our example with NUMBER_OF_BINS = 32 and the current file as target_path
# the target_bin is "delegated_bin-0d"

# Now we can add the current target to the bin responsible for it.
delegated_bins[target_bin].signed.targets[target_path] = target_file_info

# Sign and persist
# ----------------
# Sign all metadata and write to a temporary directory at CWD for review using
# versioned file names. Most notably see '1.targets.json' and
# '1.delegated_bin-0d.json'.

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


targets.sign(targets_signer)
targets.to_file(os.path.join(TMP_DIR, "1.targets.json"), serializer=PRETTY)

for bin_name, bin_target_role in delegated_bins.items():
file_name = f"1.{bin_name}.json"
file_path = os.path.join(TMP_DIR, file_name)

bin_target_role.sign(bins_signer, append=True)

bin_target_role.to_file(file_path, serializer=PRETTY)
106 changes: 73 additions & 33 deletions tests/test_examples.py
Expand Up @@ -92,39 +92,79 @@ def test_hashed_bin_delegation(self) -> None:
self._run_script_and_assert_files(
"hashed_bin_delegation.py",
[
"bins.json",
"00-07.json",
"08-0f.json",
"10-17.json",
"18-1f.json",
"20-27.json",
"28-2f.json",
"30-37.json",
"38-3f.json",
"40-47.json",
"48-4f.json",
"50-57.json",
"58-5f.json",
"60-67.json",
"68-6f.json",
"70-77.json",
"78-7f.json",
"80-87.json",
"88-8f.json",
"90-97.json",
"98-9f.json",
"a0-a7.json",
"a8-af.json",
"b0-b7.json",
"b8-bf.json",
"c0-c7.json",
"c8-cf.json",
"d0-d7.json",
"d8-df.json",
"e0-e7.json",
"e8-ef.json",
"f0-f7.json",
"f8-ff.json",
"1.bins.json",
"1.00-07.json",
"1.08-0f.json",
"1.10-17.json",
"1.18-1f.json",
"1.20-27.json",
"1.28-2f.json",
"1.30-37.json",
"1.38-3f.json",
"1.40-47.json",
"1.48-4f.json",
"1.50-57.json",
"1.58-5f.json",
"1.60-67.json",
"1.68-6f.json",
"1.70-77.json",
"1.78-7f.json",
"1.80-87.json",
"1.88-8f.json",
"1.90-97.json",
"1.98-9f.json",
"1.a0-a7.json",
"1.a8-af.json",
"1.b0-b7.json",
"1.b8-bf.json",
"1.c0-c7.json",
"1.c8-cf.json",
"1.d0-d7.json",
"1.d8-df.json",
"1.e0-e7.json",
"1.e8-ef.json",
"1.f0-f7.json",
"1.f8-ff.json",
],
)

def test_succinct_hash_bin_delegation(self) -> None:
self._run_script_and_assert_files(
"succinct_hash_bin_delegations.py",
[
"1.targets.json",
"1.delegated_bin-00.json",
"1.delegated_bin-01.json",
"1.delegated_bin-02.json",
"1.delegated_bin-03.json",
"1.delegated_bin-04.json",
"1.delegated_bin-05.json",
"1.delegated_bin-06.json",
"1.delegated_bin-07.json",
"1.delegated_bin-08.json",
"1.delegated_bin-09.json",
"1.delegated_bin-0a.json",
"1.delegated_bin-0b.json",
"1.delegated_bin-0c.json",
"1.delegated_bin-0d.json",
"1.delegated_bin-0e.json",
"1.delegated_bin-0f.json",
"1.delegated_bin-10.json",
"1.delegated_bin-11.json",
"1.delegated_bin-12.json",
"1.delegated_bin-13.json",
"1.delegated_bin-14.json",
"1.delegated_bin-15.json",
"1.delegated_bin-16.json",
"1.delegated_bin-17.json",
"1.delegated_bin-18.json",
"1.delegated_bin-19.json",
"1.delegated_bin-1a.json",
"1.delegated_bin-1b.json",
"1.delegated_bin-1c.json",
"1.delegated_bin-1d.json",
"1.delegated_bin-1e.json",
"1.delegated_bin-1f.json",
],
)

Expand Down
5 changes: 3 additions & 2 deletions tuf/api/metadata.py
Expand Up @@ -1495,8 +1495,9 @@ def __init__(
self.name_prefix = name_prefix

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

0 comments on commit a773e8f

Please sign in to comment.