Skip to content

Commit 28b6917

Browse files
author
Martin Vrachev
committed
Root and Targets key API changes
Here is the list of all breaking API changes: 1) The "role" and "key" arguments in "Root.add_key()" are in reverse order - "key" becomes first and "role" second. 2) "Root.remove_key()" has been renamed to "Root.revoke_key()". 3) The "role" and "keyid" arguments in "Root.revoke_key()" are in reverse order - "keyid" becomes first and "role" second. 4) The "role" and "key" arguments in "Targets.add_key()" are in reverse order - "key" becomes first and "role" second. 5) "Targets.remove_key()" has been renamed to "Targets.revoke_key()". 6) The "role" and "keyid" arguments in "Targets.revoke_key()" are in reverse order - "keyid" becomes first and "role" second. 7) In both methods "Targets.add_key()" and "Targets.revoke_key()" the "role" argument becomes an optional with a default value of None. Those changes are made in an effort to make those methods logical for both cases when standard roles and succinct_roles are used. The "Root" API change was done in order to preserve naming and argument order consistency with "Targets" API. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent 15cd42c commit 28b6917

7 files changed

Lines changed: 129 additions & 51 deletions

File tree

docs/repository-library-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ are found, in the python-tuf library):
6363
```python
6464
with repository.edit(“targets”) as targets:
6565
# adds a key for role1 (as an example, arbitrary edits are allowed)
66-
targets.add_key(“role1”, key)
66+
targets.add_key(key, “role1”)
6767
```
6868

6969
This code loads current targets metadata for editing, adds the key to a role,

examples/repo_example/basic_repo.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def _in(days: float) -> datetime:
157157
for name in ["targets", "snapshot", "timestamp", "root"]:
158158
keys[name] = generate_ed25519_key()
159159
roles["root"].signed.add_key(
160-
name, Key.from_securesystemslib_key(keys[name])
160+
Key.from_securesystemslib_key(keys[name]), name
161161
)
162162

163163
# NOTE: We only need the public part to populate root, so it is possible to use
@@ -173,7 +173,7 @@ def _in(days: float) -> datetime:
173173
# required signature threshold.
174174
another_root_key = generate_ed25519_key()
175175
roles["root"].signed.add_key(
176-
"root", Key.from_securesystemslib_key(another_root_key)
176+
Key.from_securesystemslib_key(another_root_key), "root"
177177
)
178178
roles["root"].signed.roles["root"].threshold = 2
179179

@@ -343,9 +343,9 @@ def _in(days: float) -> datetime:
343343
# remains in place, it can be used to count towards the old and new threshold.
344344
new_root_key = generate_ed25519_key()
345345

346-
roles["root"].signed.remove_key("root", keys["root"]["keyid"])
346+
roles["root"].signed.revoke_key(keys["root"]["keyid"], "root")
347347
roles["root"].signed.add_key(
348-
"root", Key.from_securesystemslib_key(new_root_key)
348+
Key.from_securesystemslib_key(new_root_key), "root"
349349
)
350350
roles["root"].signed.version += 1
351351

