Skip to content

Commit 2ef8546

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1370 from MVrachev/delegation-classes
New API: Add DelegationRole and Delegations classes
2 parents a261d49 + b2cde9b commit 2ef8546

File tree

2 files changed

+212
-36
lines changed

2 files changed

+212
-36
lines changed

tests/test_api.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
Timestamp,
2929
Targets,
3030
Key,
31-
Role
31+
Role,
32+
Delegations,
33+
DelegatedRole,
3234
)
3335

3436
from tuf.api.serialization import (
@@ -328,7 +330,7 @@ def test_key_class(self):
328330
"public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd"
329331
},
330332
"scheme": "ed25519"
331-
},
333+
},
332334
}
333335
for key_dict in keys.values():
334336
# Testing that the workflow of deserializing and serializing
@@ -422,6 +424,76 @@ def test_metadata_root(self):
422424
with self.assertRaises(KeyError):
423425
root.signed.remove_key('root', 'nosuchkey')
424426

427+
def test_delegated_role_class(self):
428+
roles = [
429+
{
430+
"keyids": [
431+
"c8022fa1e9b9cb239a6b362bbdffa9649e61ad2cb699d2e4bc4fdf7930a0e64a"
432+
],
433+
"name": "role1",
434+
"paths": [
435+
"file3.txt"
436+
],
437+
"terminating": False,
438+
"threshold": 1
439+
}
440+
]
441+
for role in roles:
442+
# Testing that the workflow of deserializing and serializing
443+
# a delegation role dictionary doesn't change the content.
444+
key_obj = DelegatedRole.from_dict(role.copy())
445+
self.assertEqual(role, key_obj.to_dict())
446+
447+
# Test creating a DelegatedRole object with both "paths" and
448+
# "path_hash_prefixes" set.
449+
role["path_hash_prefixes"] = "foo"
450+
with self.assertRaises(ValueError):
451+
DelegatedRole.from_dict(role.copy())
452+
453+
# Test creating DelegatedRole only with "path_hash_prefixes"
454+
del role["paths"]
455+
DelegatedRole.from_dict(role.copy())
456+
role["paths"] = "foo"
457+
458+
# Test creating DelegatedRole only with "paths"
459+
del role["path_hash_prefixes"]
460+
DelegatedRole.from_dict(role.copy())
461+
role["path_hash_prefixes"] = "foo"
462+
463+
# Test creating DelegatedRole without "paths" and
464+
# "path_hash_prefixes" set
465+
del role["paths"]
466+
del role["path_hash_prefixes"]
467+
DelegatedRole.from_dict(role)
468+
469+
470+
def test_delegation_class(self):
471+
roles = [
472+
{
473+
"keyids": [
474+
"c8022fa1e9b9cb239a6b362bbdffa9649e61ad2cb699d2e4bc4fdf7930a0e64a"
475+
],
476+
"name": "role1",
477+
"paths": [
478+
"file3.txt"
479+
],
480+
"terminating": False,
481+
"threshold": 1
482+
}
483+
]
484+
keys = {
485+
"59a4df8af818e9ed7abe0764c0b47b4240952aa0d179b5b78346c470ac30278d":{
486+
"keytype": "ed25519",
487+
"keyval": {
488+
"public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd"
489+
},
490+
"scheme": "ed25519"
491+
},
492+
}
493+
delegations_dict = {"keys": keys, "roles": roles}
494+
delegations = Delegations.from_dict(copy.deepcopy(delegations_dict))
495+
self.assertEqual(delegations_dict, delegations.to_dict())
496+
425497

