Skip to content

Commit de78251

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1423 from jku/verify-in-key
Metadata API: implement sig verification in Key, store id in key
2 parents 7f3b15e + 414dfc8 commit de78251

File tree

2 files changed

+99
-101
lines changed

2 files changed

+99
-101
lines changed

tests/test_api.py

Lines changed: 32 additions & 39 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):
@@ -373,18 +366,18 @@ def test_key_class(self):
373366
# Testing that the workflow of deserializing and serializing
374367
# a key dictionary doesn't change the content.
375368
test_key_dict = key_dict.copy()
376-
key_obj = Key.from_dict(test_key_dict)
369+
key_obj = Key.from_dict("id", test_key_dict)
377370
self.assertEqual(key_dict, key_obj.to_dict())
378371
# Test creating an instance without a required attribute.
379372
for key in key_dict.keys():
380373
test_key_dict = key_dict.copy()
381374
del test_key_dict[key]
382375
with self.assertRaises(KeyError):
383-
Key.from_dict(test_key_dict)
376+
Key.from_dict("id", test_key_dict)
384377
# Test creating a Key instance with wrong keyval format.
385378
key_dict["keyval"] = {}
386379
with self.assertRaises(ValueError):
387-
Key.from_dict(key_dict)
380+
Key.from_dict("id", key_dict)
388381

389382

390383
def test_role_class(self):
@@ -413,7 +406,7 @@ def test_role_class(self):
413406
test_role_dict = role_dict.copy()
414407
del test_role_dict[role_attr]
415408
with self.assertRaises(KeyError):
416-
Key.from_dict(test_role_dict)
409+
Key.from_dict("id", test_role_dict)
417410
# Test creating a Role instance with keyid dublicates.
418411
# for keyid in role_dict["keyids"]:
419412
role_dict["keyids"].append(role_dict["keyids"][0])
@@ -433,15 +426,15 @@ def test_metadata_root(self):
433426

434427

435428
keyid = root_key2['keyid']
436-
key_metadata = Key(root_key2['keytype'], root_key2['scheme'],
429+
key_metadata = Key(keyid, root_key2['keytype'], root_key2['scheme'],
437430
root_key2['keyval'])
438431

439432
# Assert that root does not contain the new key
440433
self.assertNotIn(keyid, root.signed.roles['root'].keyids)
441434
self.assertNotIn(keyid, root.signed.keys)
442435

443436
# Add new root key
444-
root.signed.add_key('root', keyid, key_metadata)
437+
root.signed.add_key('root', key_metadata)
445438

446439
# Assert that key is added
447440
self.assertIn(keyid, root.signed.roles['root'].keyids)
@@ -453,7 +446,7 @@ def test_metadata_root(self):
453446

454447
# Try adding the same key again and assert its ignored.
455448
pre_add_keyid = root.signed.roles['root'].keyids.copy()
456-
root.signed.add_key('root', keyid, key_metadata)
449+
root.signed.add_key('root', key_metadata)
457450
self.assertEqual(pre_add_keyid, root.signed.roles['root'].keyids)
458451

459452
# Remove the key

tuf/api/metadata.py

Lines changed: 67 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from datetime import datetime, timedelta
2121
from typing import Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Type
2222

23-
from securesystemslib.keys import verify_signature
23+
from securesystemslib import keys as sslib_keys
2424
from securesystemslib.signer import Signature, Signer
2525
from securesystemslib.storage import FilesystemBackend, StorageBackendInterface
2626
from securesystemslib.util import persist_temp_file
@@ -251,59 +251,6 @@ def sign(
251251

252252
return signature
253253

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

308255
class Signed(metaclass=abc.ABCMeta):
309256
"""A base class for the signed part of TUF metadata.
@@ -431,6 +378,9 @@ class Key:
431378
"""A container class representing the public portion of a Key.
432379
433380
Attributes:
381+
keyid: An identifier string that must uniquely identify a key within
382+
the metadata it is used in. This implementation does not verify
383+
that keyid is the hash of a specific representation of the key.
434384
keytype: A string denoting a public key signature system,
435385
such as "rsa", "ed25519", and "ecdsa-sha2-nistp256".
436386
scheme: A string denoting a corresponding signature scheme. For example:
@@ -442,26 +392,28 @@ class Key:
442392

443393
def __init__(
444394
self,
395+
keyid: str,
445396
keytype: str,
446397
scheme: str,
447398
keyval: Dict[str, str],
448399
unrecognized_fields: Optional[Mapping[str, Any]] = None,
449400
) -> None:
450401
if not keyval.get("public"):
451402
raise ValueError("keyval doesn't follow the specification format!")
403+
self.keyid = keyid
452404
self.keytype = keytype
453405
self.scheme = scheme
454406
self.keyval = keyval
455407
self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {}
456408

457409
@classmethod
458-
def from_dict(cls, key_dict: Dict[str, Any]) -> "Key":
410+
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "Key":
459411
"""Creates Key object from its dict representation."""
460412
keytype = key_dict.pop("keytype")
461413
scheme = key_dict.pop("scheme")
462414
keyval = key_dict.pop("keyval")
463415
# All fields left in the key_dict are unrecognized.
464-
return cls(keytype, scheme, keyval, key_dict)
416+
return cls(keyid, keytype, scheme, keyval, key_dict)
465417

466418
def to_dict(self) -> Dict[str, Any]:
467419
"""Returns the dictionary representation of self."""
@@ -472,6 +424,59 @@ def to_dict(self) -> Dict[str, Any]:
472424
**self.unrecognized_fields,
473425
}
474426

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

476481
class Role:
477482
"""A container class containing the set of keyids and threshold associated
@@ -572,7 +577,7 @@ def from_dict(cls, signed_dict: Dict[str, Any]) -> "Root":
572577
roles = signed_dict.pop("roles")
573578

574579
for keyid, key_dict in keys.items():
575-
keys[keyid] = Key.from_dict(key_dict)
580+
keys[keyid] = Key.from_dict(keyid, key_dict)
576581
for role_name, role_dict in roles.items():
577582
roles[role_name] = Role.from_dict(role_dict)
578583

@@ -598,10 +603,10 @@ def to_dict(self) -> Dict[str, Any]:
598603
return root_dict
599604

600605
# Update key for a role.
601-
def add_key(self, role: str, keyid: str, key_metadata: Key) -> None:
602-
"""Adds new key for 'role' and updates the key store."""
603-
self.roles[role].keyids.add(keyid)
604-
self.keys[keyid] = key_metadata
606+
def add_key(self, role: str, key: Key) -> None:
607+
"""Adds new signing key for delegated role 'role'."""
608+
self.roles[role].keyids.add(key.keyid)
609+
self.keys[key.keyid] = key
605610

606611
def remove_key(self, role: str, keyid: str) -> None:
607612
"""Removes key from 'role' and updates the key store.
@@ -880,7 +885,7 @@ def from_dict(cls, delegations_dict: Dict[str, Any]) -> "Delegations":
880885
keys = delegations_dict.pop("keys")
881886
keys_res = {}
882887
for keyid, key_dict in keys.items():
883-
keys_res[keyid] = Key.from_dict(key_dict)
888+
keys_res[keyid] = Key.from_dict(keyid, key_dict)
884889
roles = delegations_dict.pop("roles")
885890
roles_res = []
886891
for role_dict in roles:

0 commit comments

Comments
 (0)