1 | """ |
---|
2 | Tests for the grid manager. |
---|
3 | """ |
---|
4 | |
---|
5 | from datetime import ( |
---|
6 | timedelta, |
---|
7 | ) |
---|
8 | |
---|
9 | from twisted.python.filepath import ( |
---|
10 | FilePath, |
---|
11 | ) |
---|
12 | |
---|
13 | from hypothesis import given |
---|
14 | |
---|
15 | from allmydata.node import ( |
---|
16 | config_from_string, |
---|
17 | ) |
---|
18 | from allmydata.client import ( |
---|
19 | _valid_config as client_valid_config, |
---|
20 | ) |
---|
21 | from allmydata.crypto import ( |
---|
22 | ed25519, |
---|
23 | ) |
---|
24 | from allmydata.util import ( |
---|
25 | jsonbytes as json, |
---|
26 | ) |
---|
27 | from allmydata.grid_manager import ( |
---|
28 | load_grid_manager, |
---|
29 | save_grid_manager, |
---|
30 | create_grid_manager, |
---|
31 | parse_grid_manager_certificate, |
---|
32 | create_grid_manager_verifier, |
---|
33 | SignedCertificate, |
---|
34 | ) |
---|
35 | from allmydata.test.strategies import ( |
---|
36 | base32text, |
---|
37 | ) |
---|
38 | |
---|
39 | from .common import SyncTestCase |
---|
40 | |
---|
41 | |
---|
42 | class GridManagerUtilities(SyncTestCase): |
---|
43 | """ |
---|
44 | Confirm operation of utility functions used by GridManager |
---|
45 | """ |
---|
46 | |
---|
47 | def test_load_certificates(self): |
---|
48 | """ |
---|
49 | Grid Manager certificates are deserialized from config properly |
---|
50 | """ |
---|
51 | cert_path = self.mktemp() |
---|
52 | fake_cert = { |
---|
53 | "certificate": "{\"expires\":1601687822,\"public_key\":\"pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga\",\"version\":1}", |
---|
54 | "signature": "fvjd3uvvupf2v6tnvkwjd473u3m3inyqkwiclhp7balmchkmn3px5pei3qyfjnhymq4cjcwvbpqmcwwnwswdtrfkpnlaxuih2zbdmda" |
---|
55 | } |
---|
56 | with open(cert_path, "wb") as f: |
---|
57 | f.write(json.dumps_bytes(fake_cert)) |
---|
58 | config_data = ( |
---|
59 | "[grid_managers]\n" |
---|
60 | "fluffy = pub-v0-vqimc4s5eflwajttsofisp5st566dbq36xnpp4siz57ufdavpvlq\n" |
---|
61 | "[grid_manager_certificates]\n" |
---|
62 | "ding = {}\n".format(cert_path) |
---|
63 | ) |
---|
64 | config = config_from_string("/foo", "portnum", config_data, client_valid_config()) |
---|
65 | self.assertEqual( |
---|
66 | {"fluffy": "pub-v0-vqimc4s5eflwajttsofisp5st566dbq36xnpp4siz57ufdavpvlq"}, |
---|
67 | config.enumerate_section("grid_managers") |
---|
68 | ) |
---|
69 | certs = config.get_grid_manager_certificates() |
---|
70 | self.assertEqual([fake_cert], certs) |
---|
71 | |
---|
72 | def test_load_certificates_invalid_version(self): |
---|
73 | """ |
---|
74 | An error is reported loading invalid certificate version |
---|
75 | """ |
---|
76 | gm_path = FilePath(self.mktemp()) |
---|
77 | gm_path.makedirs() |
---|
78 | config = { |
---|
79 | "grid_manager_config_version": 0, |
---|
80 | "private_key": "priv-v0-ub7knkkmkptqbsax4tznymwzc4nk5lynskwjsiubmnhcpd7lvlqa", |
---|
81 | "storage_servers": { |
---|
82 | "radia": { |
---|
83 | "public_key": "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
---|
84 | } |
---|
85 | } |
---|
86 | } |
---|
87 | with gm_path.child("config.json").open("wb") as f: |
---|
88 | f.write(json.dumps_bytes(config)) |
---|
89 | |
---|
90 | fake_cert = { |
---|
91 | "certificate": "{\"expires\":1601687822,\"public_key\":\"pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga\",\"version\":22}", |
---|
92 | "signature": "fvjd3uvvupf2v6tnvkwjd473u3m3inyqkwiclhp7balmchkmn3px5pei3qyfjnhymq4cjcwvbpqmcwwnwswdtrfkpnlaxuih2zbdmda" |
---|
93 | } |
---|
94 | with gm_path.child("radia.cert.0").open("wb") as f: |
---|
95 | f.write(json.dumps_bytes(fake_cert)) |
---|
96 | |
---|
97 | with self.assertRaises(ValueError) as ctx: |
---|
98 | load_grid_manager(gm_path) |
---|
99 | self.assertIn( |
---|
100 | "22", |
---|
101 | str(ctx.exception), |
---|
102 | ) |
---|
103 | |
---|
104 | def test_load_certificates_unknown_key(self): |
---|
105 | """ |
---|
106 | An error is reported loading certificates with invalid keys in them |
---|
107 | """ |
---|
108 | cert_path = self.mktemp() |
---|
109 | fake_cert = { |
---|
110 | "certificate": "{\"expires\":1601687822,\"public_key\":\"pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga\",\"version\":22}", |
---|
111 | "signature": "fvjd3uvvupf2v6tnvkwjd473u3m3inyqkwiclhp7balmchkmn3px5pei3qyfjnhymq4cjcwvbpqmcwwnwswdtrfkpnlaxuih2zbdmda", |
---|
112 | "something-else": "not valid in a v0 certificate" |
---|
113 | } |
---|
114 | with open(cert_path, "wb") as f: |
---|
115 | f.write(json.dumps_bytes(fake_cert)) |
---|
116 | config_data = ( |
---|
117 | "[grid_manager_certificates]\n" |
---|
118 | "ding = {}\n".format(cert_path) |
---|
119 | ) |
---|
120 | config = config_from_string("/foo", "portnum", config_data, client_valid_config()) |
---|
121 | with self.assertRaises(ValueError) as ctx: |
---|
122 | config.get_grid_manager_certificates() |
---|
123 | |
---|
124 | self.assertIn( |
---|
125 | "Unknown key in Grid Manager certificate", |
---|
126 | str(ctx.exception) |
---|
127 | ) |
---|
128 | |
---|
129 | def test_load_certificates_missing(self): |
---|
130 | """ |
---|
131 | An error is reported for missing certificates |
---|
132 | """ |
---|
133 | cert_path = self.mktemp() |
---|
134 | config_data = ( |
---|
135 | "[grid_managers]\n" |
---|
136 | "fluffy = pub-v0-vqimc4s5eflwajttsofisp5st566dbq36xnpp4siz57ufdavpvlq\n" |
---|
137 | "[grid_manager_certificates]\n" |
---|
138 | "ding = {}\n".format(cert_path) |
---|
139 | ) |
---|
140 | config = config_from_string("/foo", "portnum", config_data, client_valid_config()) |
---|
141 | with self.assertRaises(ValueError) as ctx: |
---|
142 | config.get_grid_manager_certificates() |
---|
143 | # we don't reliably know how Windows or MacOS will represent |
---|
144 | # the path in the exception, so we don't check for the *exact* |
---|
145 | # message with full-path here.. |
---|
146 | self.assertIn( |
---|
147 | "Grid Manager certificate file", |
---|
148 | str(ctx.exception) |
---|
149 | ) |
---|
150 | self.assertIn( |
---|
151 | " doesn't exist", |
---|
152 | str(ctx.exception) |
---|
153 | ) |
---|
154 | |
---|
155 | |
---|
156 | class GridManagerVerifier(SyncTestCase): |
---|
157 | """ |
---|
158 | Tests related to rejecting or accepting Grid Manager certificates. |
---|
159 | """ |
---|
160 | |
---|
161 | def setUp(self): |
---|
162 | self.gm = create_grid_manager() |
---|
163 | return super(GridManagerVerifier, self).setUp() |
---|
164 | |
---|
165 | def test_sign_cert(self): |
---|
166 | """ |
---|
167 | For a storage server previously added to a grid manager, |
---|
168 | _GridManager.sign returns a dict with "certificate" and |
---|
169 | "signature" properties where the value of "signature" gives |
---|
170 | the ed25519 signature (using the grid manager's private key of |
---|
171 | the value) of "certificate". |
---|
172 | """ |
---|
173 | priv, pub = ed25519.create_signing_keypair() |
---|
174 | self.gm.add_storage_server("test", pub) |
---|
175 | cert0 = self.gm.sign("test", timedelta(seconds=86400)) |
---|
176 | cert1 = self.gm.sign("test", timedelta(seconds=3600)) |
---|
177 | self.assertNotEqual(cert0, cert1) |
---|
178 | |
---|
179 | self.assertIsInstance(cert0, SignedCertificate) |
---|
180 | gm_key = ed25519.verifying_key_from_string(self.gm.public_identity()) |
---|
181 | self.assertEqual( |
---|
182 | ed25519.verify_signature( |
---|
183 | gm_key, |
---|
184 | cert0.signature, |
---|
185 | cert0.certificate, |
---|
186 | ), |
---|
187 | None |
---|
188 | ) |
---|
189 | |
---|
190 | def test_sign_cert_wrong_name(self): |
---|
191 | """ |
---|
192 | Try to sign a storage-server that doesn't exist |
---|
193 | """ |
---|
194 | with self.assertRaises(KeyError): |
---|
195 | self.gm.sign("doesn't exist", timedelta(seconds=86400)) |
---|
196 | |
---|
197 | def test_add_cert(self): |
---|
198 | """ |
---|
199 | Add a storage-server and serialize it |
---|
200 | """ |
---|
201 | priv, pub = ed25519.create_signing_keypair() |
---|
202 | self.gm.add_storage_server("test", pub) |
---|
203 | |
---|
204 | data = self.gm.marshal() |
---|
205 | self.assertEqual( |
---|
206 | data["storage_servers"], |
---|
207 | { |
---|
208 | "test": { |
---|
209 | "public_key": ed25519.string_from_verifying_key(pub), |
---|
210 | } |
---|
211 | } |
---|
212 | ) |
---|
213 | |
---|
214 | def test_remove(self): |
---|
215 | """ |
---|
216 | Add then remove a storage-server |
---|
217 | """ |
---|
218 | priv, pub = ed25519.create_signing_keypair() |
---|
219 | self.gm.add_storage_server("test", pub) |
---|
220 | self.gm.remove_storage_server("test") |
---|
221 | self.assertEqual(len(self.gm.storage_servers), 0) |
---|
222 | |
---|
223 | def test_serialize(self): |
---|
224 | """ |
---|
225 | Write and then read a Grid Manager config |
---|
226 | """ |
---|
227 | priv0, pub0 = ed25519.create_signing_keypair() |
---|
228 | priv1, pub1 = ed25519.create_signing_keypair() |
---|
229 | self.gm.add_storage_server("test0", pub0) |
---|
230 | self.gm.add_storage_server("test1", pub1) |
---|
231 | |
---|
232 | tempdir = self.mktemp() |
---|
233 | fp = FilePath(tempdir) |
---|
234 | |
---|
235 | save_grid_manager(fp, self.gm) |
---|
236 | gm2 = load_grid_manager(fp) |
---|
237 | self.assertEqual( |
---|
238 | self.gm.public_identity(), |
---|
239 | gm2.public_identity(), |
---|
240 | ) |
---|
241 | self.assertEqual( |
---|
242 | len(self.gm.storage_servers), |
---|
243 | len(gm2.storage_servers), |
---|
244 | ) |
---|
245 | for name, ss0 in list(self.gm.storage_servers.items()): |
---|
246 | ss1 = gm2.storage_servers[name] |
---|
247 | self.assertEqual(ss0.name, ss1.name) |
---|
248 | self.assertEqual(ss0.public_key_string(), ss1.public_key_string()) |
---|
249 | self.assertEqual(self.gm.marshal(), gm2.marshal()) |
---|
250 | |
---|
251 | def test_invalid_no_version(self): |
---|
252 | """ |
---|
253 | Invalid Grid Manager config with no version |
---|
254 | """ |
---|
255 | tempdir = self.mktemp() |
---|
256 | fp = FilePath(tempdir) |
---|
257 | bad_config = { |
---|
258 | "private_key": "at least we have one", |
---|
259 | } |
---|
260 | fp.makedirs() |
---|
261 | with fp.child("config.json").open("w") as f: |
---|
262 | f.write(json.dumps_bytes(bad_config)) |
---|
263 | |
---|
264 | with self.assertRaises(ValueError) as ctx: |
---|
265 | load_grid_manager(fp) |
---|
266 | self.assertIn( |
---|
267 | "unknown version", |
---|
268 | str(ctx.exception), |
---|
269 | ) |
---|
270 | |
---|
271 | def test_invalid_certificate_bad_version(self): |
---|
272 | """ |
---|
273 | Invalid Grid Manager config containing a certificate with an |
---|
274 | illegal version |
---|
275 | """ |
---|
276 | tempdir = self.mktemp() |
---|
277 | fp = FilePath(tempdir) |
---|
278 | config = { |
---|
279 | "grid_manager_config_version": 0, |
---|
280 | "private_key": "priv-v0-ub7knkkmkptqbsax4tznymwzc4nk5lynskwjsiubmnhcpd7lvlqa", |
---|
281 | "storage_servers": { |
---|
282 | "alice": { |
---|
283 | "public_key": "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
---|
284 | } |
---|
285 | } |
---|
286 | } |
---|
287 | bad_cert = { |
---|
288 | "certificate": "{\"expires\":1601687822,\"public_key\":\"pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga\",\"version\":0}", |
---|
289 | "signature": "fvjd3uvvupf2v6tnvkwjd473u3m3inyqkwiclhp7balmchkmn3px5pei3qyfjnhymq4cjcwvbpqmcwwnwswdtrfkpnlaxuih2zbdmda" |
---|
290 | } |
---|
291 | |
---|
292 | fp.makedirs() |
---|
293 | with fp.child("config.json").open("w") as f: |
---|
294 | f.write(json.dumps_bytes(config)) |
---|
295 | with fp.child("alice.cert.0").open("w") as f: |
---|
296 | f.write(json.dumps_bytes(bad_cert)) |
---|
297 | |
---|
298 | with self.assertRaises(ValueError) as ctx: |
---|
299 | load_grid_manager(fp) |
---|
300 | |
---|
301 | self.assertIn( |
---|
302 | "Unknown certificate version", |
---|
303 | str(ctx.exception), |
---|
304 | ) |
---|
305 | |
---|
306 | def test_invalid_no_private_key(self): |
---|
307 | """ |
---|
308 | Invalid Grid Manager config with no private key |
---|
309 | """ |
---|
310 | tempdir = self.mktemp() |
---|
311 | fp = FilePath(tempdir) |
---|
312 | bad_config = { |
---|
313 | "grid_manager_config_version": 0, |
---|
314 | } |
---|
315 | fp.makedirs() |
---|
316 | with fp.child("config.json").open("w") as f: |
---|
317 | f.write(json.dumps_bytes(bad_config)) |
---|
318 | |
---|
319 | with self.assertRaises(ValueError) as ctx: |
---|
320 | load_grid_manager(fp) |
---|
321 | self.assertIn( |
---|
322 | "'private_key' required", |
---|
323 | str(ctx.exception), |
---|
324 | ) |
---|
325 | |
---|
326 | def test_invalid_bad_private_key(self): |
---|
327 | """ |
---|
328 | Invalid Grid Manager config with bad private-key |
---|
329 | """ |
---|
330 | tempdir = self.mktemp() |
---|
331 | fp = FilePath(tempdir) |
---|
332 | bad_config = { |
---|
333 | "grid_manager_config_version": 0, |
---|
334 | "private_key": "not actually encoded key", |
---|
335 | } |
---|
336 | fp.makedirs() |
---|
337 | with fp.child("config.json").open("w") as f: |
---|
338 | f.write(json.dumps_bytes(bad_config)) |
---|
339 | |
---|
340 | with self.assertRaises(ValueError) as ctx: |
---|
341 | load_grid_manager(fp) |
---|
342 | self.assertIn( |
---|
343 | "Invalid Grid Manager private_key", |
---|
344 | str(ctx.exception), |
---|
345 | ) |
---|
346 | |
---|
347 | def test_invalid_storage_server(self): |
---|
348 | """ |
---|
349 | Invalid Grid Manager config with missing public-key for |
---|
350 | storage-server |
---|
351 | """ |
---|
352 | tempdir = self.mktemp() |
---|
353 | fp = FilePath(tempdir) |
---|
354 | bad_config = { |
---|
355 | "grid_manager_config_version": 0, |
---|
356 | "private_key": "priv-v0-ub7knkkmkptqbsax4tznymwzc4nk5lynskwjsiubmnhcpd7lvlqa", |
---|
357 | "storage_servers": { |
---|
358 | "bad": {} |
---|
359 | } |
---|
360 | } |
---|
361 | fp.makedirs() |
---|
362 | with fp.child("config.json").open("w") as f: |
---|
363 | f.write(json.dumps_bytes(bad_config)) |
---|
364 | |
---|
365 | with self.assertRaises(ValueError) as ctx: |
---|
366 | load_grid_manager(fp) |
---|
367 | self.assertIn( |
---|
368 | "No 'public_key' for storage server", |
---|
369 | str(ctx.exception), |
---|
370 | ) |
---|
371 | |
---|
372 | def test_parse_cert(self): |
---|
373 | """ |
---|
374 | Parse an ostensibly valid storage certificate |
---|
375 | """ |
---|
376 | js = parse_grid_manager_certificate('{"certificate": "", "signature": ""}') |
---|
377 | self.assertEqual( |
---|
378 | set(js.keys()), |
---|
379 | {"certificate", "signature"} |
---|
380 | ) |
---|
381 | # the signature isn't *valid*, but that's checked in a |
---|
382 | # different function |
---|
383 | |
---|
384 | def test_parse_cert_not_dict(self): |
---|
385 | """ |
---|
386 | Certificate data not even a dict |
---|
387 | """ |
---|
388 | with self.assertRaises(ValueError) as ctx: |
---|
389 | parse_grid_manager_certificate("[]") |
---|
390 | self.assertIn( |
---|
391 | "must be a dict", |
---|
392 | str(ctx.exception), |
---|
393 | ) |
---|
394 | |
---|
395 | def test_parse_cert_missing_signature(self): |
---|
396 | """ |
---|
397 | Missing the signature |
---|
398 | """ |
---|
399 | with self.assertRaises(ValueError) as ctx: |
---|
400 | parse_grid_manager_certificate('{"certificate": ""}') |
---|
401 | self.assertIn( |
---|
402 | "must contain", |
---|
403 | str(ctx.exception), |
---|
404 | ) |
---|
405 | |
---|
406 | def test_validate_cert(self): |
---|
407 | """ |
---|
408 | Validate a correctly-signed certificate |
---|
409 | """ |
---|
410 | priv0, pub0 = ed25519.create_signing_keypair() |
---|
411 | self.gm.add_storage_server("test0", pub0) |
---|
412 | cert0 = self.gm.sign("test0", timedelta(seconds=86400)) |
---|
413 | |
---|
414 | verify = create_grid_manager_verifier( |
---|
415 | [self.gm._public_key], |
---|
416 | [cert0], |
---|
417 | ed25519.string_from_verifying_key(pub0), |
---|
418 | ) |
---|
419 | |
---|
420 | self.assertTrue(verify()) |
---|
421 | |
---|
422 | |
---|
423 | class GridManagerInvalidVerifier(SyncTestCase): |
---|
424 | """ |
---|
425 | Invalid certificate rejection tests |
---|
426 | """ |
---|
427 | |
---|
428 | def setUp(self): |
---|
429 | self.gm = create_grid_manager() |
---|
430 | self.priv0, self.pub0 = ed25519.create_signing_keypair() |
---|
431 | self.gm.add_storage_server("test0", self.pub0) |
---|
432 | self.cert0 = self.gm.sign("test0", timedelta(seconds=86400)) |
---|
433 | return super(GridManagerInvalidVerifier, self).setUp() |
---|
434 | |
---|
435 | @given( |
---|
436 | base32text(), |
---|
437 | ) |
---|
438 | def test_validate_cert_invalid(self, invalid_signature): |
---|
439 | """ |
---|
440 | An incorrect signature is rejected |
---|
441 | """ |
---|
442 | # make signature invalid |
---|
443 | invalid_cert = SignedCertificate( |
---|
444 | self.cert0.certificate, |
---|
445 | invalid_signature.encode("ascii"), |
---|
446 | ) |
---|
447 | |
---|
448 | verify = create_grid_manager_verifier( |
---|
449 | [self.gm._public_key], |
---|
450 | [invalid_cert], |
---|
451 | ed25519.string_from_verifying_key(self.pub0), |
---|
452 | bad_cert = lambda key, cert: None, |
---|
453 | ) |
---|
454 | |
---|
455 | self.assertFalse(verify()) |
---|