diff --git a/src/allmydata/scripts/admin.py b/src/allmydata/scripts/admin.py index 55ad224c2..0a641728c 100644 --- a/src/allmydata/scripts/admin.py +++ b/src/allmydata/scripts/admin.py @@ -1,7 +1,17 @@ from __future__ import print_function +import sys +import json +from os.path import exists, join + from twisted.python import usage +#from allmydata.node import read_config +from allmydata.client import read_config +from allmydata.scripts.cli import _default_nodedir from allmydata.scripts.common import BaseOptions +from allmydata.util.encodingutil import argv_to_abspath +from allmydata.util import fileutil + class GenerateKeypairOptions(BaseOptions): @@ -46,10 +56,93 @@ def derive_pubkey(options): return 0 +class AddGridManagerCertOptions(BaseOptions): + + optParameters = [ + ['filename', 'f', None, "Filename of the certificate ('-', a dash, for stdin)"], + ['name', 'n', "default", "Name to give this certificate"], + ] + + def getSynopsis(self): + return "Usage: tahoe [global-options] admin add-grid-manager-cert [options]" + + def postOptions(self): + if self['filename'] is None: + raise usage.UsageError( + "Must provide --filename option" + ) + if self['filename'] == '-': + print >>self.parent.parent.stderr, "reading certificate from stdin" + data = sys.stdin.read() + if len(data) == 0: + raise usage.UsageError( + "Reading certificate from stdin failed" + ) + from allmydata.storage_client import parse_grid_manager_data + try: + self.certificate_data = parse_grid_manager_data(data) + except ValueError as e: + print >>self.parent.parent.stderr, "Error parsing certificate: {}".format(e) + self.certificate_data = None + else: + with open(self['filename'], 'r') as f: + self.certificate_data = f.read() + + def getUsage(self, width=None): + t = BaseOptions.getUsage(self, width) + t += ( + "Adds a Grid Manager certificate to a Storage Server.\n\n" + "The certificate will be copied into the base-dir and config\n" + "will be added to 'tahoe.cfg', which will be re-written. A\n" + "restart is required for changes to take effect.\n\n" + "The human who operates a Grid Manager would produce such a\n" + "certificate and communicate it securely to you.\n" + ) + return t + + def add_grid_manager_cert(options): """ Add a new Grid Manager certificate to our config """ + if options.certificate_data is None: + return 1 + # XXX is there really not already a function for this? + if options.parent.parent['node-directory']: + nd = argv_to_abspath(options.parent.parent['node-directory']) + else: + nd = _default_nodedir + + config = read_config(nd, "portnum") + config_path = join(nd, "tahoe.cfg") + cert_fname = "{}.cert".format(options['name']) + cert_path = config.get_config_path(cert_fname) + cert_bytes = json.dumps(options.certificate_data, indent=4) + '\n' + cert_name = options['name'] + + if exists(cert_path): + print >>options.parent.parent.stderr, "Already have file '{}'".format(cert_path) + return 1 + + cfg = config.config # why aren't methods we call on cfg in _Config itself? + + gm_certs = config.get_config("storage", "grid_manager_certificate_files", "").split() + if cert_fname not in gm_certs: + gm_certs.append(cert_fname) + cfg.set("storage", "grid_manager_certificate_files", " ".join(gm_certs)) + + # print("grid_manager_certificate_files in {}: {}".format(config_path, len(gm_certs))) + + # write all the data out + + fileutil.write(cert_path, cert_bytes) + # print("created {}: {} bytes".format(cert_fname, len(cert_bytes))) + with open(config_path, "w") as f: + cfg.write(f) + # print("wrote {}".format(config_fname)) + + print >>options.parent.parent.stderr, "There are now {} certificates".format(len(gm_certs)) + return 0 @@ -59,6 +152,9 @@ class AdminCommand(BaseOptions): "Generate a public/private keypair, write to stdout."), ("derive-pubkey", None, DerivePubkeyOptions, "Derive a public key from a private key."), + ("add-grid-manager-cert", None, AddGridManagerCertOptions, + "Add a Grid Manager-provided certificate to a storage " + "server's config."), ] def postOptions(self): if not hasattr(self, 'subOptions'): diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 34b2c0caf..53b0d5c53 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -282,7 +282,39 @@ class StubServer(object): return "?" -def _validate_grid_manager_certificate(gm_key, alleged_cert): +def parse_grid_manager_data(gm_data): + """ + :param gm_data: some data that might be JSON that might be a valid + Grid Manager Certificate + + :returns: json data of a valid Grid Manager certificate, or an + exception if the data is not valid. + """ + + required_keys = allowed_keys = [ + 'certificate', + 'signature', + ] + + js = json.loads(gm_data) + for k in js.keys(): + if k not in allowed_keys: + raise ValueError( + "Grid Manager certificate JSON may not contain '{}'".format( + k, + ) + ) + for k in allowed_keys: + if k not in js: + raise ValueError( + "Grid Manager certificate JSON must contain '{}'".format( + k, + ) + ) + return js + + +def validate_grid_manager_certificate(gm_key, alleged_cert): """ :param gm_key: a VerifyingKey instance, a Grid Manager's public key. @@ -423,7 +455,7 @@ class NativeStorageServer(service.MultiService): return False for gm_key in self._grid_manager_keys: for cert in self._grid_manager_certificates: - if _validate_grid_manager_certificate(gm_key, cert): + if validate_grid_manager_certificate(gm_key, cert): # print("valid: {}\n{}".format(gm_key, cert)) return True # print("didn't validate {} keys".format(len(self._grid_manager_keys)))