'tahoe admin generate-keypair/derive-pubkey': add Ed25519 keypair commands

Also add parse_privkey/parse_pubkey tools to util.keyutil
This commit is contained in:
Brian Warner 2012-03-12 15:02:58 -07:00
parent 0e60920baf
commit 5ea8b698a5
4 changed files with 181 additions and 2 deletions

View File

@ -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,
}

View File

@ -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:

View File

@ -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"

View File

@ -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)