tests/generated_data/generate_md.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ def generate_all_files(
8989
md_snapshot = Metadata(Snapshot(expires=EXPIRY))
9090
md_targets = Metadata(Targets(expires=EXPIRY))
9191

92-
md_root.signed.add_key("root", keys["ed25519_0"])
93-
md_root.signed.add_key("timestamp", keys["ed25519_1"])
94-
md_root.signed.add_key("snapshot", keys["ed25519_2"])
95-
md_root.signed.add_key("targets", keys["ed25519_3"])
92+
md_root.signed.add_key(keys["ed25519_0"], "root")
93+
md_root.signed.add_key(keys["ed25519_1"], "timestamp")
94+
md_root.signed.add_key(keys["ed25519_2"], "snapshot")
95+
md_root.signed.add_key(keys["ed25519_3"], "targets")
9696

9797
for i, md in enumerate([md_root, md_timestamp, md_snapshot, md_targets]):
9898
assert isinstance(md, Metadata)

tests/repository_simulator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def rotate_keys(self, role: str) -> None:
169169
self.signers[role].clear()
170170
for _ in range(0, self.root.roles[role].threshold):
171171
key, signer = self.create_key()
172-
self.root.add_key(role, key)
172+
self.root.add_key(key, role)
173173
self.add_signer(role, signer)
174174

175175
def _initialize(self) -> None:
@@ -182,7 +182,7 @@ def _initialize(self) -> None:
182182

183183
for role in TOP_LEVEL_ROLE_NAMES:
184184
key, signer = self.create_key()
185-
self.md_root.signed.add_key(role, key)
185+
self.md_root.signed.add_key(key, role)
186186
self.add_signer(role, signer)
187187

188188
self.publish_root()
@@ -370,7 +370,7 @@ def add_delegation(
370370

371371
# By default add one new key for the role
372372
key, signer = self.create_key()
373-
delegator.add_key(role.name, key)
373+
delegator.add_key(key, role.name)
374374
self.add_signer(role.name, signer)
375375

376376
# Add metadata for the role

tests/test_api.py

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def test_metadata_verify_delegate(self) -> None:
361361

362362
# Add a key to snapshot role, make sure the new sig fails to verify
363363
ts_keyid = next(iter(root.signed.roles[Timestamp.type].keyids))
364-
root.signed.add_key(Snapshot.type, root.signed.keys[ts_keyid])
364+
root.signed.add_key(root.signed.keys[ts_keyid], Snapshot.type)
365365
snapshot.signatures[ts_keyid] = Signature(ts_keyid, "ff" * 64)
366366

367367
# verify succeeds if threshold is reached even if some signatures
@@ -390,7 +390,7 @@ def test_key_class(self) -> None:
390390
with self.assertRaises(ValueError):
391391
Key.from_securesystemslib_key(sslib_key)
392392

393-
def test_root_add_key_and_remove_key(self) -> None:
393+
def test_root_add_key_and_revoke_key(self) -> None:
394394
root_path = os.path.join(self.repo_dir, "metadata", "root.json")
395395
root = Metadata[Root].from_file(root_path)
396396

@@ -410,8 +410,12 @@ def test_root_add_key_and_remove_key(self) -> None:
410410
self.assertNotIn(keyid, root.signed.roles[Root.type].keyids)
411411
self.assertNotIn(keyid, root.signed.keys)
412412

413+
# Assert that add_key with old argument order will raise an error
414+
with self.assertRaises(ValueError):
415+
root.signed.add_key(Root.type, key_metadata) # type: ignore
416+
413417
# Add new root key
414-
root.signed.add_key(Root.type, key_metadata)
418+
root.signed.add_key(key_metadata, Root.type)
415419

416420
# Assert that key is added
417421
self.assertIn(keyid, root.signed.roles[Root.type].keyids)
@@ -423,30 +427,30 @@ def test_root_add_key_and_remove_key(self) -> None:
423427

424428
# Try adding the same key again and assert its ignored.
425429
pre_add_keyid = root.signed.roles[Root.type].keyids.copy()
426-
root.signed.add_key(Root.type, key_metadata)
430+
root.signed.add_key(key_metadata, Root.type)
427431
self.assertEqual(pre_add_keyid, root.signed.roles[Root.type].keyids)
428432

429433
# Add the same key to targets role as well
430-
root.signed.add_key(Targets.type, key_metadata)
434+
root.signed.add_key(key_metadata, Targets.type)
431435

432436
# Add the same key to a nonexistent role.
433437
with self.assertRaises(ValueError):
434-
root.signed.add_key("nosuchrole", key_metadata)
438+
root.signed.add_key(key_metadata, "nosuchrole")
435439

436440
# Remove the key from root role (targets role still uses it)
437-
root.signed.remove_key(Root.type, keyid)
441+
root.signed.revoke_key(keyid, Root.type)
438442
self.assertNotIn(keyid, root.signed.roles[Root.type].keyids)
439443
self.assertIn(keyid, root.signed.keys)
440444

441445
# Remove the key from targets as well
442-
root.signed.remove_key(Targets.type, keyid)
446+
root.signed.revoke_key(keyid, Targets.type)
443447
self.assertNotIn(keyid, root.signed.roles[Targets.type].keyids)
444448
self.assertNotIn(keyid, root.signed.keys)
445449

446450
with self.assertRaises(ValueError):
447-
root.signed.remove_key(Root.type, "nosuchkey")
451+
root.signed.revoke_key("nosuchkey", Root.type)
448452
with self.assertRaises(ValueError):
449-
root.signed.remove_key("nosuchrole", keyid)
453+
root.signed.revoke_key(keyid, "nosuchrole")
450454

451455
def test_is_target_in_pathpattern(self) -> None:
452456
# pylint: disable=protected-access
@@ -507,9 +511,13 @@ def test_targets_key_api(self) -> None:
507511
}
508512
key = Key.from_dict("id2", key_dict)
509513

514+
# Assert that add_key with old argument order will raise an error
515+
with self.assertRaises(ValueError):
516+
targets.add_key("role1", key) # type: ignore
517+
510518
# Assert that delegated role "role1" does not contain the new key
511519
self.assertNotIn(key.keyid, targets.delegations.roles["role1"].keyids)
512-
targets.add_key("role1", key)
520+
targets.add_key(key, "role1")
513521

514522
# Assert that the new key is added to the delegated role "role1"
515523
self.assertIn(key.keyid, targets.delegations.roles["role1"].keyids)
@@ -519,46 +527,89 @@ def test_targets_key_api(self) -> None:
519527

520528
# Try adding the same key again and assert its ignored.
521529
past_keyid = targets.delegations.roles["role1"].keyids.copy()
522-
targets.add_key("role1", key)
530+
targets.add_key(key, "role1")
523531
self.assertEqual(past_keyid, targets.delegations.roles["role1"].keyids)
524532

525533
# Try adding a key to a delegated role that doesn't exists
526534
with self.assertRaises(ValueError):
527-
targets.add_key("nosuchrole", key)
535+
targets.add_key(key, "nosuchrole")
528536

529537
# Add the same key to "role2" as well
530-
targets.add_key("role2", key)
538+
targets.add_key(key, "role2")
531539

532540
# Remove the key from "role1" role ("role2" still uses it)
533-
targets.remove_key("role1", key.keyid)
541+
targets.revoke_key(key.keyid, "role1")
534542

535543
# Assert that delegated role "role1" doesn't contain the key.
536544
self.assertNotIn(key.keyid, targets.delegations.roles["role1"].keyids)
537545
self.assertIn(key.keyid, targets.delegations.roles["role2"].keyids)
538546

539547
# Remove the key from "role2" as well
540-
targets.remove_key("role2", key.keyid)
548+
targets.revoke_key(key.keyid, "role2")
541549
self.assertNotIn(key.keyid, targets.delegations.roles["role2"].keyids)
542550

543551
# Try remove key not used by "role1"
544552
with self.assertRaises(ValueError):
545-
targets.remove_key("role1", key.keyid)
553+
targets.revoke_key(key.keyid, "role1")
546554

547555
# Try removing a key from delegated role that doesn't exists
548556
with self.assertRaises(ValueError):
549-
targets.remove_key("nosuchrole", key.keyid)
557+
targets.revoke_key(key.keyid, "nosuchrole")
550558

551559
# Remove delegations as a whole
552560
targets.delegations = None
553-
# Test that calling add_key and remove_key throws an error
561+
# Test that calling add_key and revoke_key throws an error
554562
# and that delegations is still None after each of the api calls
555563
with self.assertRaises(ValueError):
556-
targets.add_key("role1", key)
564+
targets.add_key(key, "role1")
557565
self.assertTrue(targets.delegations is None)
558566
with self.assertRaises(ValueError):
559-
targets.remove_key("role1", key.keyid)
567+
targets.revoke_key(key.keyid, "role1")
560568
self.assertTrue(targets.delegations is None)
561569

570+
def test_targets_key_api_with_succinct_roles(self) -> None:
571+
targets_path = os.path.join(self.repo_dir, "metadata", "targets.json")
572+
targets: Targets = Metadata[Targets].from_file(targets_path).signed
573+
key_dict = {
574+
"keytype": "ed25519",
575+
"keyval": {
576+
"public": "edcd0a32a07dce33f7c7873aaffbff36d20ea30787574ead335eefd337e4dacd"
577+
},
578+
"scheme": "ed25519",
579+
}
580+
key = Key.from_dict("id2", key_dict)
581+
582+
# Remove delegated roles.
583+
assert targets.delegations is not None
584+
assert targets.delegations.roles is not None
585+
targets.delegations.roles = None
586+
targets.delegations.keys = {}
587+
588+
# Add succinct_roles information.
589+
targets.delegations.succinct_roles = SuccinctRoles([], 1, 8, "foo")
590+
self.assertEqual(len(targets.delegations.keys), 0)
591+
self.assertEqual(len(targets.delegations.succinct_roles.keyids), 0)
592+
593+
# Add a key to succinct_roles and verify it's saved.
594+
targets.add_key(key)
595+
self.assertIn(key.keyid, targets.delegations.keys)
596+
self.assertIn(key.keyid, targets.delegations.succinct_roles.keyids)
597+
self.assertEqual(len(targets.delegations.keys), 1)
598+
599+
# Try adding the same key again and verify that noting is added.
600+
targets.add_key(key)
601+
self.assertEqual(len(targets.delegations.keys), 1)
602+
603+
# Remove the key and verify it's not stored anymore.
604+
targets.revoke_key(key.keyid)
605+
self.assertNotIn(key.keyid, targets.delegations.keys)
606+
self.assertNotIn(key.keyid, targets.delegations.succinct_roles.keyids)
607+
self.assertEqual(len(targets.delegations.keys), 0)
608+
609+
# Try removing it again.
610+
with self.assertRaises(ValueError):
611+
targets.revoke_key(key.keyid)
612+
562613
def test_length_and_hash_validation(self) -> None:
563614

564615
# Test metadata files' hash and length verification.

tests/test_updater_key_rotations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def test_root_rotation(self, root_versions: List[MdVersion]) -> None:
184184

185185
self.sim.root.roles[Root.type].threshold = rootver.threshold
186186
for i in rootver.keys:
187-
self.sim.root.add_key(Root.type, self.keys[i])
187+
self.sim.root.add_key(self.keys[i], Root.type)
188188
for i in rootver.sigs:
189189
self.sim.add_signer(Root.type, self.signers[i])
190190
self.sim.root.version += 1
@@ -254,7 +254,7 @@ def test_non_root_rotations(self, md_version: MdVersion) -> None:
254254

255255
self.sim.root.roles[role].threshold = md_version.threshold
256256
for i in md_version.keys:
257-
self.sim.root.add_key(role, self.keys[i])
257+
self.sim.root.add_key(self.keys[i], role)
258258

259259
for i in md_version.sigs:
260260
self.sim.add_signer(role, self.signers[i])

tuf/api/metadata.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -945,28 +945,33 @@ def to_dict(self) -> Dict[str, Any]:
945945
)
946946
return root_dict
947947

