From 5ea8b698a5b51c4468185e1d03497de00975f868 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 12 Mar 2012 15:02:58 -0700 Subject: [PATCH] 'tahoe admin generate-keypair/derive-pubkey': add Ed25519 keypair commands Also add parse_privkey/parse_pubkey tools to util.keyutil --- src/allmydata/scripts/admin.py | 87 +++++++++++++++++++++++++++++++++ src/allmydata/scripts/runner.py | 5 +- src/allmydata/test/test_cli.py | 52 +++++++++++++++++++- src/allmydata/util/keyutil.py | 39 +++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/allmydata/scripts/admin.py create mode 100644 src/allmydata/util/keyutil.py diff --git a/src/allmydata/scripts/admin.py b/src/allmydata/scripts/admin.py new file mode 100644 index 00000000..581224d6 --- /dev/null +++ b/src/allmydata/scripts/admin.py @@ -0,0 +1,87 @@ + +from twisted.python import usage + +class GenerateKeypairOptions(usage.Options): + def getSynopsis(self): + return "Usage: tahoe admin generate-keypair" + + def getUsage(self, width=None): + t = usage.Options.getUsage(self, width) + t += """ +Generate a public/private keypair, dumped to stdout as two lines of ASCII.. + +""" + return t + +def print_keypair(options): + from allmydata.util.keyutil import make_keypair + out = options.stdout + privkey_vs, pubkey_vs = make_keypair() + print >>out, "private:", privkey_vs + print >>out, "public:", pubkey_vs + +class DerivePubkeyOptions(usage.Options): + def parseArgs(self, privkey): + self.privkey = privkey + + def getSynopsis(self): + return "Usage: tahoe admin derive-pubkey PRIVKEY" + + def getUsage(self, width=None): + t = usage.Options.getUsage(self, width) + t += """ +Given a private (signing) key that was previously generated with +generate-keypair, derive the public key and print it to stdout. + +""" + return t + +def derive_pubkey(options): + out = options.stdout + from allmydata.util import keyutil + privkey_vs = options.privkey + sk, pubkey_vs = keyutil.parse_privkey(privkey_vs) + print >>out, "private:", privkey_vs + print >>out, "public:", pubkey_vs + return 0 + +class AdminCommand(usage.Options): + subCommands = [ + ("generate-keypair", None, GenerateKeypairOptions, + "Generate a public/private keypair, write to stdout."), + ("derive-pubkey", None, DerivePubkeyOptions, + "Derive a public key from a private key."), + ] + def postOptions(self): + if not hasattr(self, 'subOptions'): + raise usage.UsageError("must specify a subcommand") + def getSynopsis(self): + return "Usage: tahoe admin SUBCOMMAND" + def getUsage(self, width=None): + t = usage.Options.getUsage(self, width) + t += """ +Please run e.g. 'tahoe admin generate-keypair --help' for more details on +each subcommand. +""" + return t + +subDispatch = { + "generate-keypair": print_keypair, + "derive-pubkey": derive_pubkey, + } + +def do_admin(options): + so = options.subOptions + so.stdout = options.stdout + so.stderr = options.stderr + f = subDispatch[options.subCommand] + return f(so) + + +subCommands = [ + ["admin", None, AdminCommand, "admin subcommands: use 'tahoe admin' for a list"], + ] + +dispatch = { + "admin": do_admin, + } diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index 3147fb5a..e66f8d33 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -5,7 +5,7 @@ from cStringIO import StringIO from twisted.python import usage from allmydata.scripts.common import BaseOptions -from allmydata.scripts import debug, create_node, startstop_node, cli, keygen, stats_gatherer +from allmydata.scripts import debug, create_node, startstop_node, cli, keygen, stats_gatherer, admin from allmydata.util.encodingutil import quote_output, get_io_encoding def GROUP(s): @@ -21,6 +21,7 @@ class Options(BaseOptions, usage.Options): + create_node.subCommands + keygen.subCommands + stats_gatherer.subCommands + + admin.subCommands + GROUP("Controlling a node") + startstop_node.subCommands + GROUP("Debugging") @@ -95,6 +96,8 @@ def runner(argv, rc = startstop_node.dispatch[command](so, stdout, stderr) elif command in debug.dispatch: rc = debug.dispatch[command](so) + elif command in admin.dispatch: + rc = admin.dispatch[command](so) elif command in cli.dispatch: rc = cli.dispatch[command](so) elif command in ac_dispatch: diff --git a/src/allmydata/test/test_cli.py b/src/allmydata/test/test_cli.py index 464e2b56..59e2c6aa 100644 --- a/src/allmydata/test/test_cli.py +++ b/src/allmydata/test/test_cli.py @@ -7,12 +7,13 @@ import simplejson from mock import patch -from allmydata.util import fileutil, hashutil, base32 +from allmydata.util import fileutil, hashutil, base32, keyutil from allmydata import uri from allmydata.immutable import upload from allmydata.interfaces import MDMF_VERSION, SDMF_VERSION from allmydata.mutable.publish import MutableData from allmydata.dirnode import normalize +from pycryptopp.publickey import ed25519 # Test that the scripts can be imported. from allmydata.scripts import create_node, debug, keygen, startstop_node, \ @@ -1366,6 +1367,55 @@ class Put(GridTestMixin, CLITestMixin, unittest.TestCase): return d +class Admin(unittest.TestCase): + def do_cli(self, *args, **kwargs): + argv = list(args) + stdin = kwargs.get("stdin", "") + stdout, stderr = StringIO(), StringIO() + d = threads.deferToThread(runner.runner, argv, run_by_human=False, + stdin=StringIO(stdin), + stdout=stdout, stderr=stderr) + def _done(res): + return stdout.getvalue(), stderr.getvalue() + d.addCallback(_done) + return d + + def test_generate_keypair(self): + d = self.do_cli("admin", "generate-keypair") + def _done( (stdout, stderr) ): + lines = [line.strip() for line in stdout.splitlines()] + privkey_bits = lines[0].split() + pubkey_bits = lines[1].split() + sk_header = "private:" + vk_header = "public:" + self.failUnlessEqual(privkey_bits[0], sk_header, lines[0]) + self.failUnlessEqual(pubkey_bits[0], vk_header, lines[1]) + self.failUnless(privkey_bits[1].startswith("priv-v0-"), lines[0]) + self.failUnless(pubkey_bits[1].startswith("pub-v0-"), lines[1]) + sk_bytes = base32.a2b(keyutil.remove_prefix(privkey_bits[1], "priv-v0-")) + sk = ed25519.SigningKey(sk_bytes) + vk_bytes = base32.a2b(keyutil.remove_prefix(pubkey_bits[1], "pub-v0-")) + self.failUnlessEqual(sk.get_verifying_key_bytes(), vk_bytes) + d.addCallback(_done) + return d + + def test_derive_pubkey(self): + priv1,pub1 = keyutil.make_keypair() + d = self.do_cli("admin", "derive-pubkey", priv1) + def _done( (stdout, stderr) ): + lines = stdout.split("\n") + privkey_line = lines[0].strip() + pubkey_line = lines[1].strip() + sk_header = "private: priv-v0-" + vk_header = "public: pub-v0-" + self.failUnless(privkey_line.startswith(sk_header), privkey_line) + self.failUnless(pubkey_line.startswith(vk_header), pubkey_line) + pub2 = pubkey_line[len(vk_header):] + self.failUnlessEqual("pub-v0-"+pub2, pub1) + d.addCallback(_done) + return d + + class List(GridTestMixin, CLITestMixin, unittest.TestCase): def test_list(self): self.basedir = "cli/List/list" diff --git a/src/allmydata/util/keyutil.py b/src/allmydata/util/keyutil.py new file mode 100644 index 00000000..ee28bd74 --- /dev/null +++ b/src/allmydata/util/keyutil.py @@ -0,0 +1,39 @@ +import os +from pycryptopp.publickey import ed25519 +from allmydata.util.base32 import a2b, b2a + +BadSignatureError = ed25519.BadSignatureError + +class BadPrefixError(Exception): + pass + +def remove_prefix(s_bytes, prefix): + if not s_bytes.startswith(prefix): + raise BadPrefixError("did not see expected '%s' prefix" % (prefix,)) + return s_bytes[len(prefix):] + +# in base32, keys are 52 chars long (both signing and verifying keys) +# in base62, keys is 43 chars long +# in base64, keys is 43 chars long +# +# We can't use base64 because we want to reserve punctuation and preserve +# cut-and-pasteability. The base62 encoding is shorter than the base32 form, +# but the minor usability improvement is not worth the documentation and +# specification confusion of using a non-standard encoding. So we stick with +# base32. + +def make_keypair(): + sk_bytes = os.urandom(32) + sk = ed25519.SigningKey(sk_bytes) + vk_bytes = sk.get_verifying_key_bytes() + return ("priv-v0-"+b2a(sk_bytes), "pub-v0-"+b2a(vk_bytes)) + +def parse_privkey(privkey_vs): + sk_bytes = a2b(remove_prefix(privkey_vs, "priv-v0-")) + sk = ed25519.SigningKey(sk_bytes) + vk_bytes = sk.get_verifying_key_bytes() + return (sk, "pub-v0-"+b2a(vk_bytes)) + +def parse_pubkey(pubkey_vs): + vk_bytes = a2b(remove_prefix(pubkey_vs, "pub-v0-")) + return ed25519.VerifyingKey(vk_bytes) -- 2.45.2