Skip to content

Commit 414dfc8

Browse files
author
Jussi Kukkonen
committed
Metadata API: Move signature verification to Key
This is likely not needed by users of the API (as they are interested in the higher level functionality "verify delegate metadata with threshold of signatures"). Moving verify to Key makes the API cleaner because including both "verify myself" and "verify a delegate with threshold" can look awkward in Metadata, and because the ugly Securesystemslib integration is now Key class implementation detail (see Key.to_securesystemslib_key()). Also raise on verify failure instead of returning false: this was found to confuse API users (and was arguably not a pythonic way to handle it). * Name the function verify_signature() to make it clear what is being verified. * Assume only one signature per keyid exists: see #1422 * Raise only UnsignedMetadataError (when no signatures or verify failure), the remaining lower level errors will be handled in #1351 * Stop using a "keystore" in tests for the public keys: everything we need is in metadata already This changes API, but also should not be something API users want to call in the future when "verify a delegate with threshold" exists. Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent 41a6dac commit 414dfc8

File tree

2 files changed

+82
-87
lines changed

2 files changed

+82
-87
lines changed

tests/test_api.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,10 @@ def setUpClass(cls):
8181
# Load keys into memory
8282
cls.keystore = {}
8383
for role in ['delegation', 'snapshot', 'targets', 'timestamp']:
84-
cls.keystore[role] = {
85-
'private': import_ed25519_privatekey_from_file(
86-
os.path.join(cls.keystore_dir, role + '_key'),
87-
password="password"),
88-
'public': import_ed25519_publickey_from_file(
89-
os.path.join(cls.keystore_dir, role + '_key.pub'))
90-
}
84+
cls.keystore[role] = import_ed25519_privatekey_from_file(
85+
os.path.join(cls.keystore_dir, role + '_key'),
86+
password="password"
87+
)
9188

9289

9390
@classmethod
@@ -162,50 +159,46 @@ def test_read_write_read_compare(self):
162159

163160

164161
def test_sign_verify(self):
162+
root_path = os.path.join(self.repo_dir, 'metadata', 'root.json')
163+
root:Root = Metadata.from_file(root_path).signed
164+
165+
# Locate the public keys we need from root
166+
targets_keyid = next(iter(root.roles["targets"].keyids))
167+
targets_key = root.keys[targets_keyid]
168+
snapshot_keyid = next(iter(root.roles["snapshot"].keyids))
169+
snapshot_key = root.keys[snapshot_keyid]
170+
timestamp_keyid = next(iter(root.roles["timestamp"].keyids))
171+
timestamp_key = root.keys[timestamp_keyid]
172+
165173
# Load sample metadata (targets) and assert ...
166174
path = os.path.join(self.repo_dir, 'metadata', 'targets.json')
167175
metadata_obj = Metadata.from_file(path)
168176

169177
# ... it has a single existing signature,
170178
self.assertTrue(len(metadata_obj.signatures) == 1)
171179
# ... which is valid for the correct key.
172-
self.assertTrue(metadata_obj.verify(
173-
self.keystore['targets']['public']))
180+
targets_key.verify_signature(metadata_obj)
181+
with self.assertRaises(tuf.exceptions.UnsignedMetadataError):
182+
snapshot_key.verify_signature(metadata_obj)
174183

175-
sslib_signer = SSlibSigner(self.keystore['snapshot']['private'])
184+
sslib_signer = SSlibSigner(self.keystore['snapshot'])
176185
# Append a new signature with the unrelated key and assert that ...
177186
metadata_obj.sign(sslib_signer, append=True)
178187
# ... there are now two signatures, and
179188
self.assertTrue(len(metadata_obj.signatures) == 2)
180189
# ... both are valid for the corresponding keys.
181-
self.assertTrue(metadata_obj.verify(
182-
self.keystore['targets']['public']))
183-
self.assertTrue(metadata_obj.verify(
184-
self.keystore['snapshot']['public']))
190+
targets_key.verify_signature(metadata_obj)
191+
snapshot_key.verify_signature(metadata_obj)
185192

186-
sslib_signer.key_dict = self.keystore['timestamp']['private']
193+
sslib_signer = SSlibSigner(self.keystore['timestamp'])
187194
# Create and assign (don't append) a new signature and assert that ...
188195
metadata_obj.sign(sslib_signer, append=False)
189196
# ... there now is only one signature,
190197
self.assertTrue(len(metadata_obj.signatures) == 1)
191198
# ... valid for that key.
192-
self.assertTrue(metadata_obj.verify(
193-
self.keystore['timestamp']['public']))
194-
195-
# Assert exception if there are more than one signatures for a key
196-
metadata_obj.sign(sslib_signer, append=True)
197-
with self.assertRaises(tuf.exceptions.Error) as ctx:
198-
metadata_obj.verify(self.keystore['timestamp']['public'])
199-
self.assertTrue(
200-
'2 signatures for key' in str(ctx.exception),
201-
str(ctx.exception))
202-
203-
# Assert exception if there is no signature for a key
204-
with self.assertRaises(tuf.exceptions.Error) as ctx:
205-
metadata_obj.verify(self.keystore['targets']['public'])
206-
self.assertTrue(
207-
'no signature for' in str(ctx.exception),
208-
str(ctx.exception))
199+
timestamp_key.verify_signature(metadata_obj)
200+
with self.assertRaises(tuf.exceptions.UnsignedMetadataError):
201+
targets_key.verify_signature(metadata_obj)
209202

