Skip to content

Commit bd5912b

Browse files
authored
Merge pull request #1436 from jku/verify-delegate
Metadata API: Implement threshold verification
2 parents 885fcac + 271d5b7 commit bd5912b

File tree

3 files changed

+129
-7
lines changed

3 files changed

+129
-7
lines changed

tests/repository_data/repository/metadata/role2.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22
"signatures": [
33
{
44
"keyid": "c8022fa1e9b9cb239a6b362bbdffa9649e61ad2cb699d2e4bc4fdf7930a0e64a",
5-
"sig": "6c32f8cc2c642803a7b3b022ede0cf727e82964c1aa934571ef366bd5050ed02cfe3fdfe5477c08d0cbcc2dd17bb786d37ab1ce2b27e01ad79faf087594e0300"
5+
"sig": "75b196a224fd200e46e738b1216b3316c5384f61083872f8d14b8b0a378b2344e64b1a6f1a89a711206a66a0b199d65ac0e30fe15ddbc4de89fa8ff645f99403"
66
}
77
],
88
"signed": {
99
"_type": "targets",
10-
"delegations": {
11-
"keys": {},
12-
"roles": []
13-
},
1410
"expires": "2030-01-01T00:00:00Z",
1511
"spec_version": "1.0.0",
1612
"targets": {},

tests/test_api.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
)
5252

5353
from securesystemslib.signer import (
54-
SSlibSigner
54+
SSlibSigner,
55+
Signature
5556
)
5657

5758
logger = logging.getLogger(__name__)
@@ -333,6 +334,71 @@ def test_metadata_timestamp(self):
333334
)
334335

335336

337+
def test_metadata_verify_delegate(self):
338+
root_path = os.path.join(self.repo_dir, 'metadata', 'root.json')
339+
root = Metadata.from_file(root_path)
340+
snapshot_path = os.path.join(
341+
self.repo_dir, 'metadata', 'snapshot.json')
342+
snapshot = Metadata.from_file(snapshot_path)
343+
targets_path = os.path.join(
344+
self.repo_dir, 'metadata', 'targets.json')
345+
targets = Metadata.from_file(targets_path)
346+
role1_path = os.path.join(
347+
self.repo_dir, 'metadata', 'role1.json')
348+
role1 = Metadata.from_file(role1_path)
349+
role2_path = os.path.join(
350+
self.repo_dir, 'metadata', 'role2.json')
351+
role2 = Metadata.from_file(role2_path)
352+
353+
# test the expected delegation tree
354+
root.verify_delegate('root', root)
355+
root.verify_delegate('snapshot', snapshot)
356+
root.verify_delegate('targets', targets)
357+
targets.verify_delegate('role1', role1)
358+
role1.verify_delegate('role2', role2)
359+
360+
# only root and targets can verify delegates
361+
with self.assertRaises(TypeError):
362+
snapshot.verify_delegate('snapshot', snapshot)
363+
# verify fails for roles that are not delegated by delegator
364+
with self.assertRaises(ValueError):
365+
root.verify_delegate('role1', role1)
366+
with self.assertRaises(ValueError):
367+
targets.verify_delegate('targets', targets)
368+
# verify fails when delegator has no delegations
369+
with self.assertRaises(ValueError):
370+
role2.verify_delegate('role1', role1)
371+
372+
# verify fails when delegate content is modified
373+
expires = snapshot.signed.expires
374+
snapshot.signed.bump_expiration()
375+
with self.assertRaises(exceptions.UnsignedMetadataError):
376+
root.verify_delegate('snapshot', snapshot)
377+
snapshot.signed.expires = expires
378+
379+
# verify fails if roles keys do not sign the metadata
380+
with self.assertRaises(exceptions.UnsignedMetadataError):
381+
root.verify_delegate('timestamp', snapshot)
382+
383+
# Add a key to snapshot role, make sure the new sig fails to verify
384+
ts_keyid = next(iter(root.signed.roles["timestamp"].keyids))
385+
root.signed.add_key("snapshot", root.signed.keys[ts_keyid])
386+
snapshot.signatures[ts_keyid] = Signature(ts_keyid, "ff"*64)
387+
388+
# verify succeeds if threshold is reached even if some signatures
389+
# fail to verify
390+
root.verify_delegate('snapshot', snapshot)
391+
392+
# verify fails if threshold of signatures is not reached
393+
root.signed.roles['snapshot'].threshold = 2
394+
with self.assertRaises(exceptions.UnsignedMetadataError):
395+
root.verify_delegate('snapshot', snapshot)
396+
397+
# verify succeeds when we correct the new signature and reach the
398+
# threshold of 2 keys
399+
snapshot.sign(SSlibSigner(self.keystore['timestamp']), append=True)
400+
root.verify_delegate('snapshot', snapshot)
401+
336402

