Skip to content
Open
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
50 changes: 49 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from copy import copy, deepcopy
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar

from securesystemslib import exceptions as sslib_exceptions
from securesystemslib.signer import (
Expand Down Expand Up @@ -48,6 +48,9 @@
from tuf.api.serialization import DeserializationError, SerializationError
from tuf.api.serialization.json import JSONSerializer

if TYPE_CHECKING:
from collections.abc import Callable

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -1111,6 +1114,51 @@ def test_role_and_delegated_role_hash(self) -> None:
role_set = {dr, dr2}
self.assertEqual(len(role_set), 1)

def test_metadata_hash(self) -> None:
# Each of these __hash__ implementations passed a raw dict
# (unrecognized_fields, keys, roles, meta, targets, hashes,
# signatures) to hash(), which raises TypeError.
expires = datetime(2030, 1, 1, tzinfo=timezone.utc)
key = SSlibKey("kid1", "ed25519", "ed25519", {"public": "aa"})
delegated_role = DelegatedRole("r", ["kid1"], 1, False, ["*"], None)

factories: dict[str, Callable[[], object]] = {
"MetaFile": lambda: MetaFile(1, 10, {"sha256": "ab"}),
"TargetFile": lambda: TargetFile(10, {"sha256": "ab"}, "p"),
"Delegations": lambda: Delegations(
{"kid1": key}, {"r": delegated_role}
),
"Root": lambda: Root(expires=expires),
"Timestamp": lambda: Timestamp(expires=expires),
"Snapshot": lambda: Snapshot(expires=expires),
"Targets": lambda: Targets(expires=expires),
"Metadata": lambda: Metadata(Snapshot(expires=expires)),
}

for name, factory in factories.items():
with self.subTest(name):
obj, equal_obj = factory(), factory()

self.assertIsInstance(hash(obj), int)

# equal objects must produce equal hashes (Python data model)
self.assertEqual(obj, equal_obj)
self.assertEqual(hash(obj), hash(equal_obj))

# the object must work as a set member / dict key
self.assertEqual(len({obj, equal_obj}), 1)

def test_metadata_hash_ignores_unrecognized_fields(self) -> None:
# unrecognized_fields holds arbitrary (possibly nested) JSON, so it is
# left out of __hash__. Objects differing only in unrecognized_fields
# are unequal but may share a hash, which the data model allows.
expires = datetime(2030, 1, 1, tzinfo=timezone.utc)
plain = Snapshot(expires=expires)
extra = Snapshot(expires=expires, unrecognized_fields={"a": ["b"]})

self.assertNotEqual(plain, extra)
self.assertIsInstance(hash(extra), int)

def test_is_delegated_role_in_succinct_roles(self) -> None:
succinct_roles = SuccinctRoles([], 1, 5, "bin")
false_role_name_examples = [
Expand Down
24 changes: 9 additions & 15 deletions tuf/api/_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ def __hash__(self) -> int:
self.version,
self.spec_version,
self.expires,
self.unrecognized_fields,
)
)

Expand Down Expand Up @@ -565,10 +564,9 @@ def __hash__(self) -> int:
return hash(
(
super().__hash__(),
self.keys,
self.roles,
tuple(sorted(self.keys)),
tuple(sorted(self.roles)),
self.consistent_snapshot,
self.unrecognized_fields,
)
)

Expand Down Expand Up @@ -848,9 +846,7 @@ def __eq__(self, other: object) -> bool:
)

def __hash__(self) -> int:
return hash(
(self.version, self.length, self.hashes, self.unrecognized_fields)
)
return hash((self.version, self.length))

@classmethod
def from_dict(cls, meta_dict: dict[str, Any]) -> MetaFile:
Expand Down Expand Up @@ -1031,7 +1027,7 @@ def __eq__(self, other: object) -> bool:
return super().__eq__(other) and self.meta == other.meta

def __hash__(self) -> int:
return hash((super().__hash__(), self.meta))
return hash((super().__hash__(), len(self.meta)))

@classmethod
def from_dict(cls, signed_dict: dict[str, Any]) -> Snapshot:
Expand Down Expand Up @@ -1463,10 +1459,10 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash(
(
self.keys,
self.roles,
tuple(sorted(self.keys)),
# Order of the delegated roles matters (see __eq__)
tuple(self.roles) if self.roles is not None else None,
self.succinct_roles,
self.unrecognized_fields,
)
)

Expand Down Expand Up @@ -1592,9 +1588,7 @@ def __eq__(self, other: object) -> bool:
)

def __hash__(self) -> int:
return hash(
(self.length, self.hashes, self.path, self.unrecognized_fields)
)
return hash((self.length, self.path))

@classmethod
def from_dict(cls, target_dict: dict[str, Any], path: str) -> TargetFile:
Expand Down Expand Up @@ -1740,7 +1734,7 @@ def __eq__(self, other: object) -> bool:
)

def __hash__(self) -> int:
return hash((super().__hash__(), self.targets, self.delegations))
return hash((super().__hash__(), len(self.targets), self.delegations))

@classmethod
def from_dict(cls, signed_dict: dict[str, Any]) -> Targets:
Expand Down
2 changes: 1 addition & 1 deletion tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def __eq__(self, other: object) -> bool:
)

def __hash__(self) -> int:
return hash((self.signatures, self.signed, self.unrecognized_fields))
return hash((tuple(self.signatures), self.signed))

@property
def signed_bytes(self) -> bytes:
Expand Down
Loading