948-
def add_key(self, role: str, key: Key) -> None:
948+
def add_key(self, key: Key, role: str) -> None:
949949
"""Adds new signing key for delegated role ``role``.
950950
951951
Args:
952-
role: Name of the role, for which ``key`` is added.
953952
key: Signing key to be added for ``role``.
953+
role: Name of the role, for which ``key`` is added.
954954
955955
Raises:
956-
ValueError: If ``role`` doesn't exist.
956+
ValueError: If the argument order is wrong or if ``role`` doesn't
957+
exist.
957958
"""
959+
# Verify that our users are not using the old argument order.
960+
if isinstance(role, Key):
961+
raise ValueError("Role must be a string, not a Key instance")
962+
958963
if role not in self.roles:
959964
raise ValueError(f"Role {role} doesn't exist")
960965
if key.keyid not in self.roles[role].keyids:
961966
self.roles[role].keyids.append(key.keyid)
962967
self.keys[key.keyid] = key
963968

964-
def remove_key(self, role: str, keyid: str) -> None:
965-
"""Removes key from ``role`` and updates the key store.
969+
def revoke_key(self, keyid: str, role: str) -> None:
970+
"""Revoke key from ``role`` and updates the key store.
966971
967972
Args:
968-
role: Name of the role, for which a signing key is removed.
969973
keyid: Identifier of the key to be removed for ``role``.
974+
role: Name of the role, for which a signing key is removed.
970975
971976
Raises:
972977
ValueError: If ``role`` doesn't exist or if ``role`` doesn't include
@@ -1972,17 +1977,23 @@ def to_dict(self) -> Dict[str, Any]:
19721977
targets_dict["delegations"] = self.delegations.to_dict()
19731978
return targets_dict
19741979

