Skip to content

Commit 15cd42c

Browse files
author
Martin Vrachev
committed
Delegations: add support for succinct_roles
This commit contains 2 API changes in "Delegations" class from tuf/api/metadata.py: 1. roles argment is made optional 2. unrecognized_fields argument becomes the 4-th rather than the 3-rd as it used to be In this commit, I add support for succinct_roles roles inside Delegations class. This change is related to TAP 15 proposal. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent f80b4ca commit 15cd42c

7 files changed

Lines changed: 141 additions & 48 deletions

File tree

examples/repo_example/hashed_bin_delegation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def find_hash_bin(path: str) -> str:
162162
# 10-17 10 11 12 13 14 15 16 17
163163
# ... ...
164164
# f8-ff f8 f9 fa fb fc fd fe ff
165+
assert roles["bins"].signed.delegations.roles is not None
165166
for bin_n_name, bin_n_hash_prefixes in generate_hash_bins():
166167
# Update delegating targets role (bins) with delegation details for each
167168
# delegated targets role (bin_n).

tests/repository_simulator.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,17 @@ def add_delegation(
354354
else:
355355
delegator = self.md_delegates[delegator_name].signed
356356

357+
if (
358+
delegator.delegations is not None
359+
and delegator.delegations.succinct_roles is not None
360+
):
361+
raise ValueError("Can't add a role when succinct_roles is used")
362+
357363
# Create delegation
358364
if delegator.delegations is None:
359-
delegator.delegations = Delegations({}, {})
365+
delegator.delegations = Delegations({}, roles={})
366+
367+
assert delegator.delegations.roles is not None
360368
# put delegation last by default
361369
delegator.delegations.roles[role.name] = role
362370

tests/test_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ def test_targets_key_api(self) -> None:
495495
}
496496
)
497497
assert isinstance(targets.delegations, Delegations)
498+
assert isinstance(targets.delegations.roles, Dict)
498499
targets.delegations.roles["role2"] = delegated_role
499500

500501
key_dict = {

tests/test_metadata_eq_.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def test_delegations_eq_roles_reversed_order(self) -> None:
169169
# Create a second delegations obj with reversed roles order
170170
delegations_2 = copy.deepcopy(delegations)
171171
# In python3.7 we need to cast to a list and then reverse.
172+
assert isinstance(delegations.roles, dict)
172173
delegations_2.roles = dict(reversed(list(delegations.roles.items())))
173174

174175
# Both objects are not the equal because of delegated roles order.

tests/test_metadata_serialization.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,13 @@ def test_invalid_succinct_roles_serialization(self, test_data: str) -> None:
491491
{"keyids": ["keyid1"], "name": "b", "terminating": true, "paths": ["fn1"], "threshold": 3}, \
492492
{"keyids": ["keyid2"], "name": "root", "terminating": true, "paths": ["fn2"], "threshold": 4} ] \
493493
}',
494+
"roles and succinct_roles set": '{"keys": { \
495+
"keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \
496+
"keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \
497+
"roles": [ \
498+
{"keyids": ["keyid"], "name": "a", "terminating": true, "paths": ["fn1"], "threshold": 3}, \
499+
{"keyids": ["keyid2"], "name": "b", "terminating": true, "paths": ["fn2"], "threshold": 4} ], \
500+
"succinct_roles": {"keyids": ["keyid"], "threshold": 1, "bit_length": 8, "name_prefix": "foo"}}',
494501
}
495502

