Skip to content

Commit 2a5bfb9

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1424 from jku/enforce-unique-sigs
Metadata API: Store signatures as dict
2 parents de78251 + 146eb10 commit 2a5bfb9

File tree

2 files changed

+31
-21
lines changed

2 files changed

+31
-21
lines changed

tests/test_api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def test_sign_verify(self):
175175
metadata_obj = Metadata.from_file(path)
176176

177177
# ... it has a single existing signature,
178-
self.assertTrue(len(metadata_obj.signatures) == 1)
178+
self.assertEqual(len(metadata_obj.signatures), 1)
179179
# ... which is valid for the correct key.
180180
targets_key.verify_signature(metadata_obj)
181181
with self.assertRaises(tuf.exceptions.UnsignedMetadataError):
@@ -185,7 +185,7 @@ def test_sign_verify(self):
185185
# Append a new signature with the unrelated key and assert that ...
186186
metadata_obj.sign(sslib_signer, append=True)
187187
# ... there are now two signatures, and
188-
self.assertTrue(len(metadata_obj.signatures) == 2)
188+
self.assertEqual(len(metadata_obj.signatures), 2)
189189
# ... both are valid for the corresponding keys.
190190
targets_key.verify_signature(metadata_obj)
191191
snapshot_key.verify_signature(metadata_obj)
@@ -194,7 +194,7 @@ def test_sign_verify(self):
194194
# Create and assign (don't append) a new signature and assert that ...
195195
metadata_obj.sign(sslib_signer, append=False)
196196
# ... there now is only one signature,
197-
self.assertTrue(len(metadata_obj.signatures) == 1)
197+
self.assertEqual(len(metadata_obj.signatures), 1)
198198
# ... valid for that key.
199199
timestamp_key.verify_signature(metadata_obj)
200200
with self.assertRaises(tuf.exceptions.UnsignedMetadataError):
@@ -236,6 +236,12 @@ def test_metadata_base(self):
236236
self.assertFalse(is_expired)
237237
md.signed.expires = expires
238238

239+
# Test deserializing metadata with non-unique signatures:
240+
data = md.to_dict()
241+
data["signatures"].append({"keyid": data["signatures"][0]["keyid"], "sig": "foo"})
242+
with self.assertRaises(ValueError):
243+
Metadata.from_dict(data)
244+
239245

240246
def test_metafile_class(self):
241247
# Test from_dict and to_dict with all attributes.

tuf/api/metadata.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
import abc
1919
import tempfile
20+
from collections import OrderedDict
2021
from datetime import datetime, timedelta
2122
from typing import Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Type
2223

@@ -48,12 +49,13 @@ class Metadata:
4849
signed: A subclass of Signed, which has the actual metadata payload,
4950
i.e. one of Targets, Snapshot, Timestamp or Root.
5051
51-
signatures: A list of Securesystemslib Signature objects, each signing
52-
the canonical serialized representation of 'signed'.
53-
52+
signatures: An ordered dictionary of keyids to Signature objects, each
53+
signing the canonical serialized representation of 'signed'.
5454
"""
5555

56-
def __init__(self, signed: "Signed", signatures: List[Signature]) -> None:
56+
def __init__(
57+
self, signed: "Signed", signatures: "OrderedDict[str, Signature]"
58+
):
5759
self.signed = signed
5860
self.signatures = signatures
5961

@@ -89,10 +91,15 @@ def from_dict(cls, metadata: Dict[str, Any]) -> "Metadata":
8991
else:
9092
raise ValueError(f'unrecognized metadata type "{_type}"')
9193

92-
signatures = []
93-
for signature in metadata.pop("signatures"):
94-
signature_obj = Signature.from_dict(signature)
95-
signatures.append(signature_obj)
94+
# Make sure signatures are unique
95+
signatures: "OrderedDict[str, Signature]" = OrderedDict()
96+
for sig_dict in metadata.pop("signatures"):
97+
sig = Signature.from_dict(sig_dict)
98+
if sig.keyid in signatures:
99+
raise ValueError(
100+
f"Multiple signatures found for keyid {sig.keyid}"
101+
)
102+
signatures[sig.keyid] = sig
96103

97104
return cls(
98105
signed=inner_cls.from_dict(metadata.pop("signed")),
@@ -164,9 +171,7 @@ def from_bytes(
164171
def to_dict(self) -> Dict[str, Any]:
165172
"""Returns the dict representation of self."""
166173

167-
signatures = []
168-
for sig in self.signatures:
169-
signatures.append(sig.to_dict())
174+
signatures = [sig.to_dict() for sig in self.signatures.values()]
170175

171176
return {"signatures": signatures, "signed": self.signed.to_dict()}
172177

@@ -244,10 +249,10 @@ def sign(
244249

245250
signature = signer.sign(signed_serializer.serialize(self.signed))
246251

247-
if append:
248-
self.signatures.append(signature)
249-
else:
250-
self.signatures = [signature]
252+
if not append:
253+
self.signatures.clear()
254+
255+
self.signatures[signature.keyid] = signature
251256

252257
return signature
253258

@@ -453,9 +458,8 @@ def verify_signature(
453458
level components: Issue #1351
454459
"""
455460
try:
456-
sigs = metadata.signatures
457-
signature = next(sig for sig in sigs if sig.keyid == self.keyid)
458-
except StopIteration:
461+
signature = metadata.signatures[self.keyid]
462+
except KeyError:
459463
raise exceptions.UnsignedMetadataError(
460464
f"no signature for key {self.keyid} found in metadata",
461465
metadata.signed,

0 commit comments

Comments
 (0)