1975-
def add_key(self, role: str, key: Key) -> None:
1980+
def add_key(self, key: Key, role: Optional[str] = None) -> None:
19761981
"""Adds new signing key for delegated role ``role``.
19771982
1983+
If succinct_roles is used then the ``role`` argument is not required.
1984+
19781985
Args:
1979-
role: Name of the role, for which ``key`` is added.
19801986
key: Signing key to be added for ``role``.
1987+
role: Name of the role, for which ``key`` is added.
19811988
19821989
Raises:
1983-
ValueError: If there are no delegated roles or if ``role`` is not
1984-
delegated by this Target.
1990+
ValueError: If the argument order is wrong or if there are no
1991+
delegated roles or if ``role`` is not delegated by this Target.
19851992
"""
1993+
# Verify that our users are not using the old argument order.
1994+
if isinstance(role, Key):
1995+
raise ValueError("Role must be a string, not a Key instance")
1996+
19861997
if self.delegations is None:
19871998
raise ValueError(f"Delegated role {role} doesn't exist")
19881999

@@ -1991,19 +2002,27 @@ def add_key(self, role: str, key: Key) -> None:
19912002
raise ValueError(f"Delegated role {role} doesn't exist")
19922003
if key.keyid not in self.delegations.roles[role].keyids:
19932004
self.delegations.roles[role].keyids.append(key.keyid)
1994-
self.delegations.keys[key.keyid] = key
19952005

