source: trunk/src/allmydata/test/test_grid_manager.py

Last change on this file was 6aff94dd, checked in by meejah <meejah@…>, at 2023-02-22T07:15:32Z

flake8, more frozen

  • Property mode set to 100644
File size: 14.5 KB
Line 
1"""
2Tests for the grid manager.
3"""
4
5from datetime import (
6    timedelta,
7)
8
9from twisted.python.filepath import (
10    FilePath,
11)
12
13from hypothesis import given
14
15from allmydata.node import (
16    config_from_string,
17)
18from allmydata.client import (
19    _valid_config as client_valid_config,
20)
21from allmydata.crypto import (
22    ed25519,
23)
24from allmydata.util import (
25    jsonbytes as json,
26)
27from 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)
35from allmydata.test.strategies import (
36    base32text,
37)
38
39from .common import SyncTestCase
40
41
42class 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
156class 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
423class 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())
Note: See TracBrowser for help on using the repository browser.