← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/generate-key-pair-script into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/generate-key-pair-script into lp:launchpad.

Commit message:
Add a script to generate a NaCl key pair.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/generate-key-pair-script/+merge/371732

The instructions for generating config.snappy.store_secrets_private_key and config.snappy.store_secrets_public_key were getting a bit long, so I decided to put some of them in this script.

I used a plain entry point rather than the LaunchpadScript infrastructure, since this is very simple and doesn't need all of that machinery.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/generate-key-pair-script into lp:launchpad.
=== added directory 'lib/lp/services/crypto/scripts'
=== added file 'lib/lp/services/crypto/scripts/__init__.py'
=== added file 'lib/lp/services/crypto/scripts/generatekeypair.py'
--- lib/lp/services/crypto/scripts/generatekeypair.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/crypto/scripts/generatekeypair.py	2019-08-23 10:13:21 +0000
@@ -0,0 +1,34 @@
+# Copyright 2019 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Generate a NaCl key pair.
+
+The resulting private and public keys are base64-encoded and can be stored
+in Launchpad configuration files.  The private key should only be stored in
+secret overlays on systems that need it.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = ['main']
+
+import argparse
+import base64
+
+from nacl.public import PrivateKey
+
+
+def encode_key(key):
+    return base64.b64encode(key.encode()).decode('ASCII')
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.parse_args()
+
+    key = PrivateKey.generate()
+    print('Private: ' + encode_key(key))
+    print('Public:  ' + encode_key(key.public_key))

=== added directory 'lib/lp/services/crypto/scripts/tests'
=== added file 'lib/lp/services/crypto/scripts/tests/__init__.py'
=== added file 'lib/lp/services/crypto/scripts/tests/test_generatekeypair.py'
--- lib/lp/services/crypto/scripts/tests/test_generatekeypair.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/crypto/scripts/tests/test_generatekeypair.py	2019-08-23 10:13:21 +0000
@@ -0,0 +1,68 @@
+# Copyright 2019 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the script to generate a NaCl key pair."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+import base64
+
+from fixtures import MockPatch
+from nacl.public import (
+    PrivateKey,
+    PublicKey,
+    )
+from testtools.content import text_content
+from testtools.matchers import (
+    MatchesListwise,
+    StartsWith,
+    )
+
+from lp.services.crypto.scripts.generatekeypair import main as gkp_main
+from lp.services.utils import CapturedOutput
+from lp.testing import TestCase
+
+
+def decode_key(factory, data):
+    return factory(base64.b64decode(data.encode('ASCII')))
+
+
+class TestGenerateKeyPair(TestCase):
+
+    def runScript(self, args, expect_exit=False):
+        try:
+            with MockPatch('sys.argv', ['version-info'] + args):
+                with CapturedOutput() as captured:
+                    gkp_main()
+        except SystemExit:
+            exited = True
+        else:
+            exited = False
+        stdout = captured.stdout.getvalue()
+        stderr = captured.stderr.getvalue()
+        self.addDetail('stdout', text_content(stdout))
+        self.addDetail('stderr', text_content(stderr))
+        if expect_exit:
+            if not exited:
+                raise AssertionError('Script unexpectedly exited successfully')
+        else:
+            if exited:
+                raise AssertionError(
+                    'Script unexpectedly exited unsuccessfully')
+            self.assertEqual('', stderr)
+        return stdout
+
+    def test_bad_arguments(self):
+        self.runScript(['--nonsense'], expect_exit=True)
+
+    def test_generates_key_pair(self):
+        lines = self.runScript([]).splitlines()
+        self.assertThat(lines, MatchesListwise([
+            StartsWith('Private: '),
+            StartsWith('Public:  '),
+            ]))
+        private_key = decode_key(PrivateKey, lines[0][len('Private: '):])
+        public_key = decode_key(PublicKey, lines[1][len('Public:  '):])
+        self.assertEqual(public_key, private_key.public_key)

=== modified file 'setup.py'
--- setup.py	2019-08-22 14:23:29 +0000
+++ setup.py	2019-08-23 10:13:21 +0000
@@ -297,6 +297,8 @@
             'build-twisted-plugin-cache = '
                 'lp.services.twistedsupport.plugincache:main',
             'combine-css = lp.scripts.utilities.js.combinecss:main',
+            'generate-key-pair = '
+                'lp.services.crypto.scripts.generatekeypair:main',
             'harness = lp.scripts.harness:python',
             'iharness = lp.scripts.harness:ipython',
             'ipy = IPython.frontend.terminal.ipapp:launch_new_instance',


Follow ups