From 4f32d2b4331cb84cdd6306b254bf9ea6b6dbe74d Mon Sep 17 00:00:00 2001 From: Michael Herrmann Date: Wed, 28 Nov 2018 08:44:02 +0100 Subject: [PATCH] Add command `gengpgkey` for generating GPG keys --- fbs/builtin_commands/__init__.py | 41 +++---- fbs/builtin_commands/_docker.py | 2 +- fbs/builtin_commands/_gpg/Dockerfile | 17 +++ fbs/builtin_commands/_gpg/__init__.py | 100 ++++++++++++++++++ fbs/builtin_commands/_gpg/genkey.sh | 25 +++++ fbs/builtin_commands/_gpg/gpg-agent.conf | 3 + fbs/builtin_commands/_util.py | 27 +++++ fbs/cmdline.py | 1 + setup.py | 4 +- tests/test_fbs/builtin_commands/__init__.py | 0 .../test___init__.py} | 0 tests/test_fbs/builtin_commands/test__gpg.py | 17 +++ .../{test_settings.py => test__settings.py} | 0 13 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 fbs/builtin_commands/_gpg/Dockerfile create mode 100644 fbs/builtin_commands/_gpg/__init__.py create mode 100644 fbs/builtin_commands/_gpg/genkey.sh create mode 100644 fbs/builtin_commands/_gpg/gpg-agent.conf create mode 100644 fbs/builtin_commands/_util.py create mode 100644 tests/test_fbs/builtin_commands/__init__.py rename tests/test_fbs/{test_builtin_commands.py => builtin_commands/test___init__.py} (100%) create mode 100644 tests/test_fbs/builtin_commands/test__gpg.py rename tests/test_fbs/{test_settings.py => test__settings.py} (100%) diff --git a/fbs/builtin_commands/__init__.py b/fbs/builtin_commands/__init__.py index 772abad..b41e6b7 100644 --- a/fbs/builtin_commands/__init__.py +++ b/fbs/builtin_commands/__init__.py @@ -4,6 +4,8 @@ your Python build script and execute them there. """ from fbs import path, SETTINGS +from fbs.builtin_commands._util import prompt_for_value, \ + require_existing_project from fbs.cmdline import command from fbs.resources import copy_with_filtering from fbs_runtime import FbsError @@ -30,16 +32,15 @@ def startproject(): if exists('src'): raise FbsError('The src/ directory already exists. Aborting.') try: - app = _prompt_for_value('App name [MyApp] : ', default='MyApp') + app = prompt_for_value('App name', default='MyApp') user = getuser().title() - author = _prompt_for_value('Author [%s] : ' % user, default=user) - version = \ - _prompt_for_value('Initial version [0.0.1] : ', default='0.0.1') + author = prompt_for_value('Author', default=user) + version = prompt_for_value('Initial version', default='0.0.1') eg_bundle_id = 'com.%s.%s' % ( author.lower().split()[0], ''.join(app.lower().split()) ) - mac_bundle_identifier = _prompt_for_value( - 'Mac bundle identifier (eg. %s, optional) : ' % eg_bundle_id, + mac_bundle_identifier = prompt_for_value( + 'Mac bundle identifier (eg. %s, optional)' % eg_bundle_id, optional=True ) except KeyboardInterrupt: @@ -74,7 +75,7 @@ def run(): """ Run your app from source """ - _require_existing_project() + require_existing_project() env = dict(os.environ) pythonpath = path('src/main/python') old_pythonpath = env.get('PYTHONPATH', '') @@ -88,7 +89,7 @@ def freeze(debug=False): """ Compile your code to a standalone executable """ - _require_existing_project() + require_existing_project() # Import respective functions late to avoid circular import # fbs <-> fbs.freeze.X. app_name = SETTINGS['app_name'] @@ -127,7 +128,7 @@ def installer(): """ Create an installer for your app """ - _require_existing_project() + require_existing_project() out_file = join('target', SETTINGS['installer']) msg_parts = ['Created %s.' % out_file] if is_windows(): @@ -171,7 +172,7 @@ def test(): """ Execute your automated tests """ - _require_existing_project() + require_existing_project() sys.path.append(path('src/main/python')) suite = TestSuite() test_dirs = SETTINGS['test_dirs'] @@ -217,16 +218,6 @@ def clean(): elif islink(fpath): unlink(fpath) -def _prompt_for_value(message, optional=False, default=''): - result = input(message).strip() - if not result and default: - print(default) - return default - if not optional: - while not result: - result = input(message).strip() - return result - def _get_python_bindings(): # Use PyQt5 by default. Only use PySide2 if it is available and PyQt5 isn't. try: @@ -238,12 +229,4 @@ def _get_python_bindings(): import PyQt5 except ImportError: return 'PySide2' - return 'PyQt5' - -def _require_existing_project(): - if not exists(path('src')): - raise FbsError( - "Could not find the src/ directory. Are you in the right folder?\n" - "If yes, did you already run\n" - " fbs startproject ?" - ) \ No newline at end of file + return 'PyQt5' \ No newline at end of file diff --git a/fbs/builtin_commands/_docker.py b/fbs/builtin_commands/_docker.py index 3b1bfd2..8959ff7 100644 --- a/fbs/builtin_commands/_docker.py +++ b/fbs/builtin_commands/_docker.py @@ -69,7 +69,7 @@ def runvm(name): def _run_docker(args, **kwargs): try: - run(['docker'] + args, **kwargs) + return run(['docker'] + args, **kwargs) except FileNotFoundError: raise FbsError( 'fbs could not find Docker. Is it installed and on your PATH?' diff --git a/fbs/builtin_commands/_gpg/Dockerfile b/fbs/builtin_commands/_gpg/Dockerfile new file mode 100644 index 0000000..aad949d --- /dev/null +++ b/fbs/builtin_commands/_gpg/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:9.6 + +ARG name +ARG email +ARG passphrase +ARG keylength=1024 + +RUN apt-get update + +ADD gpg-agent.conf /root/.gnupg/gpg-agent.conf +RUN chmod -R 600 /root/.gnupg/ +RUN apt-get install gnupg2 pinentry-tty -y + +WORKDIR /root + +ADD genkey.sh /root/genkey.sh +RUN chmod +x genkey.sh \ No newline at end of file diff --git a/fbs/builtin_commands/_gpg/__init__.py b/fbs/builtin_commands/_gpg/__init__.py new file mode 100644 index 0000000..4ec85bb --- /dev/null +++ b/fbs/builtin_commands/_gpg/__init__.py @@ -0,0 +1,100 @@ +from fbs import path, SETTINGS +from fbs.builtin_commands._docker import _run_docker +from fbs.builtin_commands._util import prompt_for_value, \ + require_existing_project +from fbs.cmdline import command +from fbs_runtime import FbsError +from os import makedirs +from os.path import dirname, exists +from subprocess import DEVNULL, PIPE + +import json +import logging +import re + +_LOG = logging.getLogger(__name__) +_DOCKER_IMAGE = 'fbs:gpg_generator' +_DEST_DIR = 'src/sign/linux' +_PUBKEY_NAME = 'public-key.gpg' +_PRIVKEY_NAME = 'private-key.gpg' +_BASE_JSON = 'src/build/settings/base.json' + +@command +def gengpgkey(): + """ + Generate a GPG key for code signing on Linux + """ + require_existing_project() + if exists(_DEST_DIR): + raise FbsError('The %s folder already exists. Aborting.' % _DEST_DIR) + try: + email = prompt_for_value('Email address') + name = prompt_for_value('Real name', default=SETTINGS['author']) + passphrase = prompt_for_value('Key password', password=True) + except KeyboardInterrupt: + print('') + return + _LOG.info('Generating the GPG key. This can take a little...') + _init_docker() + args = ['run', '-t'] + if exists('/dev/urandom'): + # Give the key generator more entropy on Posix: + args.extend(['-v', '/dev/urandom:/dev/random']) + args.extend([_DOCKER_IMAGE, '/root/genkey.sh', name, email, passphrase]) + result = _run_docker(args, check=True, stdout=PIPE, universal_newlines=True) + key = _snip( + result.stdout, + "revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/", + ".rev'", + include_bounds=False + ) + pubkey = _snip(result.stdout, + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n', + '-----END PGP PUBLIC KEY BLOCK-----\n') + privkey = _snip(result.stdout, + '-----BEGIN PGP PRIVATE KEY BLOCK-----\n', + '-----END PGP PRIVATE KEY BLOCK-----\n') + makedirs(path(_DEST_DIR), exist_ok=True) + pubkey_dest = _DEST_DIR + '/' + _PUBKEY_NAME + with open(path(pubkey_dest), 'w') as f: + f.write(pubkey) + privkey_dest = _DEST_DIR + '/' + _PRIVKEY_NAME + with open(path(privkey_dest), 'w') as f: + f.write(privkey) + with open(path(_BASE_JSON)) as f: + base_contents = f.read() + new_base_contents = _extend_json(base_contents, { + 'gpg_key': key, 'gpg_name': name, 'gpg_pass': passphrase + }) + with open(path(_BASE_JSON), 'w') as f: + f.write(new_base_contents) + _LOG.info( + 'Done. Created %s and ...%s. Also updated %s with the values you ' + 'entered.', pubkey_dest, _PRIVKEY_NAME, _BASE_JSON + ) + +def _init_docker(): + _run_docker( + ['build', '-t', _DOCKER_IMAGE, dirname(__file__)], stdout=DEVNULL + ) + +def _snip(str_, preamble, postamble, include_bounds=True): + start = str_.index(preamble) + end = str_.index(postamble, start + len(preamble)) + if not include_bounds: + start += len(preamble) + if include_bounds: + end += len(postamble) + return str_[start:end] + +def _extend_json(f_contents, dict_): + if not dict_: + return f_contents + start = f_contents.index('{') + end = f_contents.rindex('}', start + 1) + body = f_contents[start:end] + match = re.search('\n(\\s+)', body) + indent = match.group(1) if match else '' + append = json.dumps(dict_, indent=indent)[1:-1] + new_body = body.rstrip() + ',' + append + return f_contents[:start] + new_body + f_contents[end:] \ No newline at end of file diff --git a/fbs/builtin_commands/_gpg/genkey.sh b/fbs/builtin_commands/_gpg/genkey.sh new file mode 100644 index 0000000..af3c030 --- /dev/null +++ b/fbs/builtin_commands/_gpg/genkey.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +tmpfile=$(mktemp) + +cat >"$tmpfile" <