Skip to content

Commit 9d3ef85

Browse files
authored
Merge pull request #1193 from sechkova/metadata-root
Add root metadata class to new TUF metadata model
2 parents 2aae0ba + 5bfd9dd commit 9d3ef85

File tree

2 files changed

+152
-4
lines changed

2 files changed

+152
-4
lines changed

tests/test_api.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import shutil
1414
import tempfile
1515
import unittest
16+
import copy
1617

1718
from datetime import datetime, timedelta
1819
from dateutil.relativedelta import relativedelta
@@ -42,6 +43,10 @@ def setUpModule():
4243
import_ed25519_privatekey_from_file
4344
)
4445

46+
from securesystemslib.keys import (
47+
format_keyval_to_metadata
48+
)
49+
4550
logger = logging.getLogger(__name__)
4651

4752

@@ -215,12 +220,14 @@ def test_metadata_snapshot(self):
215220
snapshot = Metadata.from_json_file(snapshot_path)
216221

217222
# Create a dict representing what we expect the updated data to be
218-
fileinfo = snapshot.signed.meta
223+
fileinfo = copy.deepcopy(snapshot.signed.meta)
219224
hashes = {'sha256': 'c2986576f5fdfd43944e2b19e775453b96748ec4fe2638a6d2f32f1310967095'}
220225
fileinfo['role1.json']['version'] = 2
221226
fileinfo['role1.json']['hashes'] = hashes
222227
fileinfo['role1.json']['length'] = 123
223228

229+
230+
self.assertNotEqual(snapshot.signed.meta, fileinfo)
224231
snapshot.signed.update('role1', 2, 123, hashes)
225232
self.assertEqual(snapshot.signed.meta, fileinfo)
226233

@@ -250,14 +257,73 @@ def test_metadata_timestamp(self):
250257
self.assertEqual(timestamp.signed.expires, datetime(2036, 1, 3, 0, 0))
251258

252259
hashes = {'sha256': '0ae9664468150a9aa1e7f11feecb32341658eb84292851367fea2da88e8a58dc'}
253-
fileinfo = timestamp.signed.meta['snapshot.json']
260+
fileinfo = copy.deepcopy(timestamp.signed.meta['snapshot.json'])
254261
fileinfo['hashes'] = hashes
255262
fileinfo['version'] = 2
256263
fileinfo['length'] = 520
264+
265+
self.assertNotEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
257266
timestamp.signed.update(2, 520, hashes)
258267
self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo)
259268

260269

270+
def test_metadata_root(self):
271+
root_path = os.path.join(
272+
self.repo_dir, 'metadata', 'root.json')
273+
root = Metadata.from_json_file(root_path)
274+
275+
# Add a second key to root role
276+
root_key2 = import_ed25519_publickey_from_file(
277+
os.path.join(self.keystore_dir, 'root_key2.pub'))
278+
279+
keyid = root_key2['keyid']
280+
key_metadata = format_keyval_to_metadata(
281+
root_key2['keytype'], root_key2['scheme'], root_key2['keyval'])
282+
283+
# Assert that root does not contain the new key
284+
self.assertNotIn(keyid, root.signed.roles['root']['keyids'])
285+
self.assertNotIn(keyid, root.signed.keys)
286+
287+
# Add new root key
288+
root.signed.add_key('root', keyid, key_metadata)
289+
290+
# Assert that key is added
291+
self.assertIn(keyid, root.signed.roles['root']['keyids'])
292+
self.assertIn(keyid, root.signed.keys)
293+
294+
# Remove the key
295+
root.signed.remove_key('root', keyid)
296+
297+
# Assert that root does not contain the new key anymore
298+
self.assertNotIn(keyid, root.signed.roles['root']['keyids'])
299+
self.assertNotIn(keyid, root.signed.keys)
300+
301+
302+
303+
def test_metadata_targets(self):
304+
targets_path = os.path.join(
305+
self.repo_dir, 'metadata', 'targets.json')
306+
targets = Metadata.from_json_file(targets_path)
307+
308+
# Create a fileinfo dict representing what we expect the updated data to be
309+
filename = 'file2.txt'
310+
hashes = {
311+
"sha256": "141f740f53781d1ca54b8a50af22cbf74e44c21a998fa2a8a05aaac2c002886b",
312+
"sha512": "ef5beafa16041bcdd2937140afebd485296cd54f7348ecd5a4d035c09759608de467a7ac0eb58753d0242df873c305e8bffad2454aa48f44480f15efae1cacd0"
313+
},
314+
315+
fileinfo = {
316+
'hashes': hashes,
317+
'length': 28
318+
}
319+
320+
# Assert that data is not aleady equal
321+
self.assertNotEqual(targets.signed.targets[filename], fileinfo)
322+
# Update an already existing fileinfo
323+
targets.signed.update(filename, fileinfo)
324+
# Verify that data is updated
325+
self.assertEqual(targets.signed.targets[filename], fileinfo)
326+
261327
# Run unit test.
262328
if __name__ == '__main__':
263329
utils.configure_test_logging(sys.argv)

