Skip to content

Commit fb713d8

Browse files
committed
Take order into account for certain cases
After we have dropped OrderedDict in e3b267e we are relying on python3.7+ default behavior to preserve the insertion order, but there is one caveat. When comparing dictionaries the order is still irrelevant compared to OrderedDict. For example: >>> OrderedDict([(1,1), (2,2)]) == OrderedDict([(2,2), (1,1)]) False >>> dict([(1,1), (2,2)]) == dict([(2,2), (1,1)]) True There are two special attributes, defined in the specification, where the order makes a difference when comparing two objects: - Metadata.signatures - Targets.delegations.roles. We want to make sure that the order in those two cases makes a difference when comparing two objects and that's why those changes are required inside two __eq__ implementations. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 30ca1fd commit fb713d8

File tree

2 files changed

+114
-6
lines changed

2 files changed

+114
-6
lines changed

tests/test_metadata_eq_.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import os
1111
import sys
1212
import unittest
13-
from typing import Any, ClassVar, Dict
13+
from typing import Any, ClassVar, Dict, List
14+
15+
from securesystemslib.signer import SSlibSigner
1416

1517
from tests import utils
18+
from tests.repository_simulator import RepositorySimulator
1619
from tuf.api.metadata import (
1720
TOP_LEVEL_ROLE_NAMES,
1821
DelegatedRole,
@@ -60,6 +63,35 @@ def test_metadata_eq_(self, test_case_data: Any) -> None:
6063
setattr(md_2, self.case_name, test_case_data)
6164
self.assertNotEqual(md, md_2)
6265

66+
def test_md_eq_signatures_reversed_order(self) -> None:
67+
# Test comparing objects with same signatures but different order.
68+
69+
# Remove all signatures and create new ones.
70+
md = Metadata.from_bytes(self.metadata["snapshot"])
71+
md.signatures = {}
72+
md_2 = copy.deepcopy(md)
73+
signatures: List[SSlibSigner] = []
74+
# Assign the new signatures to the md object in the straight order.
75+
for i in ["0", "1", "2"]:
76+
_, signer = RepositorySimulator.create_key()
77+
signature = signer.sign(md.to_bytes())
78+
signatures.append(signature)
79+
md.signatures[i] = signature
80+
81+
# Assign the new signatures in a reverse order for the md_2 object.
82+
for j in [2, 1, 0]:
83+
md_2.signatures[str(j)] = signatures[j]
84+
85+
# Assert that both objects are not the same because of signatures order.
86+
self.assertNotEqual(md, md_2)
87+
88+
# but if we fix the signatures order they will be equal
89+
md_2.signatures = {}
90+
for j in [0, 1, 2]:
91+
md_2.signatures[str(j)] = signatures[j]
92+
93+
self.assertEqual(md, md_2)
94+
6395
def test_md_eq_special_signatures_tests(self) -> None:
6496
# Test that metadata objects with different signatures are not equal.
6597
md = Metadata.from_bytes(self.metadata["snapshot"])
@@ -284,6 +316,64 @@ def test_delegations_eq_(self, test_case_data: Any) -> None:
284316
setattr(delegations_2, self.case_name, test_case_data)
285317
self.assertNotEqual(delegations, delegations_2)
286318

319+
def test_delegations_eq_roles_reversed_order(self) -> None:
320+
# Test comparing objects with same delegated roles but different order.
321+
role_one_dict = {
322+
"keyids": ["keyid1"],
323+
"name": "a",
324+
"terminating": False,
325+
"paths": ["fn1"],
326+
"threshold": 1,
327+
}
328+
role_two_dict = {
329+
"keyids": ["keyid2"],
330+
"name": "b",
331+
"terminating": True,
332+
"paths": ["fn2"],
333+
"threshold": 4,
334+
}
335+
role_one = DelegatedRole.from_dict(role_one_dict)
336+
role_two = DelegatedRole.from_dict(role_two_dict)
337+
338+
delegations_dict = {
339+
"keys": {
340+
"keyid2": {
341+
"keytype": "ed25519",
342+
"scheme": "ed25519",
343+
"keyval": {"public": "bar"},
344+
}
345+
},
346+
"roles": [],
347+
}
348+
delegations = Delegations.from_dict(delegations_dict)
349+
delegations.roles["a"] = role_one
350+
delegations.roles["b"] = role_two
351+
352+
# Create a second delegations obj with reversed roles order
353+
delegations_dict_2 = {
354+
"keys": {
355+
"keyid2": {
356+
"keytype": "ed25519",
357+
"scheme": "ed25519",
358+
"keyval": {"public": "bar"},
359+
}
360+
},
361+
"roles": [],
362+
}
363+
delegations_2 = Delegations.from_dict(delegations_dict_2)
364+
delegations.roles["b"] = copy.deepcopy(role_two)
365+
delegations.roles["a"] = copy.deepcopy(role_one)
366+
367+
# Both objects are not the equal because of delegated roles order.
368+
self.assertNotEqual(delegations, delegations_2)
369+
370+
# but if we fix the delegated roles order they will be equal
371+
delegations_2.roles = {}
372+
delegations_2.roles["a"] = copy.deepcopy(role_one)
373+
delegations_2.roles["b"] = copy.deepcopy(role_two)
374+
375+
self.assertEqual(delegations, delegations_2)
376+
287377
# Common attributes between TargetFile and MetaFile doesn't need testing.
288378
targetfile_attributes_modifications: utils.DataSet = {
289379
"path": "",

tuf/api/metadata.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,14 @@ def __eq__(self, other: Any) -> bool:
130130
if self.signatures.keys() != other.signatures.keys():
131131
return False
132132

133-
for sig_info in self.signatures.values():
134-
sig = sig_info.signature
135-
keyid = sig_info.keyid
136-
if sig != other.signatures[keyid].signature:
133+
# Iterate over self.signatures and other.signatures at the same time
134+
# as the signatures in the file format are ordered.
135+
keyids = zip(self.signatures.keys(), other.signatures.keys())
136+
for self_keyid, other_keyid in keyids:
137+
self_sig = self.signatures[self_keyid].signature
138+
other_sig = other.signatures[other_keyid].signature
139+
140+
if (self_keyid != other_keyid) or (self_sig != other_sig):
137141
return False
138142

139143
# Finally, check that the signed portions are the same
@@ -1347,9 +1351,23 @@ def __eq__(self, other: Any) -> bool:
13471351
if not isinstance(other, Delegations):
13481352
return False
13491353

1354+
# Assert that roles keys are the same before traversing them.
1355+
if self.roles.keys() != other.roles.keys():
1356+
return False
1357+
1358+
# Iterate over self.roles and other.roles at the same time
1359+
# as the order of the roles dictionaries is important.
1360+
role_names = zip(self.roles.keys(), other.roles.keys())
1361+
for self_role_name, other_role_name in role_names:
1362+
self_role = self.roles[self_role_name]
1363+
other_role = other.roles[other_role_name]
1364+
1365+
if (self_role_name != other_role_name) or (self_role != other_role):
1366+
return False
1367+
1368+
# Compare the rest of the fields besides "roles"
13501369
return (
13511370
self.keys == other.keys
1352-
and self.roles == other.roles
13531371
and self.unrecognized_fields == other.unrecognized_fields
13541372
)
13551373

0 commit comments

Comments
 (0)