210203

211204
def test_metadata_base(self):

tuf/api/metadata.py

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from datetime import datetime, timedelta
2020
from typing import Any, Dict, List, Mapping, Optional
2121

22-
from securesystemslib.keys import verify_signature
22+
from securesystemslib import keys as sslib_keys
2323
from securesystemslib.signer import Signature, Signer
2424
from securesystemslib.storage import FilesystemBackend, StorageBackendInterface
2525
from securesystemslib.util import persist_temp_file
@@ -250,59 +250,6 @@ def sign(
250250

251251
return signature
252252

253-
def verify(
254-
self,
255-
key: Mapping[str, Any],
256-
signed_serializer: Optional[SignedSerializer] = None,
257-
) -> bool:
258-
"""Verifies 'signatures' over 'signed' that match the passed key by id.
259-
260-
Arguments:
261-
key: A securesystemslib-style public key object.
262-
signed_serializer: A SignedSerializer subclass instance that
263-
implements the desired canonicalization format. Per default a
264-
CanonicalJSONSerializer is used.
265-
266-
Raises:
267-
# TODO: Revise exception taxonomy
268-
tuf.exceptions.Error: None or multiple signatures found for key.
269-
securesystemslib.exceptions.FormatError: Key argument is malformed.
270-
tuf.api.serialization.SerializationError:
271-
'signed' cannot be serialized.
272-
securesystemslib.exceptions.CryptoError, \
273-
securesystemslib.exceptions.UnsupportedAlgorithmError:
274-
Signing errors.
275-
276-
Returns:
277-
A boolean indicating if the signature is valid for the passed key.
278-
279-
"""
280-
signatures_for_keyid = list(
281-
filter(lambda sig: sig.keyid == key["keyid"], self.signatures)
282-
)
283-
284-
if not signatures_for_keyid:
285-
raise exceptions.Error(f"no signature for key {key['keyid']}.")
286-
287-
if len(signatures_for_keyid) > 1:
288-
raise exceptions.Error(
289-
f"{len(signatures_for_keyid)} signatures for key "
290-
f"{key['keyid']}, not sure which one to verify."
291-
)
292-
293-
if signed_serializer is None:
294-
# Use local scope import to avoid circular import errors
295-
# pylint: disable=import-outside-toplevel
296-
from tuf.api.serialization.json import CanonicalJSONSerializer
297-
298-
signed_serializer = CanonicalJSONSerializer()
299-
300-
return verify_signature(
301-
key,
302-
signatures_for_keyid[0].to_dict(),
303-
signed_serializer.serialize(self.signed),
304-
)
305-
306253

307254
class Signed:
308255
"""A base class for the signed part of TUF metadata.
@@ -417,7 +364,9 @@ class Key:
417364
"""A container class representing the public portion of a Key.
418365
419366
Attributes:
420-
keyid: An identifier string
367+
keyid: An identifier string that must uniquely identify a key within
368+
the metadata it is used in. This implementation does not verify
369+
that keyid is the hash of a specific representation of the key.
421370
keytype: A string denoting a public key signature system,
422371
such as "rsa", "ed25519", and "ecdsa-sha2-nistp256".
423372
scheme: A string denoting a corresponding signature scheme. For example:
@@ -461,6 +410,59 @@ def to_dict(self) -> Dict[str, Any]:
461410
**self.unrecognized_fields,
462411
}
463412

413+
def to_securesystemslib_key(self) -> Dict[str, Any]:
414+
"""Returns a Securesystemslib compatible representation of self."""
415+
return {
416+
"keyid": self.keyid,
417+
"keytype": self.keytype,
418+
"scheme": self.scheme,
419+
"keyval": self.keyval,
420+
}
421+
422+
def verify_signature(
423+
self,
424+
metadata: Metadata,
425+
signed_serializer: Optional[SignedSerializer] = None,
426+
):
427+
"""Verifies that the 'metadata.signatures' contains a signature made
428+
with this key, correctly signing 'metadata.signed'.
429+
430+
Arguments:
431+
metadata: Metadata to verify
432+
signed_serializer: Optional; SignedSerializer to serialize
433+
'metadata.signed' with. Default is CanonicalJSONSerializer.
434+
435+
Raises:
436+
UnsignedMetadataError: The signature could not be verified for a
437+
variety of possible reasons: see error message.
438+
TODO: Various other errors currently bleed through from lower
439+
level components: Issue #1351
440+
"""
441+
try:
442+
sigs = metadata.signatures
443+
signature = next(sig for sig in sigs if sig.keyid == self.keyid)
444+
except StopIteration:
445+
raise exceptions.UnsignedMetadataError(
446+
f"no signature for key {self.keyid} found in metadata",
447+
metadata.signed,
448+
) from None
449+
450+
if signed_serializer is None:
451+
# pylint: disable=import-outside-toplevel
452+
from tuf.api.serialization.json import CanonicalJSONSerializer
453+
454+
signed_serializer = CanonicalJSONSerializer()
455+
456+
if not sslib_keys.verify_signature(
457+
self.to_securesystemslib_key(),
458+
signature.to_dict(),
459+
signed_serializer.serialize(metadata.signed),
460+
):
461+
raise exceptions.UnsignedMetadataError(
462+
f"Failed to verify {self.keyid} signature for metadata",
463+
metadata.signed,
464+
)
465+
464466

465467
class Role:
466468
"""A container class containing the set of keyids and threshold associated

0 commit comments

Comments
 (0)