tuf/api/metadata.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@ class also that has a 'from_dict' factory method. (Currently this is
8888
elif _type == 'timestamp':
8989
inner_cls = Timestamp
9090
elif _type == 'root':
91-
# TODO: implement Root class
92-
raise NotImplementedError('Root not yet implemented')
91+
inner_cls = Root
9392
else:
9493
raise ValueError(f'unrecognized metadata type "{_type}"')
9594

@@ -335,6 +334,89 @@ def bump_version(self) -> None:
335334
self.version += 1
336335

337336

337+
class Root(Signed):
338+
"""A container for the signed part of root metadata.
339+
340+
Attributes:
341+
consistent_snapshot: A boolean indicating whether the repository
342+
supports consistent snapshots.
343+
keys: A dictionary that contains a public key store used to verify
344+
top level roles metadata signatures::
345+
{
346+
'<KEYID>': {
347+
'keytype': '<KEY TYPE>',
348+
'scheme': '<KEY SCHEME>',
349+
'keyid_hash_algorithms': [
350+
'<HASH ALGO 1>',
351+
'<HASH ALGO 2>'
352+
...
353+
],
354+
'keyval': {
355+
'public': '<PUBLIC KEY HEX REPRESENTATION>'
356+
}
357+
},
358+
...
359+
},
360+
roles: A dictionary that contains a list of signing keyids and
361+
a signature threshold for each top level role::
362+
{
363+
'<ROLE>': {
364+
'keyids': ['<SIGNING KEY KEYID>', ...],
365+
'threshold': <SIGNATURE THRESHOLD>,
366+
},
367+
...
368+
}
369+
370+
"""
371+
# TODO: determine an appropriate value for max-args and fix places where
372+
# we violate that. This __init__ function takes 7 arguments, whereas the
373+
# default max-args value for pylint is 5
374+
# pylint: disable=too-many-arguments
375+
def __init__(
376+
self, _type: str, version: int, spec_version: str,
377+
expires: datetime, consistent_snapshot: bool,
378+
keys: JsonDict, roles: JsonDict) -> None:
379+
super().__init__(_type, version, spec_version, expires)
380+
# TODO: Add classes for keys and roles
381+
self.consistent_snapshot = consistent_snapshot
382+
self.keys = keys
383+
self.roles = roles
384+
385+
386+
# Serialization.
387+
def to_dict(self) -> JsonDict:
388+
"""Returns the JSON-serializable dictionary representation of self. """
389+
json_dict = super().to_dict()
390+
json_dict.update({
391+
'consistent_snapshot': self.consistent_snapshot,
392+
'keys': self.keys,
393+
'roles': self.roles
394+
})
395+
return json_dict
396+
397+
398+
# Update key for a role.
399+
def add_key(self, role: str, keyid: str, key_metadata: JsonDict) -> None:
400+
"""Adds new key for 'role' and updates the key store. """
401+
if keyid not in self.roles[role]['keyids']:
402+
self.roles[role]['keyids'].append(keyid)
403+
self.keys[keyid] = key_metadata
404+
405+
406+
# Remove key for a role.
407+
def remove_key(self, role: str, keyid: str) -> None:
408+
"""Removes key for 'role' and updates the key store. """
409+
if keyid in self.roles[role]['keyids']:
410+
self.roles[role]['keyids'].remove(keyid)
411+
for keyinfo in self.roles.values():
412+
if keyid in keyinfo['keyids']:
413+
return
414+
415+
del self.keys[keyid]
416+
417+
418+
419+
338420
class Timestamp(Signed):
339421
"""A container for the signed part of timestamp metadata.
340422

0 commit comments

Comments
 (0)