426498
def test_metadata_targets(self):
427499
targets_path = os.path.join(
@@ -452,7 +524,6 @@ def test_metadata_targets(self):
452524
del targets_dict["signed"]["delegations"]
453525
tmp_dict = targets_dict["signed"].copy()
454526
targets_obj = Targets.from_dict(tmp_dict)
455-
tar_d = targets_obj.to_dict()
456527
self.assertEqual(targets_dict["signed"], targets_obj.to_dict())
457528

458529
def setup_dict_with_unrecognized_field(self, file_path, field, value):
@@ -477,6 +548,15 @@ def test_support_for_unrecognized_fields(self):
477548
dict1["signed"]["keys"][keyid]["d"] = "c"
478549
for role_str in dict1["signed"]["roles"].keys():
479550
dict1["signed"]["roles"][role_str]["e"] = "g"
551+
elif metadata == "targets" and dict1["signed"].get("delegations"):
552+
for keyid in dict1["signed"]["delegations"]["keys"].keys():
553+
dict1["signed"]["delegations"]["keys"][keyid]["d"] = "c"
554+
new_roles = []
555+
for role in dict1["signed"]["delegations"]["roles"]:
556+
role["e"] = "g"
557+
new_roles.append(role)
558+
dict1["signed"]["delegations"]["roles"] = new_roles
559+
dict1["signed"]["delegations"]["foo"] = "bar"
480560

481561
temp_copy = copy.deepcopy(dict1)
482562
metadata_obj = Metadata.from_dict(temp_copy)

tuf/api/metadata.py

Lines changed: 129 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,129 @@ def update(
738738
self.meta[metadata_fn]["hashes"] = hashes
739739

740740

741+
class DelegatedRole(Role):
742+
"""A container with information about particular delegated role.
743+
744+
Attributes:
745+
name: A string giving the name of the delegated role.
746+
keyids: A set of strings each of which represents a given key.
747+
threshold: An integer representing the required number of keys for that
748+
particular role.
749+
terminating: A boolean indicating whether subsequent delegations
750+
should be considered.
751+
paths: An optional list of strings, where each string describes
752+
a path that the role is trusted to provide.
753+
path_hash_prefixes: An optional list of HEX_DIGESTs used to succinctly
754+
describe a set of target paths. Only one of the attributes "paths"
755+
and "path_hash_prefixes" is allowed to be set.
756+
unrecognized_fields: Dictionary of all unrecognized fields.
757+
758+
"""
759+
760+
def __init__(
761+
self,
762+
name: str,
763+
keyids: List[str],
764+
threshold: int,
765+
terminating: bool,
766+
paths: Optional[List[str]] = None,
767+
path_hash_prefixes: Optional[List[str]] = None,
768+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
769+
) -> None:
770+
super().__init__(keyids, threshold, unrecognized_fields)
771+
self.name = name
772+
self.terminating = terminating
773+
if paths and path_hash_prefixes:
774+
raise ValueError(
775+
"Only one of the attributes 'paths' and"
776+
"'path_hash_prefixes' can be set!"
777+
)
778+
self.paths = paths
779+
self.path_hash_prefixes = path_hash_prefixes
780+
781+
@classmethod
782+
def from_dict(cls, role_dict: Mapping[str, Any]) -> "Role":
783+
"""Creates DelegatedRole object from its dict representation."""
784+
name = role_dict.pop("name")
785+
keyids = role_dict.pop("keyids")
786+
threshold = role_dict.pop("threshold")
787+
terminating = role_dict.pop("terminating")
788+
paths = role_dict.pop("paths", None)
789+
path_hash_prefixes = role_dict.pop("path_hash_prefixes", None)
790+
# All fields left in the role_dict are unrecognized.
791+
return cls(
792+
name,
793+
keyids,
794+
threshold,
795+
terminating,
796+
paths,
797+
path_hash_prefixes,
798+
role_dict,
799+
)
800+
801+
def to_dict(self) -> Dict[str, Any]:
802+
"""Returns the dict representation of self."""
803+
base_role_dict = super().to_dict()
804+
res_dict = {
805+
"name": self.name,
806+
"terminating": self.terminating,
807+
**base_role_dict,
808+
}
809+
if self.paths:
810+
res_dict["paths"] = self.paths
811+
elif self.path_hash_prefixes:
812+
res_dict["path_hash_prefixes"] = self.path_hash_prefixes
813+
return res_dict
814+
815+
816+
class Delegations:
817+
"""A container object storing information about all delegations.
818+
819+
Attributes:
820+
keys: A dictionary of keyids and key objects containing information
821+
about the corresponding key.
822+
roles: A list of DelegatedRole instances containing information about
823+
all delegated roles.
824+
unrecognized_fields: Dictionary of all unrecognized fields.
825+
826+
"""
827+
828+
def __init__(
829+
self,
830+
keys: Mapping[str, Key],
831+
roles: List[DelegatedRole],
832+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
833+
) -> None:
834+
self.keys = keys
835+
self.roles = roles
836+
self.unrecognized_fields = unrecognized_fields or {}
837+
838+
@classmethod
839+
def from_dict(cls, delegations_dict: Dict[str, Any]) -> "Delegations":
840+
"""Creates Delegations object from its dict representation."""
841+
keys = delegations_dict.pop("keys")
842+
keys_res = {}
843+
for keyid, key_dict in keys.items():
844+
keys_res[keyid] = Key.from_dict(key_dict)
845+
roles = delegations_dict.pop("roles")
846+
roles_res = []
847+
for role_dict in roles:
848+
new_role = DelegatedRole.from_dict(role_dict)
849+
roles_res.append(new_role)
850+
# All fields left in the delegations_dict are unrecognized.
851+
return cls(keys_res, roles_res, delegations_dict)
852+
853+
def to_dict(self) -> Dict[str, Any]:
854+
"""Returns the dict representation of self."""
855+
keys = {keyid: key.to_dict() for keyid, key in self.keys.items()}
856+
roles = [role_obj.to_dict() for role_obj in self.roles]
857+
return {
858+
"keys": keys,
859+
"roles": roles,
860+
**self.unrecognized_fields,
861+
}
862+
863+
741864
class Targets(Signed):
742865
"""A container for the signed part of targets metadata.
743866
@@ -757,38 +880,9 @@ class Targets(Signed):
757880
...
758881
}
759882
760-
delegations: A dictionary that contains a list of delegated target
883+
delegations: An optional object containing a list of delegated target
761884
roles and public key store used to verify their metadata
762-
signatures::
763-
764-
{
765-
'keys' : {
766-
'<KEYID>': {
767-
'keytype': '<KEY TYPE>',
768-
'scheme': '<KEY SCHEME>',
769-
'keyid_hash_algorithms': [
770-
'<HASH ALGO 1>',
771-
'<HASH ALGO 2>'
772-
...
773-
],
774-
'keyval': {
775-
'public': '<PUBLIC KEY HEX REPRESENTATION>'
776-
}
777-
},
778-
...
779-
},
780-
'roles': [
781-
{
782-
'name': '<ROLENAME>',
783-
'keyids': ['<SIGNING KEY KEYID>', ...],
784-
'threshold': <SIGNATURE THRESHOLD>,
785-
'terminating': <TERMINATING BOOLEAN>,
786-
'path_hash_prefixes': ['<HEX DIGEST>', ... ], // or
787-
'paths' : ['PATHPATTERN', ... ],
788-
},
789-
...
790-
]
791-
}
885+
signatures.
792886
793887
"""
794888

@@ -804,7 +898,7 @@ def __init__(
804898
spec_version: str,
805899
expires: datetime,
806900
targets: Dict[str, Any],
807-
delegations: Optional[Dict[str, Any]] = None,
901+
delegations: Optional[Delegations] = None,
808902
unrecognized_fields: Optional[Mapping[str, Any]] = None,
809903
) -> None:
810904
super().__init__(version, spec_version, expires, unrecognized_fields)
@@ -818,6 +912,8 @@ def from_dict(cls, targets_dict: Dict[str, Any]) -> "Targets":
818912
common_args = cls._common_fields_from_dict(targets_dict)
819913
targets = targets_dict.pop("targets")
820914
delegations = targets_dict.pop("delegations", None)
915+
if delegations:
916+
delegations = Delegations.from_dict(delegations)
821917
# All fields left in the targets_dict are unrecognized.
822918
return cls(*common_args, targets, delegations, targets_dict)
823919

@@ -826,7 +922,7 @@ def to_dict(self) -> Dict[str, Any]:
826922
targets_dict = self._common_fields_to_dict()
827923
targets_dict["targets"] = self.targets
828924
if self.delegations:
829-
targets_dict["delegations"] = self.delegations
925+
targets_dict["delegations"] = self.delegations.to_dict()
830926
return targets_dict
831927

832928
# Modification.

0 commit comments

Comments
 (0)