diff options
author | Ben Boeckel <mathstuf@users.noreply.github.com> | 2017-05-26 13:31:04 -0400 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2017-05-26 10:31:04 -0700 |
commit | f38d250e508ef933a6f0bf1e0e5be89c23e20559 (patch) | |
tree | d3669e79a944d66486503aa3e56ae11a48b77ad4 /lib | |
parent | 71cc4e2ad1f413be1abde48e7de0df1328e0e37d (diff) | |
download | spack-f38d250e508ef933a6f0bf1e0e5be89c23e20559.tar.gz spack-f38d250e508ef933a6f0bf1e0e5be89c23e20559.tar.bz2 spack-f38d250e508ef933a6f0bf1e0e5be89c23e20559.tar.xz spack-f38d250e508ef933a6f0bf1e0e5be89c23e20559.zip |
gpg: add 'spack gpg subcommand (#3845)
- Add a `spack gpg` subcommand in anticipation of signed binaries.
- GPG keys are stored in var/spack/gpg, and the spack gpg command manages them.
- Docs are included on the command.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/docs/basic_usage.rst | 64 | ||||
-rw-r--r-- | lib/spack/docs/getting_started.rst | 1 | ||||
-rw-r--r-- | lib/spack/spack/__init__.py | 7 | ||||
-rw-r--r-- | lib/spack/spack/cmd/gpg.py | 168 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/gpg.py | 181 | ||||
-rw-r--r-- | lib/spack/spack/util/gpg.py | 120 |
6 files changed, 541 insertions, 0 deletions
diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst index f25247579b..6eba26a4b5 100644 --- a/lib/spack/docs/basic_usage.rst +++ b/lib/spack/docs/basic_usage.rst @@ -276,6 +276,70 @@ Seeing installed packages We know that ``spack list`` shows you the names of available packages, but how do you figure out which are already installed? +.. _cmd-spack-gpg: + +^^^^^^^^^^^^^ +``spack gpg`` +^^^^^^^^^^^^^ + +Spack has support for signing and verifying packages using GPG keys. A +separate keyring is used for Spack, so any keys available in the user's home +directory are not used. + +^^^^^^^^^^^^^^^^^^ +``spack gpg init`` +^^^^^^^^^^^^^^^^^^ + +When Spack is first installed, its keyring is empty. Keys stored in +:file:`var/spack/gpg` are the default keys for a Spack installation. These +keys may be imported by running ``spack gpg init``. This will import the +default keys into the keyring as trusted keys. + +------------- +Trusting keys +------------- + +Additional keys may be added to the keyring using +``spack gpg trust <keyfile>``. Once a key is trusted, packages signed by the +owner of they key may be installed. + +------------- +Creating keys +------------- + +You may also create your own key so that you may sign your own packages using +``spack gpg create <name> <email>``. By default, the key has no expiration, +but it may be set with the ``--expires <date>`` flag (see the ``gnupg2`` +documentation for accepted date formats). It is also recommended to add a +comment as to the use of the key using the ``--comment <comment>`` flag. The +public half of the key can also be exported for sharing with others so that +they may use packages you have signed using the ``--export <keyfile>`` flag. +Secret keys may also be later exported using the +``spack gpg export <location> [<key>...]`` command. + +------------ +Listing keys +------------ + +In order to list the keys available in the keyring, the +``spack gpg list`` command will list trusted keys with the ``--trusted`` flag +and keys available for signing using ``--signing``. If you would like to +remove keys from your keyring, ``spack gpg untrust <keyid>``. Key IDs can be +email addresses, names, or (best) fingerprints. + +------------------------------ +Signing and Verifying Packages +------------------------------ + +In order to sign a package, ``spack gpg sign <file>`` should be used. By +default, the signature will be written to ``<file>.asc``, but that may be +changed by using the ``--output <file>`` flag. If there is only one signing +key available, it will be used, but if there is more than one, the key to use +must be specified using the ``--key <keyid>`` flag. The ``--clearsign`` flag +may also be used to create a signed file which contains the contents, but it +is not recommended. Signed packages may be verified by using +``spack gpg verify <file>``. + .. _cmd-spack-find: ^^^^^^^^^^^^^^ diff --git a/lib/spack/docs/getting_started.rst b/lib/spack/docs/getting_started.rst index 971d42cea0..75c2f662b5 100644 --- a/lib/spack/docs/getting_started.rst +++ b/lib/spack/docs/getting_started.rst @@ -14,6 +14,7 @@ before Spack is run: 1. Python 2 (2.6 or 2.7) or 3 (3.3 - 3.6) 2. A C/C++ compiler 3. The ``git`` and ``curl`` commands. +4. If using the ``gpg`` subcommand, ``gnupg2`` is required. These requirements can be easily installed on most modern Linux systems; on Macintosh, XCode is required. Spack is designed to run on HPC diff --git a/lib/spack/spack/__init__.py b/lib/spack/spack/__init__.py index 27283d10a9..057c54d665 100644 --- a/lib/spack/spack/__init__.py +++ b/lib/spack/spack/__init__.py @@ -68,6 +68,13 @@ opt_path = join_path(prefix, "opt") etc_path = join_path(prefix, "etc") +# GPG paths. +gpg_keys_path = join_path(var_path, "gpg") +mock_gpg_data_path = join_path(var_path, "gpg.mock", "data") +mock_gpg_keys_path = join_path(var_path, "gpg.mock", "keys") +gpg_path = join_path(opt_path, "spack", "gpg") + + #----------------------------------------------------------------------------- # Initial imports (only for use in this file -- see __all__ below.) #----------------------------------------------------------------------------- diff --git a/lib/spack/spack/cmd/gpg.py b/lib/spack/spack/cmd/gpg.py new file mode 100644 index 0000000000..ff511b6520 --- /dev/null +++ b/lib/spack/spack/cmd/gpg.py @@ -0,0 +1,168 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +from spack.util.gpg import Gpg +import spack +import os + +description = "handle GPG actions for spack" +section = "developer" +level = "long" + + +def setup_parser(subparser): + setup_parser.parser = subparser + subparsers = subparser.add_subparsers(help='GPG sub-commands') + + verify = subparsers.add_parser('verify') + verify.add_argument('package', type=str, + help='the package to verify') + verify.add_argument('signature', type=str, nargs='?', + help='the signature file') + verify.set_defaults(func=gpg_verify) + + trust = subparsers.add_parser('trust') + trust.add_argument('keyfile', type=str, + help='add a key to the trust store') + trust.set_defaults(func=gpg_trust) + + untrust = subparsers.add_parser('untrust') + untrust.add_argument('--signing', action='store_true', + help='allow untrusting signing keys') + untrust.add_argument('keys', nargs='+', type=str, + help='remove keys from the trust store') + untrust.set_defaults(func=gpg_untrust) + + sign = subparsers.add_parser('sign') + sign.add_argument('--output', metavar='DEST', type=str, + help='the directory to place signatures') + sign.add_argument('--key', metavar='KEY', type=str, + help='the key to use for signing') + sign.add_argument('--clearsign', action='store_true', + help='if specified, create a clearsign signature') + sign.add_argument('package', type=str, + help='the package to sign') + sign.set_defaults(func=gpg_sign) + + create = subparsers.add_parser('create') + create.add_argument('name', type=str, + help='the name to use for the new key') + create.add_argument('email', type=str, + help='the email address to use for the new key') + create.add_argument('--comment', metavar='COMMENT', type=str, + default='GPG created for Spack', + help='a description for the intended use of the key') + create.add_argument('--expires', metavar='EXPIRATION', type=str, + default='0', help='when the key should expire') + create.add_argument('--export', metavar='DEST', type=str, + help='export the public key to a file') + create.set_defaults(func=gpg_create) + + list = subparsers.add_parser('list') + list.add_argument('--trusted', action='store_true', + help='list trusted keys') + list.add_argument('--signing', action='store_true', + help='list keys which may be used for signing') + list.set_defaults(func=gpg_list) + + init = subparsers.add_parser('init') + init.set_defaults(func=gpg_init) + init.set_defaults(import_dir=spack.gpg_keys_path) + + export = subparsers.add_parser('export') + export.add_argument('location', type=str, + help='where to export keys') + export.add_argument('keys', nargs='*', + help='the keys to export; ' + 'all secret keys if unspecified') + export.set_defaults(func=gpg_export) + + +def gpg_create(args): + if args.export: + old_sec_keys = Gpg.signing_keys() + Gpg.create(name=args.name, email=args.email, + comment=args.comment, expires=args.expires) + if args.export: + new_sec_keys = set(Gpg.signing_keys()) + new_keys = new_sec_keys.difference(old_sec_keys) + Gpg.export_keys(args.export, *new_keys) + + +def gpg_export(args): + keys = args.keys + if not keys: + keys = Gpg.signing_keys() + Gpg.export_keys(args.location, *keys) + + +def gpg_list(args): + Gpg.list(args.trusted, args.signing) + + +def gpg_sign(args): + key = args.key + if key is None: + keys = Gpg.signing_keys() + if len(keys) == 1: + key = keys[0] + elif not keys: + raise RuntimeError('no signing keys are available') + else: + raise RuntimeError('multiple signing keys are available; ' + 'please choose one') + output = args.output + if not output: + output = args.package + '.asc' + # TODO: Support the package format Spack creates. + Gpg.sign(key, args.package, output, args.clearsign) + + +def gpg_trust(args): + Gpg.trust(args.keyfile) + + +def gpg_init(args): + for root, _, filenames in os.walk(args.import_dir): + for filename in filenames: + if not filename.endswith('.key'): + continue + Gpg.trust(os.path.join(root, filename)) + + +def gpg_untrust(args): + Gpg.untrust(args.signing, *args.keys) + + +def gpg_verify(args): + # TODO: Support the package format Spack creates. + signature = args.signature + if signature is None: + signature = args.package + '.asc' + Gpg.verify(signature, args.package) + + +def gpg(parser, args): + if args.func: + args.func(args) diff --git a/lib/spack/spack/test/cmd/gpg.py b/lib/spack/spack/test/cmd/gpg.py new file mode 100644 index 0000000000..cc6d57d91e --- /dev/null +++ b/lib/spack/spack/test/cmd/gpg.py @@ -0,0 +1,181 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +import argparse +import os.path + +import pytest +import spack +import spack.cmd.gpg as gpg +import spack.util.gpg as gpg_util +from spack.util.executable import ProcessError + + +@pytest.fixture(scope='function') +def testing_gpg_directory(tmpdir): + old_gpg_path = gpg_util.GNUPGHOME + gpg_util.GNUPGHOME = str(tmpdir.join('gpg')) + yield + gpg_util.GNUPGHOME = old_gpg_path + + +def has_gnupg2(): + try: + gpg_util.Gpg.gpg()('--version') + return True + except Exception: + return False + + +@pytest.mark.usefixtures('testing_gpg_directory') +@pytest.mark.skipif(not has_gnupg2(), + reason='These tests require gnupg2') +def test_gpg(tmpdir): + parser = argparse.ArgumentParser() + gpg.setup_parser(parser) + + # Verify a file with an empty keyring. + args = parser.parse_args(['verify', os.path.join( + spack.mock_gpg_data_path, 'content.txt')]) + with pytest.raises(ProcessError): + gpg.gpg(parser, args) + + # Import the default key. + args = parser.parse_args(['init']) + args.import_dir = spack.mock_gpg_keys_path + gpg.gpg(parser, args) + + # List the keys. + # TODO: Test the output here. + args = parser.parse_args(['list', '--trusted']) + gpg.gpg(parser, args) + args = parser.parse_args(['list', '--signing']) + gpg.gpg(parser, args) + + # Verify the file now that the key has been trusted. + args = parser.parse_args(['verify', os.path.join( + spack.mock_gpg_data_path, 'content.txt')]) + gpg.gpg(parser, args) + + # Untrust the default key. + args = parser.parse_args(['untrust', 'Spack testing']) + gpg.gpg(parser, args) + + # Now that the key is untrusted, verification should fail. + args = parser.parse_args(['verify', os.path.join( + spack.mock_gpg_data_path, 'content.txt')]) + with pytest.raises(ProcessError): + gpg.gpg(parser, args) + + # Create a file to test signing. + test_path = tmpdir.join('to-sign.txt') + with open(str(test_path), 'w+') as fout: + fout.write('Test content for signing.\n') + + # Signing without a private key should fail. + args = parser.parse_args(['sign', str(test_path)]) + with pytest.raises(RuntimeError) as exc_info: + gpg.gpg(parser, args) + assert exc_info.value.args[0] == 'no signing keys are available' + + # Create a key for use in the tests. + keypath = tmpdir.join('testing-1.key') + args = parser.parse_args(['create', + '--comment', 'Spack testing key', + '--export', str(keypath), + 'Spack testing 1', + 'spack@googlegroups.com']) + gpg.gpg(parser, args) + keyfp = gpg_util.Gpg.signing_keys()[0] + + # List the keys. + # TODO: Test the output here. + args = parser.parse_args(['list', '--trusted']) + gpg.gpg(parser, args) + args = parser.parse_args(['list', '--signing']) + gpg.gpg(parser, args) + + # Signing with the default (only) key. + args = parser.parse_args(['sign', str(test_path)]) + gpg.gpg(parser, args) + + # Verify the file we just verified. + args = parser.parse_args(['verify', str(test_path)]) + gpg.gpg(parser, args) + + # Export the key for future use. + export_path = tmpdir.join('export.testing.key') + args = parser.parse_args(['export', str(export_path)]) + gpg.gpg(parser, args) + + # Create a second key for use in the tests. + args = parser.parse_args(['create', + '--comment', 'Spack testing key', + 'Spack testing 2', + 'spack@googlegroups.com']) + gpg.gpg(parser, args) + + # List the keys. + # TODO: Test the output here. + args = parser.parse_args(['list', '--trusted']) + gpg.gpg(parser, args) + args = parser.parse_args(['list', '--signing']) + gpg.gpg(parser, args) + + test_path = tmpdir.join('to-sign-2.txt') + with open(str(test_path), 'w+') as fout: + fout.write('Test content for signing.\n') + + # Signing with multiple signing keys is ambiguous. + args = parser.parse_args(['sign', str(test_path)]) + with pytest.raises(RuntimeError) as exc_info: + gpg.gpg(parser, args) + assert exc_info.value.args[0] == \ + 'multiple signing keys are available; please choose one' + + # Signing with a specified key. + args = parser.parse_args(['sign', '--key', keyfp, str(test_path)]) + gpg.gpg(parser, args) + + # Untrusting signing keys needs a flag. + args = parser.parse_args(['untrust', 'Spack testing 1']) + with pytest.raises(ProcessError): + gpg.gpg(parser, args) + + # Untrust the key we created. + args = parser.parse_args(['untrust', '--signing', keyfp]) + gpg.gpg(parser, args) + + # Verification should now fail. + args = parser.parse_args(['verify', str(test_path)]) + with pytest.raises(ProcessError): + gpg.gpg(parser, args) + + # Trust the exported key. + args = parser.parse_args(['trust', str(export_path)]) + gpg.gpg(parser, args) + + # Verification should now succeed again. + args = parser.parse_args(['verify', str(test_path)]) + gpg.gpg(parser, args) diff --git a/lib/spack/spack/util/gpg.py b/lib/spack/spack/util/gpg.py new file mode 100644 index 0000000000..ccaf33519b --- /dev/null +++ b/lib/spack/spack/util/gpg.py @@ -0,0 +1,120 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## + +import os + +import spack +from spack.util.executable import Executable + + +GNUPGHOME = spack.gpg_path + + +class Gpg(object): + @staticmethod + def gpg(): + # TODO: Support loading up a GPG environment from a built gpg. + gpg = Executable('gpg2') + if not os.path.exists(GNUPGHOME): + os.makedirs(GNUPGHOME) + os.chmod(GNUPGHOME, 0o700) + gpg.add_default_env('GNUPGHOME', GNUPGHOME) + return gpg + + @classmethod + def create(cls, **kwargs): + r, w = os.pipe() + r = os.fdopen(r, 'r') + w = os.fdopen(w, 'w') + w.write(''' + Key-Type: rsa + Key-Length: 4096 + Key-Usage: sign + Name-Real: %(name)s + Name-Email: %(email)s + Name-Comment: %(comment)s + Expire-Date: %(expires)s + %%no-protection + %%commit + ''' % kwargs) + w.close() + cls.gpg()('--gen-key', '--batch', input=r) + r.close() + + @classmethod + def signing_keys(cls): + keys = [] + output = cls.gpg()('--list-secret-keys', '--with-colons', + '--fingerprint', output=str) + for line in output.split('\n'): + if line.startswith('fpr'): + keys.append(line.split(':')[9]) + return keys + + @classmethod + def export_keys(cls, location, *keys): + cls.gpg()('--armor', '--export', '--output', location, *keys) + + @classmethod + def trust(cls, keyfile): + cls.gpg()('--import', keyfile) + + @classmethod + def untrust(cls, signing, *keys): + args = [ + '--yes', + '--batch', + ] + if signing: + signing_args = args + ['--delete-secret-keys'] + list(keys) + cls.gpg()(*signing_args) + args.append('--delete-keys') + args.extend(keys) + cls.gpg()(*args) + + @classmethod + def sign(cls, key, file, output, clearsign=False): + args = [ + '--armor', + '--default-key', key, + '--output', output, + file, + ] + if clearsign: + args.insert(0, '--clearsign') + else: + args.insert(0, '--detach-sign') + cls.gpg()(*args) + + @classmethod + def verify(cls, signature, file): + cls.gpg()('--verify', signature, file) + + @classmethod + def list(cls, trusted, signing): + if trusted: + cls.gpg()('--list-public-keys') + if signing: + cls.gpg()('--list-secret-keys') |