10
10
import tempfile
11
11
import unittest
12
12
from dataclasses import dataclass
13
- from typing import List , Optional , Type
13
+ from typing import Dict , List , Optional , Type
14
14
15
15
from securesystemslib .signer import SSlibSigner
16
16
17
17
from tests import utils
18
18
from tests .repository_simulator import RepositorySimulator
19
19
from tests .utils import run_sub_tests_with_dataset
20
- from tuf .api .metadata import Key , Root
20
+ from tuf .api .metadata import Key , Metadata , Root
21
21
from tuf .exceptions import UnsignedMetadataError
22
22
from tuf .ngclient import Updater
23
23
24
24
25
25
@dataclass
26
- class RootVersion :
26
+ class MdVersion :
27
27
keys : List [int ]
28
28
threshold : int
29
29
sigs : List [int ]
@@ -36,35 +36,34 @@ class TestUpdaterKeyRotations(unittest.TestCase):
36
36
# set dump_dir to trigger repository state dumps
37
37
dump_dir : Optional [str ] = None
38
38
39
- def setUp ( self ) -> None :
40
- self . sim : RepositorySimulator
41
- self . metadata_dir : str
42
- self . subtest_count = 0
39
+ @ classmethod
40
+ def setUpClass ( cls ) -> None :
41
+ cls . sim : RepositorySimulator
42
+ cls . metadata_dir : str
43
43
# pylint: disable-next=consider-using-with
44
- self .temp_dir = tempfile .TemporaryDirectory ()
44
+ cls .temp_dir = tempfile .TemporaryDirectory ()
45
45
46
46
# Pre-create a bunch of keys and signers
47
- self .keys : List [Key ] = []
48
- self .signers : List [SSlibSigner ] = []
47
+ cls .keys : List [Key ] = []
48
+ cls .signers : List [SSlibSigner ] = []
49
49
for _ in range (10 ):
50
50
key , signer = RepositorySimulator .create_key ()
51
- self .keys .append (key )
52
- self .signers .append (signer )
51
+ cls .keys .append (key )
52
+ cls .signers .append (signer )
53
53
54
- def tearDown (self ) -> None :
55
- self .temp_dir .cleanup ()
54
+ @classmethod
55
+ def tearDownClass (cls ) -> None :
56
+ cls .temp_dir .cleanup ()
56
57
57
58
def setup_subtest (self ) -> None :
58
- self .subtest_count += 1
59
-
60
59
# Setup repository for subtest: make sure no roots have been published
61
60
self .sim = RepositorySimulator ()
62
61
self .sim .signed_roots .clear ()
63
62
self .sim .root .version = 0
64
63
65
64
if self .dump_dir is not None :
66
65
# create subtest dumpdir
67
- name = f"{ self .id ().split ('.' )[- 1 ]} -{ self .subtest_count } "
66
+ name = f"{ self .id ().split ('.' )[- 1 ]} -{ self .case_name } "
68
67
self .sim .dump_dir = os .path .join (self .dump_dir , name )
69
68
os .mkdir (self .sim .dump_dir )
70
69
@@ -88,82 +87,82 @@ def _run_refresh(self) -> None:
88
87
# fmt: off
89
88
root_rotation_cases = {
90
89
"1-of-1 key rotation" : [
91
- RootVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
92
- RootVersion (keys = [2 ], threshold = 1 , sigs = [2 , 1 ]),
93
- RootVersion (keys = [2 ], threshold = 1 , sigs = [2 ]),
90
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
91
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [2 , 1 ]),
92
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [2 ]),
94
93
],
95
94
"1-of-1 key rotation, unused signatures" : [
96
- RootVersion (keys = [1 ], threshold = 1 , sigs = [3 , 1 , 4 ]),
97
- RootVersion (keys = [2 ], threshold = 1 , sigs = [3 , 2 , 1 , 4 ]),
98
- RootVersion (keys = [2 ], threshold = 1 , sigs = [3 , 2 , 4 ]),
95
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [3 , 1 , 4 ]),
96
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [3 , 2 , 1 , 4 ]),
97
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [3 , 2 , 4 ]),
99
98
],
100
99
"1-of-1 key rotation fail: not signed with old key" : [
101
- RootVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
102
- RootVersion (keys = [2 ], threshold = 1 , sigs = [2 , 3 , 4 ], res = UnsignedMetadataError ),
100
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
101
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [2 , 3 , 4 ], res = UnsignedMetadataError ),
103
102
],
104
103
"1-of-1 key rotation fail: not signed with new key" : [
105
- RootVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
106
- RootVersion (keys = [2 ], threshold = 1 , sigs = [1 , 3 , 4 ], res = UnsignedMetadataError ),
104
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
105
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [1 , 3 , 4 ], res = UnsignedMetadataError ),
107
106
],
108
107
"3-of-5, sign with different keycombos" : [
109
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
110
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 4 , 1 ]),
111
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 1 , 3 ]),
112
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 1 , 3 ]),
108
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
109
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 4 , 1 ]),
110
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 1 , 3 ]),
111
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 1 , 3 ]),
113
112
],
114
113
"3-of-5, one key rotated" : [
115
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
116
- RootVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 1 ]),
114
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
115
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 1 ]),
117
116
],
118
117
"3-of-5, one key rotate fails: not signed with 3 new keys" : [
119
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
120
- RootVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 2 , 4 ], res = UnsignedMetadataError ),
118
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
119
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 2 , 4 ], res = UnsignedMetadataError ),
121
120
],
122
121
"3-of-5, one key rotate fails: not signed with 3 old keys" : [
123
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
124
- RootVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 5 ], res = UnsignedMetadataError ),
122
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
123
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 5 ], res = UnsignedMetadataError ),
125
124
],
126
125
"3-of-5, one key rotated, with intermediate step" : [
127
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
128
- RootVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 2 , 4 , 5 ]),
129
- RootVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 5 ]),
126
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
127
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 2 , 4 , 5 ]),
128
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 , 5 ]),
130
129
],
131
130
"3-of-5, all keys rotated, with intermediate step" : [
132
- RootVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
133
- RootVersion (keys = [5 , 6 , 7 , 8 , 9 ], threshold = 3 , sigs = [0 , 2 , 4 , 5 , 6 , 7 ]),
134
- RootVersion (keys = [5 , 6 , 7 , 8 , 9 ], threshold = 3 , sigs = [5 , 6 , 7 ]),
131
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
132
+ MdVersion (keys = [5 , 6 , 7 , 8 , 9 ], threshold = 3 , sigs = [0 , 2 , 4 , 5 , 6 , 7 ]),
133
+ MdVersion (keys = [5 , 6 , 7 , 8 , 9 ], threshold = 3 , sigs = [5 , 6 , 7 ]),
135
134
],
136
135
"1-of-3 threshold increase to 2-of-3" : [
137
- RootVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
138
- RootVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
136
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
137
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
139
138
],
140
139
"1-of-3 threshold bump to 2-of-3 fails: new threshold not reached" : [
141
- RootVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
142
- RootVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [2 ], res = UnsignedMetadataError ),
140
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
141
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [2 ], res = UnsignedMetadataError ),
143
142
],
144
143
"2-of-3 threshold decrease to 1-of-3" : [
145
- RootVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
146
- RootVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 , 2 ]),
147
- RootVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
144
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
145
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 , 2 ]),
146
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ]),
148
147
],
149
148
"2-of-3 threshold decr. to 1-of-3 fails: old threshold not reached" : [
150
- RootVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
151
- RootVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ], res = UnsignedMetadataError ),
149
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 2 , sigs = [1 , 2 ]),
150
+ MdVersion (keys = [1 , 2 , 3 ], threshold = 1 , sigs = [1 ], res = UnsignedMetadataError ),
152
151
],
153
152
"1-of-2 threshold increase to 2-of-2" : [
154
- RootVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
155
- RootVersion (keys = [1 , 2 ], threshold = 2 , sigs = [1 , 2 ]),
153
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [1 ]),
154
+ MdVersion (keys = [1 , 2 ], threshold = 2 , sigs = [1 , 2 ]),
156
155
],
157
156
}
158
157
# fmt: on
159
158
160
159
@run_sub_tests_with_dataset (root_rotation_cases )
161
- def test_root_rotation (self , root_versions : List [RootVersion ]) -> None :
160
+ def test_root_rotation (self , root_versions : List [MdVersion ]) -> None :
162
161
"""Test Updater.refresh() with various sequences of root updates
163
162
164
- Each RootVersion in the list describes root keys and signatures of a
163
+ Each MdVersion in the list describes root keys and signatures of a
165
164
remote root metadata version. As an example:
166
- RootVersion ([1,2,3], 2, [1,2])
165
+ MdVersion ([1,2,3], 2, [1,2])
167
166
defines a root that contains keys 1, 2 and 3 with threshold 2. The
168
167
metadata is signed with keys 1 and 2.
169
168
@@ -188,20 +187,93 @@ def test_root_rotation(self, root_versions: List[RootVersion]) -> None:
188
187
self .sim .publish_root ()
189
188
190
189
# run client workflow, assert success/failure
191
- expected_result = root_versions [- 1 ].res
192
- if expected_result is None :
190
+ expected_error = root_versions [- 1 ].res
191
+ if expected_error is None :
193
192
self ._run_refresh ()
194
193
expected_local_root = self .sim .signed_roots [- 1 ]
195
194
else :
196
195
# failure expected: local root should be the root before last
197
- with self .assertRaises (expected_result ):
196
+ with self .assertRaises (expected_error ):
198
197
self ._run_refresh ()
199
198
expected_local_root = self .sim .signed_roots [- 2 ]
200
199
201
200
# assert local root on disk is expected
202
201
with open (os .path .join (self .metadata_dir , "root.json" ), "rb" ) as f :
203
202
self .assertEqual (f .read (), expected_local_root )
204
203
204
+ # fmt: off
205
+ non_root_rotation_cases : Dict [str , MdVersion ] = {
206
+ "1-of-1 key rotation" :
207
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [2 ]),
208
+ "1-of-1 key rotation, unused signatures" :
209
+ MdVersion (keys = [1 ], threshold = 1 , sigs = [3 , 1 , 4 ]),
210
+ "1-of-1 key rotation fail: not signed with new key" :
211
+ MdVersion (keys = [2 ], threshold = 1 , sigs = [1 , 3 , 4 ], res = UnsignedMetadataError ),
212
+ "3-of-5, one key signature wrong: not signed with 3 expected keys" :
213
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 2 , 4 ], res = UnsignedMetadataError ),
214
+ "2-of-5, one key signature mising: threshold not reached" :
215
+ MdVersion (keys = [0 , 1 , 3 , 4 , 5 ], threshold = 3 , sigs = [0 , 4 ], res = UnsignedMetadataError ),
216
+ "3-of-5, sign first combo" :
217
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 2 , 4 ]),
218
+ "3-of-5, sign second combo" :
219
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 4 , 1 ]),
220
+ "3-of-5, sign third combo" :
221
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [0 , 1 , 3 ]),
222
+ "3-of-5, sign fourth combo" :
223
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [1 , 2 , 3 ]),
224
+ "3-of-5, sign fifth combo" :
225
+ MdVersion (keys = [0 , 1 , 2 , 3 , 4 ], threshold = 3 , sigs = [2 , 3 , 4 ]),
226
+ }
227
+ # fmt: on
228
+
229
+ @run_sub_tests_with_dataset (non_root_rotation_cases )
230
+ def test_non_root_rotations (self , md_version : MdVersion ) -> None :
231
+ """Test Updater.refresh() with various sequences of metadata updates
232
+
233
+ Each MdVersion in the list describes metadata keys and signatures
234
+ of a remote metadata version. As an example:
235
+ MdVersion([1,2,3], 2, [1,2])
236
+ defines a metadata that contains keys 1, 2 and 3 with threshold 2. The
237
+ metadata is signed with keys 1 and 2.
238
+
239
+ Assert that refresh() result is expected and that local metadata on disk
240
+ is the expected one after all roots have been loaded from remote using
241
+ the standard client update workflow.
242
+ """
243
+ self .setup_subtest ()
244
+ roles = ["timestamp" , "snapshot" , "targets" ]
245
+ for role in roles :
246
+
247
+ # clear role keys, signers
248
+ self .sim .root .roles [role ].keyids .clear ()
249
+ self .sim .signers [role ].clear ()
250
+
251
+ self .sim .root .roles [role ].threshold = md_version .threshold
252
+ for i in md_version .keys :
253
+ self .sim .root .add_key (role , self .keys [i ])
254
+
255
+ for i in md_version .sigs :
256
+ self .sim .add_signer (role , self .signers [i ])
257
+
258
+ self .sim .root .version += 1
259
+ self .sim .publish_root ()
260
+
261
+ # run client workflow, assert success/failure
262
+ expected_error = md_version .res
263
+ if expected_error is None :
264
+ self ._run_refresh ()
265
+
266
+ # Call fetch_metadata to sign metadata with new keys
267
+ expected_local_md : Metadata = self .sim ._fetch_metadata (role )
268
+ # assert local metadata role is on disk as expected
269
+ md_path = os .path .join (self .metadata_dir , f"{ role } .json" )
270
+ with open (md_path , "rb" ) as f :
271
+ data = f .read ()
272
+ self .assertEqual (data , expected_local_md )
273
+ else :
274
+ # failure expected
275
+ with self .assertRaises (expected_error ):
276
+ self ._run_refresh ()
205
277
206
278
if __name__ == "__main__" :
207
279
if "--dump" in sys .argv :
0 commit comments