--- /dev/null
+
+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,
+ }
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):
+ create_node.subCommands
+ keygen.subCommands
+ stats_gatherer.subCommands
+ + admin.subCommands
+ GROUP("Controlling a node")
+ startstop_node.subCommands
+ GROUP("Debugging")
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:
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, \
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"
--- /dev/null
+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)