Skip to content

Commit a74f7a1

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1775 from MVrachev/validation-during-serialization
Add a "validate" argument option to JSONSerializer. The argument defaults to false: by default serialization works exactly as before.
2 parents a75abe3 + 6ea5372 commit a74f7a1

5 files changed

Lines changed: 460 additions & 5 deletions

File tree

tests/test_api.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
Targets,
4040
Timestamp,
4141
)
42-
from tuf.api.serialization import DeserializationError
42+
from tuf.api.serialization import DeserializationError, SerializationError
4343
from tuf.api.serialization.json import CanonicalJSONSerializer, JSONSerializer
4444

4545
logger = logging.getLogger(__name__)
@@ -157,6 +157,13 @@ def test_read_write_read_compare(self) -> None:
157157

158158
os.remove(path_2)
159159

160+
def test_serialize_with_validate(self) -> None:
161+
# Assert that by changing one required attribute validation will fail.
162+
root = Metadata.from_file(f"{self.repo_dir}/metadata/root.json")
163+
root.signed.version = 0
164+
with self.assertRaises(SerializationError):
165+
root.to_bytes(JSONSerializer(validate=True))
166+
160167
def test_to_from_bytes(self) -> None:
161168
for metadata in TOP_LEVEL_ROLE_NAMES:
162169
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")

