Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for TAP 15 #1948

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions tests/repository_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ def add_target(self, role: str, data: bytes, path: str) -> None:
self.target_files[path] = RepositoryTarget(data, target)

def add_delegation(
self, delegator_name: str, role: DelegatedRole, targets: Targets
self,
delegator_name: str,
role: DelegatedRole,
targets: Optional[Targets],
) -> None:
"""Add delegated target role to the repository."""
if delegator_name == Targets.type:
Expand All @@ -357,17 +360,33 @@ def add_delegation(
# Create delegation
if delegator.delegations is None:
delegator.delegations = Delegations({}, {})
# put delegation last by default
delegator.delegations.roles[role.name] = role

# By default add one new key for the role
role_name: str = role.name if role.name is not None else ""
key, signer = self.create_key()
delegator.add_key(role.name, key)
self.add_signer(role.name, signer)

# Add metadata for the role
if role.name not in self.md_delegates:
self.md_delegates[role.name] = Metadata(targets, {})
if role.succinct_hash_info:
# Add target metadata for all bins.
for delegated_name in role.succinct_hash_info.get_all_bin_names():
self.md_delegates[delegated_name] = Metadata(
Targets(expires=self.safe_expiry)
)

self.add_signer(delegated_name, signer)

role_name = role.succinct_hash_info.get_bin_name()

else:
# By default add one new key for the role
self.add_signer(role_name, signer)

# Add metadata for the role
if targets is not None:
if role_name not in self.md_delegates:
self.md_delegates[role_name] = Metadata(targets)

# put delegation last by default
delegator.delegations.roles[role_name] = role
delegator.add_key(role_name, key)

def write(self) -> None:
"""Dump current repository metadata to self.dump_dir
Expand Down
115 changes: 115 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import tempfile
import unittest
from copy import copy
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, ClassVar, Dict

Expand All @@ -35,6 +36,7 @@
Metadata,
Root,
Snapshot,
SuccinctHashDelegations,
TargetFile,
Targets,
Timestamp,
Expand Down Expand Up @@ -695,6 +697,119 @@ def test_is_delegated_role(self) -> None:
self.assertFalse(role.is_delegated_path("a/non-matching path"))
self.assertTrue(role.is_delegated_path("a/path"))

@dataclass
class TestData:
prefix_bit_len: int
target_path: str
expected_bin_name: str

# Every hex digit can be fitted in 4 bits. This means that if
# prefix_bit_len is a power of 4, then the expected bin name will have a
# suffix which will contain and a (prefix_bit_len / 4) number of common hex
# digits with target_hash. Part of the test cases verify that.
# For "target.txt" the hex is: 199b3badd968634ea14e351d1134ada738894a90a2efa66983101ece99a33572

# The rest of the test cases are validating that zero padding is added.
find_delegation_dataset: utils.DataSet = {
"1 hash-prefix": TestData(
prefix_bit_len=1,
target_path="target.txt",
expected_bin_name="delegated-0",
),
"4 hash_prefix": TestData(
prefix_bit_len=4,
target_path="target.txt",
expected_bin_name="delegated-1",
),
"5 hash_prefix": TestData(
prefix_bit_len=5,
target_path="target.txt",
expected_bin_name="delegated-03", # Validating zero padding.
),
"8 hash_prefix": TestData(
prefix_bit_len=8,
target_path="target.txt",
expected_bin_name="delegated-19",
),
"9 hash_prefix": TestData(
prefix_bit_len=9,
target_path="target.txt",
expected_bin_name="delegated-033", # Validating zero padding.
),
"16 hash_prefix": TestData(
prefix_bit_len=16,
target_path="target.txt",
expected_bin_name="delegated-199b",
),
"21 hash_prefix": TestData(
prefix_bit_len=21,
target_path="target.txt",
expected_bin_name="delegated-033367", # Validating zero padding.
),
"24 hash_prefix": TestData(
prefix_bit_len=24,
target_path="target.txt",
expected_bin_name="delegated-199b3b",
),
"31 hash_prefix": TestData(
prefix_bit_len=31,
target_path="target.txt",
expected_bin_name="delegated-0ccd9dd6", # Validating zero padding.
),
"32 hash_prefix": TestData(
prefix_bit_len=32,
target_path="target.txt",
expected_bin_name="delegated-199b3bad",
),
}

@utils.run_sub_tests_with_dataset(find_delegation_dataset)
def test_find_delegation_with_succinct_hash_info(
self, test_data: TestData
) -> None:
r = DelegatedRole(
None,
[],
1,
False,
succinct_hash_info=SuccinctHashDelegations(
test_data.prefix_bit_len, "delegated"
),
)
assert r.succinct_hash_info is not None

result_bin = r.find_delegation(test_data.target_path)
self.assertEqual(result_bin, test_data.expected_bin_name)

def test_is_bin_in_succinct_hash_delegations(self) -> None:
succinct_delegation = SuccinctHashDelegations(5, "delegated")
# delegated role name suffixes are in hex format.
false_role_name_examples = [
"foo",
"delegated-",
"delegated-s",
"delegated-20",
"delegated-100",
]
for role_name in false_role_name_examples:
msg = f"Error for {role_name}"
self.assertFalse(succinct_delegation.is_bin(role_name), msg)

true_role_name_examples = ["delegated-0", "delegated-f", "delegated-1f"]
for role_name in true_role_name_examples:
msg = f"Error for {role_name}"
self.assertTrue(succinct_delegation.is_bin(role_name), msg)

def test_get_all_bin_names_in_succinct_hash_delegations(self) -> None:
succinct_delegation = SuccinctHashDelegations(3, "delegated")
bin_amount = 0
for i, role_name in enumerate(succinct_delegation.get_all_bin_names()):
self.assertEqual(role_name, f"delegated-{i}")
bin_amount = i

# Assert that the last bin is number 7 (starting to count from 0).
self.assertEqual(bin_amount, 7)


# Run unit test.
if __name__ == "__main__":
Expand Down
15 changes: 15 additions & 0 deletions tests/test_metadata_eq_.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Role,
Root,
Snapshot,
SuccinctHashDelegations,
TargetFile,
Targets,
Timestamp,
Expand Down Expand Up @@ -194,6 +195,19 @@ def test_snapshot_eq_(self) -> None:
setattr(signed_copy, "meta", None)
self.assertNotEqual(md.signed, signed_copy)

def test_hash_delegations_eq(self) -> None:
hash_deleg = SuccinctHashDelegations(8, "delegated")
hash_deleg_2: SuccinctHashDelegations = self.copy_and_simple_assert(
hash_deleg
)

for attr, value in [("prefix_bit_len", 0), ("bin_name_prefix", "")]:
setattr(hash_deleg_2, attr, value)
msg = f"Failed case: {attr}"
self.assertNotEqual(hash_deleg, hash_deleg_2, msg)
# Restore the old value of the attribute.
setattr(hash_deleg_2, attr, getattr(hash_deleg, attr))

def test_delegated_role_eq_(self) -> None:
delegated_role_dict = {
"keyids": ["keyid"],
Expand All @@ -213,6 +227,7 @@ def test_delegated_role_eq_(self) -> None:
("terminating", None),
("paths", [""]),
("path_hash_prefixes", [""]),
("succinct_hash_info", ""),
]:
setattr(delegated_role_2, attr, value)
msg = f"Failed case: {attr}"
Expand Down
58 changes: 51 additions & 7 deletions tests/test_metadata_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Role,
Root,
Snapshot,
SuccinctHashDelegations,
TargetFile,
Targets,
Timestamp,
Expand Down Expand Up @@ -366,12 +367,48 @@ def test_snapshot_serialization(self, test_case_data: str) -> None:
snapshot = Snapshot.from_dict(copy.deepcopy(case_dict))
self.assertDictEqual(case_dict, snapshot.to_dict())

valid_succinct_delegations: utils.DataSet = {
"standard succinct_hash_delegation information": '{"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo"}',
"succinct_hash_delegation with unrecognized fields": '{"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo", "foo": "bar"}',
}

@utils.run_sub_tests_with_dataset(valid_succinct_delegations)
def test_succinct_delegations_serialization(
self, test_case_data: str
) -> None:
case_dict = json.loads(test_case_data)
succinct_delegations = SuccinctHashDelegations.from_dict(
copy.copy(case_dict)
)
self.assertDictEqual(case_dict, succinct_delegations.to_dict())

invalid_succinct_delegations: utils.DataSet = {
"missing delegation_hash_prefix_len from succinct_hash_delegations": '{"bin_name_prefix": "foo"}',
"missing bin_name_prefix from succinct_hash_delegations": '{"delegation_hash_prefix_len": 8}',
"succinct_hash_delegation with invalid delegation_hash_prefix_len type": '{"delegation_hash_prefix_len": "a", "bin_name_prefix": "foo"}',
"succinct_hash_delegation with invalid bin_name_prefix type": '{"delegation_hash_prefix_len": 8, "bin_name_prefix": 1}',
"succinct_hash_delegation with high delegation_hash_prefix_len value": '{"delegation_hash_prefix_len": 50, "bin_name_prefix": "foo"}',
"succinct_hash_delegation with low delegation_hash_prefix_len value": '{"delegation_hash_prefix_len": 0, "bin_name_prefix": "foo"}',
}

@utils.run_sub_tests_with_dataset(invalid_succinct_delegations)
def test_invalid_succinct_delegations_serialization(
self, test_case_data: str
) -> None:
case_dict = json.loads(test_case_data)
with self.assertRaises((ValueError, KeyError, TypeError)):
SuccinctHashDelegations.from_dict(case_dict)

valid_delegated_roles: utils.DataSet = {
# DelegatedRole inherits Role and some use cases can be found in the valid_roles.
"no hash prefix attribute": '{"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], \
"missing name when succinct_hash_delegations is used": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 1, "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo"}}',
"no hash prefix and succinct_hash_delegations": '{"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], \
"terminating": false, "threshold": 1}',
"no path attribute": '{"keyids": ["keyid"], "name": "a", "terminating": false, \
"no path and succinct_hash_delegations": '{"keyids": ["keyid"], "name": "a", "terminating": false, \
"path_hash_prefixes": ["h1", "h2"], "threshold": 99}',
"no path and path_hash_prefixes": '{"keyids": ["keyid"], "terminating": false, \
"threshold": 99, "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo"}}',
"empty paths": '{"keyids": ["keyid"], "name": "a", "paths": [], \
"terminating": false, "threshold": 1}',
"empty path_hash_prefixes": '{"keyids": ["keyid"], "name": "a", "terminating": false, \
Expand All @@ -386,26 +423,33 @@ def test_snapshot_serialization(self, test_case_data: str) -> None:
@utils.run_sub_tests_with_dataset(valid_delegated_roles)
def test_delegated_role_serialization(self, test_case_data: str) -> None:
case_dict = json.loads(test_case_data)
deserialized_role = DelegatedRole.from_dict(copy.copy(case_dict))
deserialized_role = DelegatedRole.from_dict(copy.deepcopy(case_dict))
self.assertDictEqual(case_dict, deserialized_role.to_dict())

invalid_delegated_roles: utils.DataSet = {
# DelegatedRole inherits Role and some use cases can be found in the invalid_roles.
"missing hash prefixes and paths": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false}',
"both hash prefixes and paths": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, \
"paths": ["fn1", "fn2"], "path_hash_prefixes": ["h1", "h2"]}',
"missing name and succinct_hash_delegations": '{"keyids": ["keyid"], "paths": ["fn1", "fn2"], \
"terminating": false, "threshold": 1}',
"missing hash prefixes, paths and succinct_hash_delegations": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false}',
"hash prefixes, paths and succinct_hash_delegations set": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, \
"paths": ["fn1", "fn2"], "path_hash_prefixes": ["h1", "h2"], "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "f"}}',
"paths and succinct_hash_delegations set": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, \
"paths": ["fn1", "fn2"], "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "f"}}',
"path_hash_prefixes and succinct_hash_delegations": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, \
"path_hash_prefixes": ["h1", "h2"], "succinct_hash_delegations": {"delegation_hash_prefix_len": 8, "bin_name_prefix": "foo"}}',
"invalid path type": '{"keyids": ["keyid"], "name": "a", "paths": [1,2,3], \
"terminating": false, "threshold": 1}',
"invalid path_hash_prefixes type": '{"keyids": ["keyid"], "name": "a", "path_hash_prefixes": [1,2,3], \
"terminating": false, "threshold": 1}',
"invalid succinct_hash_delegations type": '{"name": "a", "keyids": ["keyid"], "threshold": 1, "terminating": false, "succinct_hash_delegations": ""}',
}

@utils.run_sub_tests_with_dataset(invalid_delegated_roles)
def test_invalid_delegated_role_serialization(
self, test_case_data: str
) -> None:
case_dict = json.loads(test_case_data)
with self.assertRaises(ValueError):
with self.assertRaises((ValueError, AttributeError)):
DelegatedRole.from_dict(case_dict)

invalid_delegations: utils.DataSet = {
Expand Down
Loading