From c88130d8a819d0600538182d9e1c6103d656d403 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 26 Aug 2021 16:55:12 -0400 Subject: [PATCH] Make signed certificates an object instead of a dict. --- src/allmydata/cli/grid_manager.py | 4 +- src/allmydata/grid_manager.py | 51 ++++++++++++++++--------- src/allmydata/test/test_grid_manager.py | 12 +++--- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/allmydata/cli/grid_manager.py b/src/allmydata/cli/grid_manager.py index acf233b29..eb09e787a 100644 --- a/src/allmydata/cli/grid_manager.py +++ b/src/allmydata/cli/grid_manager.py @@ -1,3 +1,5 @@ +from __future__ import print_function + from datetime import ( datetime, timedelta, @@ -188,7 +190,7 @@ def sign(ctx, name, expiry_days): "No storage-server called '{}' exists".format(name) ) - certificate_data = json.dumps(certificate, indent=4) + certificate_data = json.dumps(certificate.asdict(), indent=4) click.echo(certificate_data) if fp is not None: next_serial = 0 diff --git a/src/allmydata/grid_manager.py b/src/allmydata/grid_manager.py index c288a34bb..a5a120a67 100644 --- a/src/allmydata/grid_manager.py +++ b/src/allmydata/grid_manager.py @@ -2,6 +2,9 @@ Functions and classes relating to the Grid Manager internal state """ +from future.utils import PY2, PY3 +from past.builtins import unicode + import sys import json from datetime import ( @@ -18,6 +21,24 @@ from allmydata.util import ( import attr +@attr.s +class SignedCertificate(object): + """ + A signed certificate. + """ + # A JSON-encoded certificate. + certificate = attr.ib(type=unicode) + # The signature in base32. + signature = attr.ib(type=unicode) + + @classmethod + def load(cls, file_like): + return cls(**json.load(file_like)) + + def asdict(self): + return attr.asdict(self) + + @attr.s class _GridManagerStorageServer(object): """ @@ -97,10 +118,10 @@ def _load_certificates_for(config_path, name, gm_key=None): cert_path = config_path.child('{}.cert.{}'.format(name, cert_index)) certificates = [] while cert_path.exists(): - container = json.load(cert_path.open('r')) + container = SignedCertificate.load(cert_path.open('r')) if gm_key is not None: validate_grid_manager_certificate(gm_key, container) - cert_data = json.loads(container['certificate']) + cert_data = json.loads(container.certificate) if cert_data['version'] != 1: raise ValueError( "Unknown certificate version '{}' in '{}'".format( @@ -207,8 +228,7 @@ class _GridManager(object): :param timedelta expiry: how far in the future the certificate should expire. - :returns: a dict defining the certificate (it has - "certificate" and "signature" keys). + :returns SignedCertificate: the signed certificate. """ try: srv = self._storage_servers[name] @@ -225,11 +245,10 @@ class _GridManager(object): } cert_data = json.dumps(cert_info, separators=(',',':'), sort_keys=True).encode('utf8') sig = ed25519.sign_data(self._private_key, cert_data) - certificate = { - u"certificate": cert_data, - u"signature": base32.b2a(sig), - } - + certificate = SignedCertificate( + certificate=cert_data, + signature=base32.b2a(sig), + ) vk = ed25519.verifying_key_from_signing_key(self._private_key) ed25519.verify_signature(vk, sig, cert_data) @@ -343,9 +362,7 @@ def validate_grid_manager_certificate(gm_key, alleged_cert): :param gm_key: a VerifyingKey instance, a Grid Manager's public key. - :param alleged_cert: dict with "certificate" and "signature" keys, where - "certificate" contains a JSON-serialized certificate for a Storage - Server (comes from a Grid Manager). + :param alleged_cert SignedCertificate: A signed certificate. :return: a dict consisting of the deserialized certificate data or None if the signature is invalid. Note we do NOT check the @@ -354,13 +371,13 @@ def validate_grid_manager_certificate(gm_key, alleged_cert): try: ed25519.verify_signature( gm_key, - base32.a2b(alleged_cert['signature'].encode('ascii')), - alleged_cert['certificate'].encode('ascii'), + base32.a2b(alleged_cert.signature.encode('ascii')), + alleged_cert.certificate.encode('ascii'), ) except ed25519.BadSignature: return None # signature is valid; now we can load the actual data - cert = json.loads(alleged_cert['certificate']) + cert = json.loads(alleged_cert.certificate) return cert @@ -371,10 +388,10 @@ def create_grid_manager_verifier(keys, certs, public_key, now_fn=None, bad_cert= (instead of just returning True/False here) so that the expiry-time can be tested on each call. - :param list keys: 0 or more `VerifyingKey` instances + :param list keys: 0 or more ``VerifyingKey`` instances :param list certs: 1 or more Grid Manager certificates each of - which is a `dict` containing 'signature' and 'certificate' keys. + which is a ``SignedCertificate``. :param str public_key: the identifier of the server we expect certificates for. diff --git a/src/allmydata/test/test_grid_manager.py b/src/allmydata/test/test_grid_manager.py index e53984a05..539ff5c2e 100644 --- a/src/allmydata/test/test_grid_manager.py +++ b/src/allmydata/test/test_grid_manager.py @@ -27,6 +27,7 @@ from allmydata.grid_manager import ( create_grid_manager, parse_grid_manager_certificate, create_grid_manager_verifier, + SignedCertificate, ) from allmydata.test.strategies import ( base32text, @@ -165,16 +166,13 @@ class GridManagerVerifier(SyncTestCase): cert1 = self.gm.sign("test", timedelta(seconds=3600)) self.assertNotEqual(cert0, cert1) - self.assertEqual( - set(cert0.keys()), - {"certificate", "signature"}, - ) + self.assertIsInstance(cert0, SignedCertificate) gm_key = ed25519.verifying_key_from_string(self.gm.public_identity()) self.assertEqual( ed25519.verify_signature( gm_key, - base32.a2b(cert0["signature"]), - cert0["certificate"], + base32.a2b(cert0.signature.encode("ascii")), + cert0.certificate.encode("ascii"), ), None ) @@ -432,7 +430,7 @@ class GridManagerInvalidVerifier(SyncTestCase): An incorrect signature is rejected """ # make signature invalid - self.cert0["signature"] = invalid_signature + self.cert0.signature = invalid_signature verify = create_grid_manager_verifier( [self.gm._public_key],