Ticket #466: 2011-02-p2.diff

File 2011-02-p2.diff, 9.3 KB (added by warner, at 2011-02-07T18:29:36Z)

add 'tahoe admin generate-keypair/derive-pubkey' commands, apply on top of p1

  • new file src/allmydata/scripts/admin.py

    diff --git a/src/allmydata/scripts/admin.py b/src/allmydata/scripts/admin.py
    new file mode 100755
    index 0000000..d958063
    - +  
     1
     2from twisted.python import usage
     3
     4class GenerateKeypairOptions(usage.Options):
     5    def getSynopsis(self):
     6        return "Usage: tahoe admin generate-keypair"
     7
     8    def getUsage(self, width=None):
     9        t = usage.Options.getUsage(self, width)
     10        t += """
     11Generate an ECDSA192 public/private keypair, dumped to stdout as two lines of
     12base32-encoded text.
     13
     14"""
     15        return t
     16
     17def print_keypair(options):
     18    from allmydata.util.keyutil import make_keypair
     19    out = options.stdout
     20    privkey_vs, pubkey_vs = make_keypair()
     21    print >>out, "private:", privkey_vs
     22    print >>out, "public:", pubkey_vs
     23
     24class DerivePubkeyOptions(usage.Options):
     25    def parseArgs(self, privkey):
     26        self.privkey = privkey
     27
     28    def getSynopsis(self):
     29        return "Usage: tahoe admin derive-pubkey PRIVKEY"
     30
     31    def getUsage(self, width=None):
     32        t = usage.Options.getUsage(self, width)
     33        t += """
     34Given a private (signing) key that was previously generated with
     35generate-keypair, derive the public key and print it to stdout.
     36
     37"""
     38        return t
     39
     40def derive_pubkey(options):
     41    out = options.stdout
     42    err = options.stderr
     43    from allmydata.util import keyutil
     44    privkey_vs = options.privkey
     45    sk, pubkey_vs = keyutil.parse_privkey(privkey_vs)
     46    print >>out, "private:", privkey_vs
     47    print >>out, "public:", pubkey_vs
     48    return 0
     49
     50class AdminCommand(usage.Options):
     51    subCommands = [
     52        ("generate-keypair", None, GenerateKeypairOptions,
     53         "Generate a public/private keypair, write to stdout."),
     54        ("derive-pubkey", None, DerivePubkeyOptions,
     55         "Derive a public key from a private key."),
     56        ]
     57    def postOptions(self):
     58        if not hasattr(self, 'subOptions'):
     59            raise usage.UsageError("must specify a subcommand")
     60    def getSynopsis(self):
     61        return "Usage: tahoe admin SUBCOMMAND"
     62    def getUsage(self, width=None):
     63        t = usage.Options.getUsage(self, width)
     64        t += """
     65Please run e.g. 'tahoe admin generate-keypair --help' for more details on
     66each subcommand.
     67"""
     68        return t
     69
     70subDispatch = {
     71    "generate-keypair": print_keypair,
     72    "derive-pubkey": derive_pubkey,
     73    }
     74
     75def do_admin(options):
     76    so = options.subOptions
     77    so.stdout = options.stdout
     78    so.stderr = options.stderr
     79    f = subDispatch[options.subCommand]
     80    return f(so)
     81
     82
     83subCommands = [
     84    ["admin", None, AdminCommand, "admin subcommands: use 'tahoe admin' for a list"],
     85    ]
     86
     87dispatch = {
     88    "admin": do_admin,
     89    }
  • src/allmydata/scripts/runner.py

    diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py
    index 50f2f07..17c2f7b 100644
    a b from cStringIO import StringIO 
    55from twisted.python import usage
    66
    77from allmydata.scripts.common import BaseOptions
    8 from allmydata.scripts import debug, create_node, startstop_node, cli, keygen, stats_gatherer
     8from allmydata.scripts import debug, create_node, startstop_node, cli, keygen, stats_gatherer, admin
    99from allmydata.util.encodingutil import quote_output, get_argv_encoding
    1010
    1111def GROUP(s):
    class Options(BaseOptions, usage.Options): 
    2121                    +   create_node.subCommands
    2222                    +   keygen.subCommands
    2323                    +   stats_gatherer.subCommands
     24                    +   admin.subCommands
    2425                    + GROUP("Controlling a node")
    2526                    +   startstop_node.subCommands
    2627                    + GROUP("Debugging")
    def runner(argv, 
    9596        rc = startstop_node.dispatch[command](so, stdout, stderr)
    9697    elif command in debug.dispatch:
    9798        rc = debug.dispatch[command](so)
     99    elif command in admin.dispatch:
     100        rc = admin.dispatch[command](so)
    98101    elif command in cli.dispatch:
    99102        rc = cli.dispatch[command](so)
    100103    elif command in ac_dispatch:
  • src/allmydata/test/test_cli.py

    diff --git a/src/allmydata/test/test_cli.py b/src/allmydata/test/test_cli.py
    index 46527f7..d655dc4 100644
    a b import simplejson 
    77
    88from mock import patch
    99
    10 from allmydata.util import fileutil, hashutil, base32
     10from allmydata.util import fileutil, hashutil, base32, keyutil
    1111from allmydata import uri
    1212from allmydata.immutable import upload
    1313from allmydata.dirnode import normalize
     14from allmydata.util import ecdsa
    1415
    1516# Test that the scripts can be imported.
    1617from allmydata.scripts import create_node, debug, keygen, startstop_node, \
    from allmydata.scripts import common 
    2425from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases, get_alias, \
    2526     DefaultAliasMarker
    2627
    27 from allmydata.scripts import cli, debug, runner, backupdb
     28from allmydata.scripts import cli, debug, runner, backupdb, admin
    2829from allmydata.test.common_util import StallMixin, ReallyEqualMixin
    2930from allmydata.test.no_network import GridTestMixin
    3031from twisted.internet import threads # CLI tests use deferToThread
    class Put(GridTestMixin, CLITestMixin, unittest.TestCase): 
    10121013
    10131014        return d
    10141015
     1016class Admin(unittest.TestCase):
     1017    def do_cli(self, *args, **kwargs):
     1018        argv = list(args)
     1019        stdin = kwargs.get("stdin", "")
     1020        stdout, stderr = StringIO(), StringIO()
     1021        d = threads.deferToThread(runner.runner, argv, run_by_human=False,
     1022                                  stdin=StringIO(stdin),
     1023                                  stdout=stdout, stderr=stderr)
     1024        def _done(res):
     1025            return stdout.getvalue(), stderr.getvalue()
     1026        d.addCallback(_done)
     1027        return d
     1028
     1029    def test_generate_keypair(self):
     1030        d = self.do_cli("admin", "generate-keypair")
     1031        def _done( (stdout, stderr) ):
     1032            lines = stdout.split("\n")
     1033            privkey_line = lines[0].strip()
     1034            pubkey_line = lines[1].strip()
     1035            sk_header = "private: priv-v0-"
     1036            vk_header = "public: pub-v0-"
     1037            self.failUnless(privkey_line.startswith(sk_header), privkey_line)
     1038            self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
     1039            privkey_b = base32.a2b(privkey_line[len(sk_header):])
     1040            pubkey_b = base32.a2b(pubkey_line[len(vk_header):])
     1041            sk = ecdsa.SigningKey.from_string(privkey_b)
     1042            vk = ecdsa.VerifyingKey.from_string(pubkey_b)
     1043            self.failUnlessEqual(sk.get_verifying_key().to_string(),
     1044                                 vk.to_string())
     1045        d.addCallback(_done)
     1046        return d
     1047
     1048    def test_derive_pubkey(self):
     1049        priv1,pub1 = keyutil.make_keypair()
     1050        d = self.do_cli("admin", "derive-pubkey", priv1)
     1051        def _done( (stdout, stderr) ):
     1052            lines = stdout.split("\n")
     1053            privkey_line = lines[0].strip()
     1054            pubkey_line = lines[1].strip()
     1055            sk_header = "private: priv-v0-"
     1056            vk_header = "public: pub-v0-"
     1057            self.failUnless(privkey_line.startswith(sk_header), privkey_line)
     1058            self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
     1059            pub2 = pubkey_line[len(vk_header):]
     1060            self.failUnlessEqual("pub-v0-"+pub2, pub1)
     1061        d.addCallback(_done)
     1062        return d
     1063
     1064
    10151065class List(GridTestMixin, CLITestMixin, unittest.TestCase):
    10161066    def test_list(self):
    10171067        self.basedir = "cli/List/list"
  • new file src/allmydata/util/keyutil.py

    diff --git a/src/allmydata/util/keyutil.py b/src/allmydata/util/keyutil.py
    new file mode 100644
    index 0000000..87d3778
    - +  
     1from allmydata.util.ecdsa import SigningKey, VerifyingKey, NIST192p
     2from allmydata.util import base32
     3
     4# in base32, the signing key is 39 chars long, and the verifying key is 77.
     5# in base62, the signing key is 33 chars long, and the verifying key is 65.
     6# in base64, the signing key is 32 chars long, and the verifying key is 64.
     7#
     8# We can't use base64 because we want to reserve punctuation and preserve
     9# cut-and-pasteability. The base62 encoding is not significantly shorter than
     10# the base32 form, and the minor usability improvement is not worth the
     11# documentation/specification confusion of using a non-standard encoding. So
     12# we stick with base32.
     13
     14def make_keypair():
     15    privkey = SigningKey.generate(curve=NIST192p)
     16    privkey_vs = "priv-v0-%s" % base32.b2a(privkey.to_string())
     17    pubkey = privkey.get_verifying_key()
     18    pubkey_vs = "pub-v0-%s" % base32.b2a(pubkey.to_string())
     19    return privkey_vs, pubkey_vs
     20
     21def parse_privkey(privkey_vs):
     22    if not privkey_vs.startswith("priv-v0-"):
     23        raise ValueError("private key must look like 'priv-v0-...', not '%s'" % privkey_vs)
     24    privkey_s = privkey_vs[len("priv-v0-"):]
     25    sk = SigningKey.from_string(base32.a2b(privkey_s), curve=NIST192p)
     26    pubkey = sk.get_verifying_key()
     27    pubkey_vs = "pub-v0-%s" % base32.b2a(pubkey.to_string())
     28    return sk, pubkey_vs
     29
     30def parse_pubkey(pubkey_vs):
     31    if not pubkey_vs.startswith("pub-v0-"):
     32        raise ValueError("public key must look like 'pub-v0-...', not '%s'" % pubkey_vs)
     33    pubkey_s = pubkey_vs[len("pub-v0-"):]
     34    vk = VerifyingKey.from_string(base32.a2b(pubkey_s), curve=NIST192p)
     35    return vk