1996-
def remove_key(self, role: str, keyid: str) -> None:
1997-
"""Removes key from delegated role ``role`` and updates the delegations
2006+
elif self.delegations.succinct_roles is not None:
2007+
if key.keyid not in self.delegations.succinct_roles.keyids:
2008+
self.delegations.succinct_roles.keyids.append(key.keyid)
2009+
2010+
self.delegations.keys[key.keyid] = key
2011+
2012+
def revoke_key(self, keyid: str, role: Optional[str] = None) -> None:
2013+
"""Revokes key from delegated role ``role`` and updates the delegations
19982014
key store.
19992015
2016+
If succinct_roles is used then the ``role`` argument is not required.
2017+
20002018
Args:
2001-
role: Name of the role, for which a signing key is removed.
20022019
keyid: Identifier of the key to be removed for ``role``.
2020+
role: Name of the role, for which a signing key is removed.
20032021
20042022
Raises:
20052023
ValueError: If there are no delegated roles or if ``role`` is not
2006-
delegated by this ``Target`` or if key is not used by ``role``.
2024+
delegated by this ``Target`` or if key is not used by ``role``
2025+
or if key with id ``keyid`` is not used by succinct roles.
20072026
"""
20082027
if self.delegations is None:
20092028
raise ValueError(f"Delegated role {role} doesn't exist")
@@ -2019,4 +2038,12 @@ def remove_key(self, role: str, keyid: str) -> None:
20192038
if keyid in keyinfo.keyids:
20202039
return
20212040

2022-
del self.delegations.keys[keyid]
2041+
elif self.delegations.succinct_roles is not None:
2042+
if keyid not in self.delegations.succinct_roles.keyids:
2043+
raise ValueError(
2044+
f"Key with id {keyid} is not used by succinct_roles"
2045+
)
2046+
2047+
self.delegations.succinct_roles.keyids.remove(keyid)
2048+
2049+
del self.delegations.keys[keyid]

0 commit comments

Comments
 (0)