tests/test_metadata_eq_.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""Test __eq__ implementations of classes inside tuf/api/metadata.py."""
7+
8+
9+
import copy
10+
import os
11+
import sys
12+
import unittest
13+
from typing import Any, ClassVar, Dict
14+
15+
from securesystemslib.signer import Signature
16+
17+
from tests import utils
18+
from tuf.api.metadata import (
19+
TOP_LEVEL_ROLE_NAMES,
20+
DelegatedRole,
21+
Delegations,
22+
Key,
23+
Metadata,
24+
MetaFile,
25+
Role,
26+
Root,
27+
Snapshot,
28+
TargetFile,
29+
Targets,
30+
Timestamp,
31+
)
32+
33+
34+
class TestMetadataComparisions(unittest.TestCase):
35+
"""Test __eq__ for all classes inside tuf/api/metadata.py."""
36+
37+
metadata: ClassVar[Dict[str, bytes]]
38+
39+
@classmethod
40+
def setUpClass(cls) -> None:
41+
cls.repo_dir = os.path.join(
42+
utils.TESTS_DIR, "repository_data", "repository", "metadata"
43+
)
44+
cls.metadata = {}
45+
for md in TOP_LEVEL_ROLE_NAMES:
46+
with open(os.path.join(cls.repo_dir, f"{md}.json"), "rb") as f:
47+
cls.metadata[md] = f.read()
48+
49+
def copy_and_simple_assert(self, obj: Any) -> Any:
50+
# Assert that obj is not equal to an object from another type
51+
self.assertNotEqual(obj, "")
52+
result_obj = copy.deepcopy(obj)
53+
# Assert that __eq__ works for equal objects.
54+
self.assertEqual(obj, result_obj)
55+
return result_obj
56+
57+
def test_metadata_eq_(self) -> None:
58+
md = Metadata.from_bytes(self.metadata["snapshot"])
59+
md_2: Metadata = self.copy_and_simple_assert(md)
60+
61+
for attr, value in [("signed", None), ("signatures", None)]:
62+
setattr(md_2, attr, value)
63+
self.assertNotEqual(md, md_2, f"Failed case: {attr}")
64+
65+
def test_md_eq_signatures_reversed_order(self) -> None:
66+
# Test comparing objects with same signatures but different order.
67+
68+
# Remove all signatures and create new ones.
69+
md = Metadata.from_bytes(self.metadata["snapshot"])
70+
md.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")}
71+
md_2 = copy.deepcopy(md)
72+
# Reverse signatures order in md_2.
73+
# In python3.7 we need to cast to a list and then reverse.
74+
md_2.signatures = dict(reversed(list(md_2.signatures.items())))
75+
# Assert that both objects are not the same because of signatures order.
76+
self.assertNotEqual(md, md_2)
77+
78+
# but if we fix the signatures order they will be equal
79+
md_2.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")}
80+
self.assertEqual(md, md_2)
81+
82+
def test_md_eq_special_signatures_tests(self) -> None:
83+
# Test that metadata objects with different signatures are not equal.
84+
md = Metadata.from_bytes(self.metadata["snapshot"])
85+
md_2 = copy.deepcopy(md)
86+
md_2.signatures = {}
87+
self.assertNotEqual(md, md_2)
88+
89+
# Test that metadata objects with empty signatures are equal
90+
md.signatures = {}
91+
self.assertEqual(md, md_2)
92+
93+
# Metadata objects with different signatures types are not equal.
94+
md_2.signatures = "" # type: ignore
95+
self.assertNotEqual(md, md_2)
96+
97+
def test_signed_eq_(self) -> None:
98+
md = Metadata.from_bytes(self.metadata["snapshot"])
99+
md_2: Metadata = self.copy_and_simple_assert(md)
100+
101+
# We don't need to make "signed" = None as that was done when testing
102+
# metadata attribute modifications.
103+
for attr, value in [("version", -1), ("spec_version", "0.0.0")]:
104+
setattr(md_2.signed, attr, value)
105+
self.assertNotEqual(md.signed, md_2.signed, f"Failed case: {attr}")
106+
107+
def test_key_eq_(self) -> None:
108+
key_dict = {
109+
"keytype": "rsa",
110+
"scheme": "rsassa-pss-sha256",
111+
"keyval": {"public": "foo"},
112+
}
113+
key = Key.from_dict("12sa12", key_dict)
114+
key_2: Key = self.copy_and_simple_assert(key)
115+
for attr, value in [
116+
("keyid", "a"),
117+
("keytype", "foo"),
118+
("scheme", "b"),
119+
("keytype", "b"),
120+
]:
121+
setattr(key_2, attr, value)
122+
self.assertNotEqual(key, key_2, f"Failed case: {attr}")
123+
124+
def test_role_eq_(self) -> None:
125+
role_dict = {
126+
"keyids": ["keyid1", "keyid2"],
127+
"threshold": 3,
128+
}
129+
role = Role.from_dict(role_dict)
130+
role_2: Role = self.copy_and_simple_assert(role)
131+
132+
for attr, value in [("keyids", []), ("threshold", 10)]:
133+
setattr(role_2, attr, value)
134+
self.assertNotEqual(role, role_2, f"Failed case: {attr}")
135+
136+
def test_root_eq_(self) -> None:
137+
md = Metadata.from_bytes(self.metadata["root"])
138+
signed_copy: Root = self.copy_and_simple_assert(md.signed)
139+
140+
# Common attributes between Signed and Root doesn't need testing.
141+
# Ignore mypy request for type annotations on attr and value
142+
for attr, value in [ # type: ignore
143+
("consistent_snapshot", None),
144+
("keys", {}),
145+
("roles", {}),
146+
]:
147+
148+
setattr(signed_copy, attr, value)
149+
self.assertNotEqual(md.signed, signed_copy, f"Failed case: {attr}")
150+
151+
def test_metafile_eq_(self) -> None:
152+
metafile_dict = {
153+
"version": 1,
154+
"length": 12,
155+
"hashes": {"sha256": "abc"},
156+
}
157+
metafile = MetaFile.from_dict(metafile_dict)
158+
metafile_2: MetaFile = self.copy_and_simple_assert(metafile)
159+
160+
# Ignore mypy request for type annotations on attr and value
161+
for attr, value in [ # type: ignore
162+
("version", None),
163+
("length", None),
164+
("hashes", {}),
165+
]:
166+
setattr(metafile_2, attr, value)
167+
self.assertNotEqual(metafile, metafile_2, f"Failed case: {attr}")
168+
169+
def test_timestamp_eq_(self) -> None:
170+
md = Metadata.from_bytes(self.metadata["timestamp"])
171+
signed_copy: Timestamp = self.copy_and_simple_assert(md.signed)
172+
173+
# Common attributes between Signed and Timestamp doesn't need testing.
174+
setattr(signed_copy, "snapshot_meta", None)
175+
self.assertNotEqual(md.signed, signed_copy)
176+
177+
def test_snapshot_eq_(self) -> None:
178+
md = Metadata.from_bytes(self.metadata["snapshot"])
179+
signed_copy: Snapshot = self.copy_and_simple_assert(md.signed)
180+
181+
# Common attributes between Signed and Snapshot doesn't need testing.
182+
setattr(signed_copy, "meta", None)
183+
self.assertNotEqual(md.signed, signed_copy)
184+
185+
def test_delegated_role_eq_(self) -> None:
186+
delegated_role_dict = {
187+
"keyids": ["keyid"],
188+
"name": "a",
189+
"terminating": False,
190+
"threshold": 1,
191+
"paths": ["fn1", "fn2"],
192+
}
193+
delegated_role = DelegatedRole.from_dict(delegated_role_dict)
194+
delegated_role_2: DelegatedRole = self.copy_and_simple_assert(
195+
delegated_role
196+
)
197+
198+
# Common attributes between DelegatedRole and Role doesn't need testing.
199+
for attr, value in [
200+
("name", ""),
201+
("terminating", None),
202+
("paths", [""]),
203+
("path_hash_prefixes", [""]),
204+
]:
205+
setattr(delegated_role_2, attr, value)
206+
msg = f"Failed case: {attr}"
207+
self.assertNotEqual(delegated_role, delegated_role_2, msg)
208+
209+
def test_delegations_eq_(self) -> None:
210+
delegations_dict = {
211+
"keys": {
212+
"keyid2": {
213+
"keytype": "ed25519",
214+
"scheme": "ed25519",
215+
"keyval": {"public": "bar"},
216+
}
217+
},
218+
"roles": [
219+
{
220+
"keyids": ["keyid2"],
221+
"name": "b",
222+
"terminating": True,
223+
"paths": ["fn2"],
224+
"threshold": 4,
225+
}
226+
],
227+
}
228+
delegations = Delegations.from_dict(delegations_dict)
229+
delegations_2: Delegations = self.copy_and_simple_assert(delegations)
230+
# Ignore mypy request for type annotations on attr and value
231+
for attr, value in [("keys", {}), ("roles", {})]: # type: ignore
232+
setattr(delegations_2, attr, value)
233+
msg = f"Failed case: {attr}"
234+
self.assertNotEqual(delegations, delegations_2, msg)
235+
236+
def test_targetfile_eq_(self) -> None:
237+
targetfile_dict = {
238+
"length": 12,
239+
"hashes": {"sha256": "abc"},
240+
}
241+
targetfile = TargetFile.from_dict(targetfile_dict, "file1.txt")
242+
targetfile_2: TargetFile = self.copy_and_simple_assert(targetfile)
243+
244+
# Common attr between TargetFile and MetaFile doesn't need testing.
245+
setattr(targetfile_2, "path", "")
246+
self.assertNotEqual(targetfile, targetfile_2)
247+
248+
def test_delegations_eq_roles_reversed_order(self) -> None:
249+
# Test comparing objects with same delegated roles but different order.
250+
role_one_dict = {
251+
"keyids": ["keyid1"],
252+
"name": "a",
253+
"terminating": False,
254+
"paths": ["fn1"],
255+
"threshold": 1,
256+
}
257+
role_two_dict = {
258+
"keyids": ["keyid2"],
259+
"name": "b",
260+
"terminating": True,
261+
"paths": ["fn2"],
262+
"threshold": 4,
263+
}
264+
265+
delegations_dict = {
266+
"keys": {
267+
"keyid2": {
268+
"keytype": "ed25519",
269+
"scheme": "ed25519",
270+
"keyval": {"public": "bar"},
271+
}
272+
},
273+
"roles": [role_one_dict, role_two_dict],
274+
}
275+
delegations = Delegations.from_dict(copy.deepcopy(delegations_dict))
276+
277+
# Create a second delegations obj with reversed roles order
278+
delegations_2 = copy.deepcopy(delegations)
279+
# In python3.7 we need to cast to a list and then reverse.
280+
delegations_2.roles = dict(reversed(list(delegations.roles.items())))
281+
282+
# Both objects are not the equal because of delegated roles order.
283+
self.assertNotEqual(delegations, delegations_2)
284+
285+
# but if we fix the delegated roles order they will be equal
286+
delegations_2.roles = delegations.roles
287+
288+
self.assertEqual(delegations, delegations_2)
289+
290+
def test_targets_eq_(self) -> None:
291+
md = Metadata.from_bytes(self.metadata["targets"])
292+
signed_copy: Targets = self.copy_and_simple_assert(md.signed)
293+
294+
# Common attributes between Targets and Signed doesn't need testing.
295+
# Ignore mypy request for type annotations on attr and value
296+
for attr, value in [("targets", {}), ("delegations", [])]: # type: ignore
297+
setattr(signed_copy, attr, value)
298+
self.assertNotEqual(md.signed, signed_copy, f"Failed case: {attr}")
299+
300+
301+
# Run unit test.
302+
if __name__ == "__main__":
303+
utils.configure_test_logging(sys.argv)
304+
unittest.main()

tests/test_trusted_metadata_set.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Targets,
2323
Timestamp,
2424
)
25+
from tuf.api.serialization.json import JSONSerializer
2526
from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet
2627

2728
logger = logging.getLogger(__name__)
@@ -49,7 +50,7 @@ def modify_metadata(
4950
metadata = Metadata.from_bytes(cls.metadata[rolename])
5051
modification_func(metadata.signed)
5152
metadata.sign(cls.keystore[rolename])
52-
return metadata.to_bytes()
53+
return metadata.to_bytes(JSONSerializer(validate=True))
5354

5455
@classmethod
5556
def setUpClass(cls) -> None:

0 commit comments

Comments
 (0)