mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
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>
This commit is contained in:
parent
b8360a2f69
commit
9259ced68a
3 changed files with 137 additions and 0 deletions
|
|
@ -23,6 +23,7 @@
|
|||
Metadata,
|
||||
MetaFile,
|
||||
Role,
|
||||
SuccinctRoles,
|
||||
TargetFile,
|
||||
)
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ def setUpClass(cls) -> None:
|
|||
cls.objects["Role"] = Role(["keyid1", "keyid2"], 3)
|
||||
cls.objects["MetaFile"] = MetaFile(1, 12, {"sha256": "abc"})
|
||||
cls.objects["DelegatedRole"] = DelegatedRole("a", [], 1, False, ["d"])
|
||||
cls.objects["SuccinctRoles"] = SuccinctRoles(["keyid"], 1, 8, "foo")
|
||||
cls.objects["Delegations"] = Delegations(
|
||||
{"keyid": cls.objects["Key"]}, {"a": cls.objects["DelegatedRole"]}
|
||||
)
|
||||
|
|
@ -79,6 +81,7 @@ def setUpClass(cls) -> None:
|
|||
"paths": [""],
|
||||
"path_hash_prefixes": [""],
|
||||
},
|
||||
"SuccinctRoles": {"bit_length": 0, "name_prefix": ""},
|
||||
"Delegations": {"keys": {}, "roles": {}},
|
||||
"TargetFile": {"length": 0, "hashes": {}, "path": ""},
|
||||
"Targets": {"targets": {}, "delegations": []},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
Role,
|
||||
Root,
|
||||
Snapshot,
|
||||
SuccinctRoles,
|
||||
TargetFile,
|
||||
Targets,
|
||||
Timestamp,
|
||||
|
|
@ -408,6 +409,36 @@ def test_invalid_delegated_role_serialization(
|
|||
with self.assertRaises(ValueError):
|
||||
DelegatedRole.from_dict(case_dict)
|
||||
|
||||
valid_succinct_roles: utils.DataSet = {
|
||||
# SuccinctRoles inherits Role and some use cases can be found in the valid_roles.
|
||||
"standard succinct_roles information": '{"keyids": ["keyid"], "threshold": 1, \
|
||||
"bit_length": 8, "name_prefix": "foo"}',
|
||||
"succinct_roles with unrecognized fields": '{"keyids": ["keyid"], "threshold": 1, \
|
||||
"bit_length": 8, "name_prefix": "foo", "foo": "bar"}',
|
||||
}
|
||||
|
||||
@utils.run_sub_tests_with_dataset(valid_succinct_roles)
|
||||
def test_succinct_roles_serialization(self, test_case_data: str) -> None:
|
||||
case_dict = json.loads(test_case_data)
|
||||
succinct_roles = SuccinctRoles.from_dict(copy.copy(case_dict))
|
||||
self.assertDictEqual(case_dict, succinct_roles.to_dict())
|
||||
|
||||
invalid_succinct_roles: utils.DataSet = {
|
||||
# SuccinctRoles inherits Role and some use cases can be found in the invalid_roles.
|
||||
"missing bit_length from succinct_roles": '{"keyids": ["keyid"], "threshold": 1, "name_prefix": "foo"}',
|
||||
"missing name_prefix from succinct_roles": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 8}',
|
||||
"succinct_roles with invalid bit_length type": '{"keyids": ["keyid"], "threshold": 1, "bit_length": "a", "name_prefix": "foo"}',
|
||||
"succinct_roles with invalid name_prefix type": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 8, "name_prefix": 1}',
|
||||
"succinct_roles with high bit_length value": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 50, "name_prefix": "foo"}',
|
||||
"succinct_roles with low bit_length value": '{"keyids": ["keyid"], "threshold": 1, "bit_length": 0, "name_prefix": "foo"}',
|
||||
}
|
||||
|
||||
@utils.run_sub_tests_with_dataset(invalid_succinct_roles)
|
||||
def test_invalid_succinct_roles_serialization(self, test_data: str) -> None:
|
||||
case_dict = json.loads(test_data)
|
||||
with self.assertRaises((ValueError, KeyError, TypeError)):
|
||||
SuccinctRoles.from_dict(case_dict)
|
||||
|
||||
invalid_delegations: utils.DataSet = {
|
||||
"empty delegations": "{}",
|
||||
"missing keys": '{ "roles": [ \
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,109 @@ def is_delegated_path(self, target_filepath: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
class SuccinctRoles(Role):
|
||||
"""Succinctly defines a hash bin delegation graph.
|
||||
|
||||
A ``SuccinctRoles`` object describes a delegation graph that covers all
|
||||
targets, distributing them uniformly over the delegated roles (i.e. bins)
|
||||
in the graph.
|
||||
|
||||
The total number of bins is 2 to the power of the passed ``bit_length``.
|
||||
Targets are assigned to bins by casting the left-most ``bit_length`` of
|
||||
bits of the file path hash digest to int, using it as bin index between 0
|
||||
and ``2**bit_length - 1``.
|
||||
|
||||
Bin names are the concatenation of the passed ``name_prefix`` and a hex
|
||||
representation of the bin index between separated by a hyphen.
|
||||
|
||||
The passed ``keyids`` and ``threshold`` is used for each bin, and each bin
|
||||
is 'terminating'.
|
||||
|
||||
For details: https://github.com/theupdateframework/taps/blob/master/tap15.md
|
||||
|
||||
Args:
|
||||
keyids: Signing key identifiers for any bin metadata.
|
||||
threshold: Number of keys required to sign any bin metadata.
|
||||
bit_length: Number of bits between 1 and 32.
|
||||
name_prefix: Prefix of all bin names.
|
||||
unrecognized_fields: Dictionary of all attributes that are not managed
|
||||
by TUF Metadata API.
|
||||
|
||||
Raises:
|
||||
ValueError, TypeError, AttributeError: Invalid arguments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
keyids: List[str],
|
||||
threshold: int,
|
||||
bit_length: int,
|
||||
name_prefix: str,
|
||||
unrecognized_fields: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
super().__init__(keyids, threshold, unrecognized_fields)
|
||||
|
||||
if bit_length <= 0 or bit_length > 32:
|
||||
raise ValueError("bit_length must be between 1 and 32")
|
||||
if not isinstance(name_prefix, str):
|
||||
raise ValueError("name_prefix must be a string")
|
||||
|
||||
self.bit_length = bit_length
|
||||
self.name_prefix = name_prefix
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, SuccinctRoles):
|
||||
return False
|
||||
|
||||
return (
|
||||
super().__eq__(other)
|
||||
and self.bit_length == other.bit_length
|
||||
and self.name_prefix == other.name_prefix
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, role_dict: Dict[str, Any]) -> "SuccinctRoles":
|
||||
"""Creates ``SuccinctRoles`` object from its json/dict representation.
|
||||
|
||||
Raises:
|
||||
ValueError, KeyError, AttributeError, TypeError: Invalid arguments.
|
||||
"""
|
||||
keyids = role_dict.pop("keyids")
|
||||
threshold = role_dict.pop("threshold")
|
||||
bit_length = role_dict.pop("bit_length")
|
||||
name_prefix = role_dict.pop("name_prefix")
|
||||
# All fields left in the role_dict are unrecognized.
|
||||
return cls(keyids, threshold, bit_length, name_prefix, role_dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Returns the dict representation of self."""
|
||||
base_role_dict = super().to_dict()
|
||||
return {
|
||||
"bit_length": self.bit_length,
|
||||
"name_prefix": self.name_prefix,
|
||||
**base_role_dict,
|
||||
}
|
||||
|
||||
def get_role_for_target(self, target_filepath: str) -> str:
|
||||
"""Calculates the name of the delegated role responsible for
|
||||
``target_filepath``.
|
||||
|
||||
Args:
|
||||
target_filepath: URL path to a target file, relative to a base
|
||||
targets URL.
|
||||
"""
|
||||
hasher = sslib_hash.digest(algorithm="sha256")
|
||||
hasher.update(target_filepath.encode("utf-8"))
|
||||
|
||||
# We can't ever need more than 4 bytes (32 bits).
|
||||
hash_bytes = hasher.digest()[:4]
|
||||
# Right shift hash bytes, so that we only have the leftmost
|
||||
# bit_length bits that we care about.
|
||||
shift_value = 32 - self.bit_length
|
||||
bin_number = int.from_bytes(hash_bytes, byteorder="big") >> shift_value
|
||||
return f"{self.name_prefix}-{bin_number}"
|
||||
|
||||
|
||||
class Delegations:
|
||||
"""A container object storing information about all delegations.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue