Skip to content

Commit 9259ced

Browse files
author
Martin Vrachev
committed
Add SuccinctRole class
Add SuccinctRoles class containing the information from the succint_roles dict described in TAP 15. This allows for easy mypy checks on the types, easy enforcement on TAP 15 restrictions (as for example that "bit_length" must be between 1 and 32) and support for unrecognized fields inside succinct_roles without much of a hassle. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent b8360a2 commit 9259ced

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

tests/test_metadata_eq_.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Metadata,
2424
MetaFile,
2525
Role,
26+
SuccinctRoles,
2627
TargetFile,
2728
)
2829

@@ -55,6 +56,7 @@ def setUpClass(cls) -> None:
5556
cls.objects["Role"] = Role(["keyid1", "keyid2"], 3)
5657
cls.objects["MetaFile"] = MetaFile(1, 12, {"sha256": "abc"})
5758
cls.objects["DelegatedRole"] = DelegatedRole("a", [], 1, False, ["d"])
59+
cls.objects["SuccinctRoles"] = SuccinctRoles(["keyid"], 1, 8, "foo")
5860
cls.objects["Delegations"] = Delegations(
5961
{"keyid": cls.objects["Key"]}, {"a": cls.objects["DelegatedRole"]}
6062
)
@@ -79,6 +81,7 @@ def setUpClass(cls) -> None:
7981
"paths": [""],
8082
"path_hash_prefixes": [""],
8183
},
84+
"SuccinctRoles": {"bit_length": 0, "name_prefix": ""},
8285
"Delegations": {"keys": {}, "roles": {}},
8386
"TargetFile": {"length": 0, "hashes": {}, "path": ""},
8487
"Targets": {"targets": {}, "delegations": []},

