'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:
parent
0e60920baf
commit
5ea8b698a5
|
@ -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,
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue