Merge pull request #1691 from MVrachev/key-rotations

Tests: add tests for non-root key rotations
This commit is contained in:
Jussi Kukkonen 2021-12-08 11:07:59 +02:00 committed by GitHub
commit 942e6d25ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 60 deletions

View file

@ -10,20 +10,20 @@
import tempfile
import unittest
from dataclasses import dataclass
from typing import List, Optional, Type
from typing import Dict, List, Optional, Type
from securesystemslib.signer import SSlibSigner
from tests import utils
from tests.repository_simulator import RepositorySimulator
from tests.utils import run_sub_tests_with_dataset
from tuf.api.metadata import Key, Root
from tuf.api.metadata import Key, Metadata, Root
from tuf.exceptions import UnsignedMetadataError
from tuf.ngclient import Updater
@dataclass
class RootVersion:
class MdVersion:
keys: List[int]
threshold: int
sigs: List[int]
@ -36,27 +36,26 @@ class TestUpdaterKeyRotations(unittest.TestCase):
# set dump_dir to trigger repository state dumps
dump_dir: Optional[str] = None
def setUp(self) -> None:
self.sim: RepositorySimulator
self.metadata_dir: str
self.subtest_count = 0
@classmethod
def setUpClass(cls) -> None:
cls.sim: RepositorySimulator
cls.metadata_dir: str
# pylint: disable-next=consider-using-with
self.temp_dir = tempfile.TemporaryDirectory()
cls.temp_dir = tempfile.TemporaryDirectory()
# Pre-create a bunch of keys and signers
self.keys: List[Key] = []
self.signers: List[SSlibSigner] = []
cls.keys: List[Key] = []
cls.signers: List[SSlibSigner] = []
for _ in range(10):
key, signer = RepositorySimulator.create_key()
self.keys.append(key)
self.signers.append(signer)
cls.keys.append(key)
cls.signers.append(signer)
def tearDown(self) -> None:
self.temp_dir.cleanup()
@classmethod
def tearDownClass(cls) -> None:
cls.temp_dir.cleanup()
def setup_subtest(self) -> None:
self.subtest_count += 1
# Setup repository for subtest: make sure no roots have been published
self.sim = RepositorySimulator()
self.sim.signed_roots.clear()
@ -64,7 +63,7 @@ def setup_subtest(self) -> None:
if self.dump_dir is not None:
# create subtest dumpdir
name = f"{self.id().split('.')[-1]}-{self.subtest_count}"
name = f"{self.id().split('.')[-1]}-{self.case_name}"
self.sim.dump_dir = os.path.join(self.dump_dir, name)
os.mkdir(self.sim.dump_dir)
@ -88,82 +87,82 @@ def _run_refresh(self) -> None:
# fmt: off
root_rotation_cases = {
"1-of-1 key rotation": [
RootVersion(keys=[1], threshold=1, sigs=[1]),
RootVersion(keys=[2], threshold=1, sigs=[2, 1]),
RootVersion(keys=[2], threshold=1, sigs=[2]),
MdVersion(keys=[1], threshold=1, sigs=[1]),
MdVersion(keys=[2], threshold=1, sigs=[2, 1]),
MdVersion(keys=[2], threshold=1, sigs=[2]),
],
"1-of-1 key rotation, unused signatures": [
RootVersion(keys=[1], threshold=1, sigs=[3, 1, 4]),
RootVersion(keys=[2], threshold=1, sigs=[3, 2, 1, 4]),
RootVersion(keys=[2], threshold=1, sigs=[3, 2, 4]),
MdVersion(keys=[1], threshold=1, sigs=[3, 1, 4]),
MdVersion(keys=[2], threshold=1, sigs=[3, 2, 1, 4]),
MdVersion(keys=[2], threshold=1, sigs=[3, 2, 4]),
],
"1-of-1 key rotation fail: not signed with old key": [
RootVersion(keys=[1], threshold=1, sigs=[1]),
RootVersion(keys=[2], threshold=1, sigs=[2, 3, 4], res=UnsignedMetadataError),
MdVersion(keys=[1], threshold=1, sigs=[1]),
MdVersion(keys=[2], threshold=1, sigs=[2, 3, 4], res=UnsignedMetadataError),
],
"1-of-1 key rotation fail: not signed with new key": [
RootVersion(keys=[1], threshold=1, sigs=[1]),
RootVersion(keys=[2], threshold=1, sigs=[1, 3, 4], res=UnsignedMetadataError),
MdVersion(keys=[1], threshold=1, sigs=[1]),
MdVersion(keys=[2], threshold=1, sigs=[1, 3, 4], res=UnsignedMetadataError),
],
"3-of-5, sign with different keycombos": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 4, 1]),
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 1, 3]),
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 1, 3]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 4, 1]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 1, 3]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 1, 3]),
],
"3-of-5, one key rotated": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 1]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 1]),
],
"3-of-5, one key rotate fails: not signed with 3 new keys": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 2, 4], res=UnsignedMetadataError),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 2, 4], res=UnsignedMetadataError),
],
"3-of-5, one key rotate fails: not signed with 3 old keys": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 5], res=UnsignedMetadataError),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 5], res=UnsignedMetadataError),
],
"3-of-5, one key rotated, with intermediate step": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 2, 4, 5]),
RootVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 5]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 2, 4, 5]),
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4, 5]),
],
"3-of-5, all keys rotated, with intermediate step": [
RootVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
RootVersion(keys=[5, 6, 7, 8, 9], threshold=3, sigs=[0, 2, 4, 5, 6, 7]),
RootVersion(keys=[5, 6, 7, 8, 9], threshold=3, sigs=[5, 6, 7]),
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
MdVersion(keys=[5, 6, 7, 8, 9], threshold=3, sigs=[0, 2, 4, 5, 6, 7]),
MdVersion(keys=[5, 6, 7, 8, 9], threshold=3, sigs=[5, 6, 7]),
],
"1-of-3 threshold increase to 2-of-3": [
RootVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
RootVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
MdVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
MdVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
],
"1-of-3 threshold bump to 2-of-3 fails: new threshold not reached": [
RootVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
RootVersion(keys=[1, 2, 3], threshold=2, sigs=[2], res=UnsignedMetadataError),
MdVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
MdVersion(keys=[1, 2, 3], threshold=2, sigs=[2], res=UnsignedMetadataError),
],
"2-of-3 threshold decrease to 1-of-3": [
RootVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
RootVersion(keys=[1, 2, 3], threshold=1, sigs=[1, 2]),
RootVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
MdVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
MdVersion(keys=[1, 2, 3], threshold=1, sigs=[1, 2]),
MdVersion(keys=[1, 2, 3], threshold=1, sigs=[1]),
],
"2-of-3 threshold decr. to 1-of-3 fails: old threshold not reached": [
RootVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
RootVersion(keys=[1, 2, 3], threshold=1, sigs=[1], res=UnsignedMetadataError),
MdVersion(keys=[1, 2, 3], threshold=2, sigs=[1, 2]),
MdVersion(keys=[1, 2, 3], threshold=1, sigs=[1], res=UnsignedMetadataError),
],
"1-of-2 threshold increase to 2-of-2": [
RootVersion(keys=[1], threshold=1, sigs=[1]),
RootVersion(keys=[1, 2], threshold=2, sigs=[1, 2]),
MdVersion(keys=[1], threshold=1, sigs=[1]),
MdVersion(keys=[1, 2], threshold=2, sigs=[1, 2]),
],
}
# fmt: on
@run_sub_tests_with_dataset(root_rotation_cases)
def test_root_rotation(self, root_versions: List[RootVersion]) -> None:
def test_root_rotation(self, root_versions: List[MdVersion]) -> None:
"""Test Updater.refresh() with various sequences of root updates
Each RootVersion in the list describes root keys and signatures of a
Each MdVersion in the list describes root keys and signatures of a
remote root metadata version. As an example:
RootVersion([1,2,3], 2, [1,2])
MdVersion([1,2,3], 2, [1,2])
defines a root that contains keys 1, 2 and 3 with threshold 2. The
metadata is signed with keys 1 and 2.
@ -188,13 +187,13 @@ def test_root_rotation(self, root_versions: List[RootVersion]) -> None:
self.sim.publish_root()
# run client workflow, assert success/failure
expected_result = root_versions[-1].res
if expected_result is None:
expected_error = root_versions[-1].res
if expected_error is None:
self._run_refresh()
expected_local_root = self.sim.signed_roots[-1]
else:
# failure expected: local root should be the root before last
with self.assertRaises(expected_result):
with self.assertRaises(expected_error):
self._run_refresh()
expected_local_root = self.sim.signed_roots[-2]
@ -202,6 +201,79 @@ def test_root_rotation(self, root_versions: List[RootVersion]) -> None:
with open(os.path.join(self.metadata_dir, "root.json"), "rb") as f:
self.assertEqual(f.read(), expected_local_root)
# fmt: off
non_root_rotation_cases: Dict[str, MdVersion] = {
"1-of-1 key rotation":
MdVersion(keys=[2], threshold=1, sigs=[2]),
"1-of-1 key rotation, unused signatures":
MdVersion(keys=[1], threshold=1, sigs=[3, 1, 4]),
"1-of-1 key rotation fail: not signed with new key":
MdVersion(keys=[2], threshold=1, sigs=[1, 3, 4], res=UnsignedMetadataError),
"3-of-5, one key signature wrong: not signed with 3 expected keys":
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 2, 4], res=UnsignedMetadataError),
"2-of-5, one key signature mising: threshold not reached":
MdVersion(keys=[0, 1, 3, 4, 5], threshold=3, sigs=[0, 4], res=UnsignedMetadataError),
"3-of-5, sign first combo":
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 2, 4]),
"3-of-5, sign second combo":
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 4, 1]),
"3-of-5, sign third combo":
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[0, 1, 3]),
"3-of-5, sign fourth combo":
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[1, 2, 3]),
"3-of-5, sign fifth combo":
MdVersion(keys=[0, 1, 2, 3, 4], threshold=3, sigs=[2, 3, 4]),
}
# fmt: on
@run_sub_tests_with_dataset(non_root_rotation_cases)
def test_non_root_rotations(self, md_version: MdVersion) -> None:
"""Test Updater.refresh() with various sequences of metadata updates
Each MdVersion in the list describes metadata keys and signatures
of a remote metadata version. As an example:
MdVersion([1,2,3], 2, [1,2])
defines a metadata that contains keys 1, 2 and 3 with threshold 2. The
metadata is signed with keys 1 and 2.
Assert that refresh() result is expected and that local metadata on disk
is the expected one after all roots have been loaded from remote using
the standard client update workflow.
"""
self.setup_subtest()
roles = ["timestamp", "snapshot", "targets"]
for role in roles:
# clear role keys, signers
self.sim.root.roles[role].keyids.clear()
self.sim.signers[role].clear()
self.sim.root.roles[role].threshold = md_version.threshold
for i in md_version.keys:
self.sim.root.add_key(role, self.keys[i])
for i in md_version.sigs:
self.sim.add_signer(role, self.signers[i])
self.sim.root.version += 1
self.sim.publish_root()
# run client workflow, assert success/failure
expected_error = md_version.res
if expected_error is None:
self._run_refresh()
# Call fetch_metadata to sign metadata with new keys
expected_local_md: Metadata = self.sim._fetch_metadata(role)
# assert local metadata role is on disk as expected
md_path = os.path.join(self.metadata_dir, f"{role}.json")
with open(md_path, "rb") as f:
data = f.read()
self.assertEqual(data, expected_local_md)
else:
# failure expected
with self.assertRaises(expected_error):
self._run_refresh()
if __name__ == "__main__":
if "--dump" in sys.argv:

View file

@ -60,6 +60,8 @@ def real_decorator(
def wrapper(test_cls: unittest.TestCase) -> None:
for case, data in dataset.items():
with test_cls.subTest(case=case):
# Save case name for future reference
test_cls.case_name = case.replace(" ", "_")
function(test_cls, data)
return wrapper