tests/test_metadata_serialization.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
Role,
2525
Root,
2626
Snapshot,
27+
SuccinctRoles,
2728
TargetFile,
2829
Targets,
2930
Timestamp,
@@ -408,6 +409,36 @@ def test_invalid_delegated_role_serialization(
408409
with self.assertRaises(ValueError):
409410
DelegatedRole.from_dict(case_dict)
410411

412+
valid_succinct_roles: utils.DataSet = {
413+
# SuccinctRoles inherits Role and some use cases can be found in the valid_roles.
414+
"standard succinct_roles information": '{"keyids": ["keyid"], "threshold": 1, \
415+
"bit_length": 8, "name_prefix": "foo"}',
416+
"succinct_roles with unrecognized fields": '{"keyids": ["keyid"], "threshold": 1, \
417+
"bit_length": 8, "name_prefix": "foo", "foo": "bar"}',
418+
}
419+
420+
@utils.run_sub_tests_with_dataset(valid_succinct_roles)
421+
def test_succinct_roles_serialization(self, test_case_data: str) -> None:
422+
case_dict = json.loads(test_case_data)
423+
succinct_roles = SuccinctRoles.from_dict(copy.copy(case_dict))
424+
self.assertDictEqual(case_dict, succinct_roles.to_dict())
425+
426+
invalid_succinct_roles: utils.DataSet = {
427+
# SuccinctRoles inherits Role and some use cases can be found in the invalid_roles.
428+
"missing bit_length from succinct_roles": '{"keyids": ["keyid"], "threshold": 1, "name_prefix": "foo"}',
429+
"missing name_prefix from succinct_roles": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 8}',
430+
"succinct_roles with invalid bit_length type": '{"keyids": ["keyid"], "threshold": 1, "bit_length": "a", "name_prefix": "foo"}',
431+
"succinct_roles with invalid name_prefix type": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 8, "name_prefix": 1}',
432+
"succinct_roles with high bit_length value": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 50, "name_prefix": "foo"}',
433+
"succinct_roles with low bit_length value": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 0, "name_prefix": "foo"}',
434+
}
435+
436+
@utils.run_sub_tests_with_dataset(invalid_succinct_roles)
437+
def test_invalid_succinct_roles_serialization(self, test_data: str) -> None:
438+
case_dict = json.loads(test_data)
439+
with self.assertRaises((ValueError, KeyError, TypeError)):
440+
SuccinctRoles.from_dict(case_dict)
441+
411442
invalid_delegations: utils.DataSet = {
412443
"empty delegations": "{}",
413444
"missing keys": '{ "roles": [ \

tuf/api/metadata.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,109 @@ def is_delegated_path(self, target_filepath: str) -> bool:
14351435
return False
14361436

14371437

1438+
class SuccinctRoles(Role):
1439+
"""Succinctly defines a hash bin delegation graph.
1440+
1441+
A ``SuccinctRoles`` object describes a delegation graph that covers all
1442+
targets, distributing them uniformly over the delegated roles (i.e. bins)
1443+
in the graph.
1444+
1445+
The total number of bins is 2 to the power of the passed ``bit_length``.
1446+
Targets are assigned to bins by casting the left-most ``bit_length`` of
1447+
bits of the file path hash digest to int, using it as bin index between 0
1448+
and ``2**bit_length - 1``.
1449+
1450+
Bin names are the concatenation of the passed ``name_prefix`` and a hex
1451+
representation of the bin index between separated by a hyphen.
1452+
1453+
The passed ``keyids`` and ``threshold`` is used for each bin, and each bin
1454+
is 'terminating'.
1455+
1456+
For details: https://github.com/theupdateframework/taps/blob/master/tap15.md
1457+
1458+
Args:
1459+
keyids: Signing key identifiers for any bin metadata.
1460+
threshold: Number of keys required to sign any bin metadata.
1461+
bit_length: Number of bits between 1 and 32.
1462+
name_prefix: Prefix of all bin names.
1463+
unrecognized_fields: Dictionary of all attributes that are not managed
1464+
by TUF Metadata API.
1465+
1466+
Raises:
1467+
ValueError, TypeError, AttributeError: Invalid arguments.
1468+
"""
1469+
1470+
def __init__(
1471+
self,
1472+
keyids: List[str],
1473+
threshold: int,
1474+
bit_length: int,
1475+
name_prefix: str,
1476+
unrecognized_fields: Optional[Dict[str, Any]] = None,
1477+
) -> None:
1478+
super().__init__(keyids, threshold, unrecognized_fields)
1479+
1480+
if bit_length <= 0 or bit_length > 32:
1481+
raise ValueError("bit_length must be between 1 and 32")
1482+
if not isinstance(name_prefix, str):
1483+
raise ValueError("name_prefix must be a string")
1484+
1485+
self.bit_length = bit_length
1486+
self.name_prefix = name_prefix
1487+
1488+
def __eq__(self, other: Any) -> bool:
1489+
if not isinstance(other, SuccinctRoles):
1490+
return False
1491+
1492+
return (
1493+
super().__eq__(other)
1494+
and self.bit_length == other.bit_length
1495+
and self.name_prefix == other.name_prefix
1496+
)
1497+
1498+
@classmethod
1499+
def from_dict(cls, role_dict: Dict[str, Any]) -> "SuccinctRoles":
1500+
"""Creates ``SuccinctRoles`` object from its json/dict representation.
1501+
1502+
Raises:
1503+
ValueError, KeyError, AttributeError, TypeError: Invalid arguments.
1504+
"""
1505+
keyids = role_dict.pop("keyids")
1506+
threshold = role_dict.pop("threshold")
1507+
bit_length = role_dict.pop("bit_length")
1508+
name_prefix = role_dict.pop("name_prefix")
1509+
# All fields left in the role_dict are unrecognized.
1510+
return cls(keyids, threshold, bit_length, name_prefix, role_dict)
1511+
1512+
def to_dict(self) -> Dict[str, Any]:
1513+
"""Returns the dict representation of self."""
1514+
base_role_dict = super().to_dict()
1515+
return {
1516+
"bit_length": self.bit_length,
1517+
"name_prefix": self.name_prefix,
1518+
**base_role_dict,
1519+
}
1520+
1521+
def get_role_for_target(self, target_filepath: str) -> str:
1522+
"""Calculates the name of the delegated role responsible for
1523+
``target_filepath``.
1524+
1525+
Args:
1526+
target_filepath: URL path to a target file, relative to a base
1527+
targets URL.
1528+
"""
1529+
hasher = sslib_hash.digest(algorithm="sha256")
1530+
hasher.update(target_filepath.encode("utf-8"))
1531+
1532+
# We can't ever need more than 4 bytes (32 bits).
1533+
hash_bytes = hasher.digest()[:4]
1534+
# Right shift hash bytes, so that we only have the leftmost
1535+
# bit_length bits that we care about.
1536+
shift_value = 32 - self.bit_length
1537+
bin_number = int.from_bytes(hash_bytes, byteorder="big") >> shift_value
1538+
return f"{self.name_prefix}-{bin_number}"
1539+
1540+
14381541
class Delegations:
14391542
"""A container object storing information about all delegations.
14401543

0 commit comments

Comments
 (0)