496503
@utils.run_sub_tests_with_dataset(invalid_delegations)
@@ -502,13 +509,17 @@ def test_invalid_delegation_serialization(
502509
Delegations.from_dict(case_dict)
503510

504511
valid_delegations: utils.DataSet = {
505-
"all": '{"keys": { \
512+
"with roles": '{"keys": { \
506513
"keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \
507514
"keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \
508515
"roles": [ \
509516
{"keyids": ["keyid"], "name": "a", "terminating": true, "paths": ["fn1"], "threshold": 3}, \
510517
{"keyids": ["keyid2"], "name": "b", "terminating": true, "paths": ["fn2"], "threshold": 4} ] \
511518
}',
519+
"with succinct_roles": '{"keys": { \
520+
"keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \
521+
"keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \
522+
"succinct_roles": {"keyids": ["keyid"], "threshold": 1, "bit_length": 8, "name_prefix": "foo"}}',
512523
"unrecognized field": '{"keys": {"keyid" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}}, \
513524
"roles": [ {"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], "terminating": true, "threshold": 3} ], \
514525
"foo": "bar"}',

tuf/api/metadata.py

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def verify_delegate(
413413
"""
414414

415415
# Find the keys and role in delegator metadata
416-
role = None
416+
role: Optional[Role] = None
417417
if isinstance(self.signed, Root):
418418
keys = self.signed.keys
419419
role = self.signed.roles.get(delegated_role)
@@ -422,7 +422,13 @@ def verify_delegate(
422422
raise ValueError(f"No delegation found for {delegated_role}")
423423

424424
keys = self.signed.delegations.keys
425-
role = self.signed.delegations.roles.get(delegated_role)
425+
if self.signed.delegations.roles is not None:
426+
role = self.signed.delegations.roles.get(delegated_role)
427+
elif self.signed.delegations.succinct_roles is not None:
428+
if self.signed.delegations.succinct_roles.is_delegated_role(
429+
delegated_role
430+
):
431+
role = self.signed.delegations.succinct_roles
426432
else:
427433
raise TypeError("Call is valid only on delegator metadata")
428434

@@ -1592,28 +1598,42 @@ class Delegations:
15921598
defines which keys are required to sign the metadata for a specific
15931599
role. The roles order also defines the order that role delegations
15941600
are considered during target searches.
1601+
succinct_roles: Contains succinct information about hash bin
1602+
delegations. Note that succinct roles is not a TUF specification
1603+
feature yet and setting `succinct_roles` to a value makes the
1604+
resulting metadata non-compliant. The metadata will not be accepted
1605+
as valid by specification compliant clients such as those built with
1606+
python-tuf <= 1.1.0. For more information see: https://github.com/theupdateframework/taps/blob/master/tap15.md
15951607
unrecognized_fields: Dictionary of all attributes that are not managed
15961608
by TUF Metadata API
15971609
1610+
Exactly one of ``roles`` and ``succinct_roles`` must be set.
1611+
15981612
Raises:
15991613
ValueError: Invalid arguments.
16001614
"""
16011615

16021616
def __init__(
16031617
self,
16041618
keys: Dict[str, Key],
1605-
roles: Dict[str, DelegatedRole],
1619+
roles: Optional[Dict[str, DelegatedRole]] = None,
1620+
succinct_roles: Optional[SuccinctRoles] = None,
16061621
unrecognized_fields: Optional[Dict[str, Any]] = None,
16071622
):
16081623
self.keys = keys
1609-
1610-
for role in roles:
1611-
if not role or role in TOP_LEVEL_ROLE_NAMES:
1612-
raise ValueError(
1613-
"Delegated roles cannot be empty or use top-level role names"
1614-
)
1624+
if sum(1 for v in [roles, succinct_roles] if v is not None) != 1:
1625+
raise ValueError("One of roles and succinct_roles must be set")
1626+
1627+
if roles is not None:
1628+
for role in roles:
1629+
if not role or role in TOP_LEVEL_ROLE_NAMES:
1630+
raise ValueError(
1631+
"Delegated roles cannot be empty or use top-level "
1632+
"role names"
1633+
)
16151634

16161635
self.roles = roles
1636+
self.succinct_roles = succinct_roles
16171637
if unrecognized_fields is None:
16181638
unrecognized_fields = {}
16191639

@@ -1623,14 +1643,22 @@ def __eq__(self, other: Any) -> bool:
16231643
if not isinstance(other, Delegations):
16241644
return False
16251645

1626-
return (
1646+
all_attributes_check = (
16271647
self.keys == other.keys
1628-
# Order of the delegated roles matters (see issue #1788).
1629-
and list(self.roles.items()) == list(other.roles.items())
16301648
and self.roles == other.roles
1649+
and self.succinct_roles == other.succinct_roles
16311650
and self.unrecognized_fields == other.unrecognized_fields
16321651
)
16331652

1653+
if self.roles is not None and other.roles is not None:
1654+
all_attributes_check = (
1655+
all_attributes_check
1656+
# Order of the delegated roles matters (see issue #1788).
1657+
and list(self.roles.items()) == list(other.roles.items())
1658+
)
1659+
1660+
return all_attributes_check
1661+
16341662
@classmethod
16351663
def from_dict(cls, delegations_dict: Dict[str, Any]) -> "Delegations":
16361664
"""Creates ``Delegations`` object from its json/dict representation.
@@ -1642,25 +1670,59 @@ def from_dict(cls, delegations_dict: Dict[str, Any]) -> "Delegations":
16421670
keys_res = {}
16431671
for keyid, key_dict in keys.items():
16441672
keys_res[keyid] = Key.from_dict(keyid, key_dict)
1645-
roles = delegations_dict.pop("roles")
1646-
roles_res: Dict[str, DelegatedRole] = {}
1647-
for role_dict in roles:
1648-
new_role = DelegatedRole.from_dict(role_dict)
1649-
if new_role.name in roles_res:
1650-
raise ValueError(f"Duplicate role {new_role.name}")
1651-
roles_res[new_role.name] = new_role
1673+
roles = delegations_dict.pop("roles", None)
1674+
roles_res: Optional[Dict[str, DelegatedRole]] = None
1675+
1676+
if roles is not None:
1677+
roles_res = {}
1678+
for role_dict in roles:
1679+
new_role = DelegatedRole.from_dict(role_dict)
1680+
if new_role.name in roles_res:
1681+
raise ValueError(f"Duplicate role {new_role.name}")
1682+
roles_res[new_role.name] = new_role
1683+
1684+
succinct_roles_dict = delegations_dict.pop("succinct_roles", None)
1685+
succinct_roles_info = None
1686+
if succinct_roles_dict is not None:
1687+
succinct_roles_info = SuccinctRoles.from_dict(succinct_roles_dict)
1688+
16521689
# All fields left in the delegations_dict are unrecognized.
1653-
return cls(keys_res, roles_res, delegations_dict)
1690+
return cls(keys_res, roles_res, succinct_roles_info, delegations_dict)
16541691

16551692
def to_dict(self) -> Dict[str, Any]:
16561693
"""Returns the dict representation of self."""
16571694
keys = {keyid: key.to_dict() for keyid, key in self.keys.items()}
1658-
roles = [role_obj.to_dict() for role_obj in self.roles.values()]
1659-
return {
1695+
res_dict: Dict[str, Any] = {
16601696
"keys": keys,
1661-
"roles": roles,
16621697
**self.unrecognized_fields,
16631698
}
1699+
if self.roles is not None:
1700+
roles = [role_obj.to_dict() for role_obj in self.roles.values()]
1701+
res_dict["roles"] = roles
1702+
elif self.succinct_roles is not None:
1703+
res_dict["succinct_roles"] = self.succinct_roles.to_dict()
1704+
1705+
return res_dict
1706+
1707+
def get_roles_for_target(
1708+
self, target_filepath: str
1709+
) -> Iterator[Tuple[str, bool]]:
1710+
"""Given ``target_filepath`` get names and terminating status of all
1711+
delegated roles who are responsible for it.
1712+
1713+
Args:
1714+
target_filepath: URL path to a target file, relative to a base
1715+
targets URL.
1716+
"""
1717+
if self.roles is not None:
1718+
for role in self.roles.values():
1719+
if role.is_delegated_path(target_filepath):
1720+
yield role.name, role.terminating
1721+
1722+
elif self.succinct_roles is not None:
1723+
# We consider all succinct_roles as terminating.
1724+
# For more information read TAP 15.
1725+
yield self.succinct_roles.get_role_for_target(target_filepath), True
16641726

16651727

16661728
class TargetFile(BaseFile):
@@ -1921,11 +1983,15 @@ def add_key(self, role: str, key: Key) -> None:
19211983
ValueError: If there are no delegated roles or if ``role`` is not
19221984
delegated by this Target.
19231985
"""
1924-
if self.delegations is None or role not in self.delegations.roles:
1986+
if self.delegations is None:
19251987
raise ValueError(f"Delegated role {role} doesn't exist")
1926-
if key.keyid not in self.delegations.roles[role].keyids:
1927-
self.delegations.roles[role].keyids.append(key.keyid)
1928-
self.delegations.keys[key.keyid] = key
1988+
1989+
if self.delegations.roles is not None:
1990+
if role not in self.delegations.roles:
1991+
raise ValueError(f"Delegated role {role} doesn't exist")
1992+
if key.keyid not in self.delegations.roles[role].keyids:
1993+
self.delegations.roles[role].keyids.append(key.keyid)
1994+
self.delegations.keys[key.keyid] = key
19291995

19301996
def remove_key(self, role: str, keyid: str) -> None:
19311997
"""Removes key from delegated role ``role`` and updates the delegations
@@ -1939,13 +2005,18 @@ def remove_key(self, role: str, keyid: str) -> None:
19392005
ValueError: If there are no delegated roles or if ``role`` is not
19402006
delegated by this ``Target`` or if key is not used by ``role``.
19412007
"""
1942-
if self.delegations is None or role not in self.delegations.roles:
2008+
if self.delegations is None:
19432009
raise ValueError(f"Delegated role {role} doesn't exist")
1944-
if keyid not in self.delegations.roles[role].keyids:
1945-
raise ValueError(f"Key with id {keyid} is not used by {role}")
1946-
self.delegations.roles[role].keyids.remove(keyid)
1947-
for keyinfo in self.delegations.roles.values():
1948-
if keyid in keyinfo.keyids:
1949-
return
19502010

1951-
del self.delegations.keys[keyid]
2011+
if self.delegations.roles is not None:
2012+
if role not in self.delegations.roles:
2013+
raise ValueError(f"Delegated role {role} doesn't exist")
2014+
if keyid not in self.delegations.roles[role].keyids:
2015+
raise ValueError(f"Key with id {keyid} is not used by {role}")
2016+
2017+
self.delegations.roles[role].keyids.remove(keyid)
2018+
for keyinfo in self.delegations.roles.values():
2019+
if keyid in keyinfo.keyids:
2020+
return
2021+
2022+
del self.delegations.keys[keyid]

tuf/ngclient/updater.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -438,17 +438,17 @@ def _preorder_depth_first_walk(
438438
child_roles_to_visit = []
439439
# NOTE: This may be a slow operation if there are many
440440
# delegated roles.
441-
for child_role in targets.delegations.roles.values():
442-
if child_role.is_delegated_path(target_filepath):
443-
logger.debug("Adding child role %s", child_role.name)
444-
445-
child_roles_to_visit.append(
446-
(child_role.name, role_name)
447-
)
448-
if child_role.terminating:
449-
logger.debug("Not backtracking to other roles")
450-
delegations_to_visit = []
451-
break
441+
for (
442+
child_name,
443+
terminating,
444+
) in targets.delegations.get_roles_for_target(target_filepath):
445+
446+
logger.debug("Adding child role %s", child_name)
447+
child_roles_to_visit.append((child_name, role_name))
448+
if terminating:
449+
logger.debug("Not backtracking to other roles")
450+
delegations_to_visit = []
451+
break
452452
# Push 'child_roles_to_visit' in reverse order of appearance
453453
# onto 'delegations_to_visit'. Roles are popped from the end of
454454
# the list.

0 commit comments

Comments
 (0)