Skip to content

ngclient: review preorder dfs code #1463

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

Merged
merged 8 commits into from
Aug 10, 2021
Merged
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
28 changes: 21 additions & 7 deletions tests/test_metadata_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,9 @@ def test_snapshot_serialization(self, test_case_data: str):
"no path attribute":
'{"keyids": ["keyid"], "name": "a", "terminating": false, \
"path_hash_prefixes": ["h1", "h2"], "threshold": 99}',
"no hash or path prefix":
'{"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3}',
Comment on lines -245 to -246
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't there now be a test that checks that deserialization fails with this input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one with invalid DelegatedRoles: 34f59c9

"unrecognized field":
'{"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3, "foo": "bar"}',
'{"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], \
"terminating": true, "threshold": 3, "foo": "bar"}',
}

@run_sub_tests_with_dataset(valid_delegated_roles)
Expand All @@ -255,12 +254,27 @@ def test_delegated_role_serialization(self, test_case_data: str):
self.assertDictEqual(case_dict, deserialized_role.to_dict())


invalid_delegated_roles: DataSet = {
"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"]}',
}

@run_sub_tests_with_dataset(invalid_delegated_roles)
def test_invalid_delegated_role_serialization(self, test_case_data: str):
case_dict = json.loads(test_case_data)
with self.assertRaises(ValueError):
DelegatedRole.from_dict(copy.copy(case_dict))


valid_delegations: DataSet = {
"all": '{"keys": {"keyid" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}}, \
"roles": [ {"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3} ]}',
"roles": [ {"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], "terminating": true, "threshold": 3} ]}',
"unrecognized field":
'{"keys": {"keyid" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}}, \
"roles": [ {"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3} ], \
"roles": [ {"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], "terminating": true, "threshold": 3} ], \
"foo": "bar"}',
}

Expand Down Expand Up @@ -305,13 +319,13 @@ def test_targetfile_serialization(self, test_case_data: str):
"targets": { "file.txt": {"length": 12, "hashes": {"sha256" : "abc"} } }, \
"delegations": {"keys": {"keyid" : {"keytype": "rsa", \
"scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"} }}, \
"roles": [ {"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3} ]} \
"roles": [ {"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], "terminating": true, "threshold": 3} ]} \
}',
"empty targets": '{"_type": "targets", "spec_version": "1.0.0", "version": 1, "expires": "2030-01-01T00:00:00Z", \
"targets": {}, \
"delegations": {"keys": {"keyid" : {"keytype": "rsa", \
"scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"} }}, \
"roles": [ {"keyids": ["keyid"], "name": "a", "terminating": true, "threshold": 3} ]} \
"roles": [ {"keyids": ["keyid"], "name": "a", "paths": ["fn1", "fn2"], "terminating": true, "threshold": 3} ]} \
}',
"no delegations": '{"_type": "targets", "spec_version": "1.0.0", "version": 1, "expires": "2030-01-01T00:00:00Z", \
"targets": { "file.txt": {"length": 12, "hashes": {"sha256" : "abc"} } } \
Expand Down
46 changes: 39 additions & 7 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

"""
import abc
import fnmatch
import io
import logging
import os
import tempfile
from collections import OrderedDict
from datetime import datetime, timedelta
Expand Down Expand Up @@ -960,12 +962,12 @@ def update(self, rolename: str, role_info: MetaFile) -> None:
class DelegatedRole(Role):
"""A container with information about a delegated role.

A delegation can happen in three ways:
- paths is None and path_hash_prefixes is None: delegates all targets
A delegation can happen in two ways:
- paths is set: delegates targets matching any path pattern in paths
- path_hash_prefixes is set: delegates targets whose target path hash
starts with any of the prefixes in path_hash_prefixes
paths and path_hash_prefixes are mutually exclusive: both cannot be set.
paths and path_hash_prefixes are mutually exclusive: both cannot be set,
at least one of them must be set.

Attributes:
name: A string giving the name of the delegated role.
Expand All @@ -990,10 +992,11 @@ def __init__(
self.name = name
self.terminating = terminating
if paths is not None and path_hash_prefixes is not None:
raise ValueError(
"Only one of the attributes 'paths' and"
"'path_hash_prefixes' can be set!"
)
raise ValueError("Either paths or path_hash_prefixes can be set")

if paths is None and path_hash_prefixes is None:
raise ValueError("One of paths or path_hash_prefixes must be set")

self.paths = paths
self.path_hash_prefixes = path_hash_prefixes

Expand Down Expand Up @@ -1031,6 +1034,35 @@ def to_dict(self) -> Dict[str, Any]:
res_dict["path_hash_prefixes"] = self.path_hash_prefixes
return res_dict

def is_delegated_path(self, target_filepath: str) -> bool:
"""Determines whether the given 'target_filepath' is in one of
the paths that DelegatedRole is trusted to provide"""

if self.path_hash_prefixes is not None:
# Calculate the hash of the filepath
# to determine in which bin to find the target.
digest_object = sslib_hash.digest(algorithm="sha256")
digest_object.update(target_filepath.encode("utf-8"))
target_filepath_hash = digest_object.hexdigest()

for path_hash_prefix in self.path_hash_prefixes:
if target_filepath_hash.startswith(path_hash_prefix):
return True

elif self.paths is not None:
for pathpattern in self.paths:
# A delegated role path may be an explicit path or glob
# pattern (Unix shell-style wildcards). Explicit filepaths
# are also considered matches. Make sure to strip any leading
# path separators so that a match is made.
# Example: "foo.tgz" should match with "/*.tgz".
if fnmatch.fnmatch(
target_filepath.lstrip(os.sep), pathpattern.lstrip(os.sep)
):
return True

return False


class Delegations:
"""A container object storing information about all delegations.
Expand Down
Loading