337403
def test_key_class(self):
338404
keys = {

tuf/api/metadata.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
import abc
1919
import io
20+
import logging
2021
import tempfile
2122
from collections import OrderedDict
2223
from datetime import datetime, timedelta
@@ -49,6 +50,8 @@
4950

5051
# pylint: disable=too-many-lines
5152

53+
logger = logging.getLogger(__name__)
54+
5255
# We aim to support SPECIFICATION_VERSION and require the input metadata
5356
# files to have the same major version (the first number) as ours.
5457
SPECIFICATION_VERSION = ["1", "0", "19"]
@@ -266,6 +269,63 @@ def sign(
266269

267270
return signature
268271

272+
def verify_delegate(
273+
self,
274+
delegated_role: str,
275+
delegated_metadata: "Metadata",
276+
signed_serializer: Optional[SignedSerializer] = None,
277+
) -> None:
278+
"""Verifies that 'delegated_metadata' is signed with the required
279+
threshold of keys for the delegated role 'delegated_role'.
280+
281+
Args:
282+
delegated_role: Name of the delegated role to verify
283+
delegated_metadata: The Metadata object for the delegated role
284+
signed_serializer: Optional; serializer used for delegate
285+
serialization. Default is CanonicalJSONSerializer.
286+
287+
Raises:
288+
UnsignedMetadataError: 'delegate' was not signed with required
289+
threshold of keys for 'role_name'
290+
"""
291+
292+
# Find the keys and role in delegator metadata
293+
role = None
294+
if isinstance(self.signed, Root):
295+
keys = self.signed.keys
296+
role = self.signed.roles.get(delegated_role)
297+
elif isinstance(self.signed, Targets):
298+
if self.signed.delegations is None:
299+
raise ValueError(f"No delegation found for {delegated_role}")
300+
301+
keys = self.signed.delegations.keys
302+
roles = self.signed.delegations.roles
303+
# Assume role names are unique in delegations.roles: #1426
304+
# Find first role in roles with matching name (or None if no match)
305+
role = next((r for r in roles if r.name == delegated_role), None)
306+
else:
307+
raise TypeError("Call is valid only on delegator metadata")
308+
309+
if role is None:
310+
raise ValueError(f"No delegation found for {delegated_role}")
311+
312+
# verify that delegated_metadata is signed by threshold of unique keys
313+
signing_keys = set()
314+
for keyid in role.keyids:
315+
key = keys[keyid]
316+
try:
317+
key.verify_signature(delegated_metadata, signed_serializer)
318+
signing_keys.add(key.keyid)
319+
except exceptions.UnsignedMetadataError:
320+
logger.info("Key %s failed to verify %s", keyid, delegated_role)
321+
322+
if len(signing_keys) < role.threshold:
323+
raise exceptions.UnsignedMetadataError(
324+
f"{delegated_role} was signed by {len(signing_keys)}/"
325+
f"{role.threshold} keys",
326+
delegated_metadata.signed,
327+
)
328+
269329

270330
class Signed(metaclass=abc.ABCMeta):
271331
"""A base class for the signed part of TUF metadata.
@@ -967,7 +1027,7 @@ class Delegations:
9671027

9681028
def __init__(
9691029
self,
970-
keys: Mapping[str, Key],
1030+
keys: Dict[str, Key],
9711031
roles: List[DelegatedRole],
9721032
unrecognized_fields: Optional[Mapping[str, Any]] = None,
9731033
) -> None:

0 commit comments

Comments
 (0)