summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Boeckel <mathstuf@users.noreply.github.com>2017-05-26 13:31:04 -0400
committerTodd Gamblin <tgamblin@llnl.gov>2017-05-26 10:31:04 -0700
commitf38d250e508ef933a6f0bf1e0e5be89c23e20559 (patch)
treed3669e79a944d66486503aa3e56ae11a48b77ad4
parent71cc4e2ad1f413be1abde48e7de0df1328e0e37d (diff)
downloadspack-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.
-rw-r--r--.travis.yml2
-rw-r--r--lib/spack/docs/basic_usage.rst64
-rw-r--r--lib/spack/docs/getting_started.rst1
-rw-r--r--lib/spack/spack/__init__.py7
-rw-r--r--lib/spack/spack/cmd/gpg.py168
-rw-r--r--lib/spack/spack/test/cmd/gpg.py181
-rw-r--r--lib/spack/spack/util/gpg.py120
-rw-r--r--var/spack/gpg.mock/README.md3
-rw-r--r--var/spack/gpg.mock/data/content.txt1
-rw-r--r--var/spack/gpg.mock/data/content.txt.asc17
-rw-r--r--var/spack/gpg.mock/keys/external.key30
-rw-r--r--var/spack/gpg/README.md5
12 files changed, 599 insertions, 0 deletions
diff --git a/.travis.yml b/.travis.yml
index c89bcf1275..ef3ec362c4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -71,12 +71,14 @@ addons:
- gfortran
- mercurial
- graphviz
+ - gnupg2
# Work around Travis's lack of support for Python on OSX
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew ls --versions python > /dev/null || brew install python; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew ls --versions gcc > /dev/null || brew install gcc; fi
+ - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew ls --versions gnupg2 > /dev/null || brew install gnupg2; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then virtualenv venv; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then source venv/bin/activate; fi
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')
diff --git a/var/spack/gpg.mock/README.md b/var/spack/gpg.mock/README.md
new file mode 100644
index 0000000000..95215a8634
--- /dev/null
+++ b/var/spack/gpg.mock/README.md
@@ -0,0 +1,3 @@
+# Mock GPG directory
+
+This directory contains keys and data used in the testing Spack.
diff --git a/var/spack/gpg.mock/data/content.txt b/var/spack/gpg.mock/data/content.txt
new file mode 100644
index 0000000000..6ab0f567cf
--- /dev/null
+++ b/var/spack/gpg.mock/data/content.txt
@@ -0,0 +1 @@
+This file has a signature signed by an external key.
diff --git a/var/spack/gpg.mock/data/content.txt.asc b/var/spack/gpg.mock/data/content.txt.asc
new file mode 100644
index 0000000000..7593156113
--- /dev/null
+++ b/var/spack/gpg.mock/data/content.txt.asc
@@ -0,0 +1,17 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2
+
+iQIcBAABCAAGBQJZELiKAAoJENygJBhApdriPvgP/0shBTmx4jg6QaI0zyie8a+R
++L/o9iIV4MqvBI5g+Ti+nktoCSxSOPOYFW4af740A7/43wIML9LK+gIhx/QbCrMb
+bNqzyIry9/L6PK1cCuXvd10CT+MCF1P0hdaMtKihdBYB3J8f5y1i30z+a8YWsRsX
+tPMVF/HunlpAkSWIpjmbJzFPT1R/UiBHl4VJ+mM3NNZYNIq8ZhKUiXwlQkZ8R8zg
+M0IEFkwfFtp7JxnhG7jR0k63cNm3KSocAJpwENy46RKGsAvwvqTzRh4T2MlmQIjH
+TC1MA8alJvtSdBHpkKffSU8jLewKHe1H48nc9NifMy04Ni8fSlGZe14Oe7Krqla0
+qWs+XHrGCmSleyiRUQes1MKQ7NhumKEoEaU+q0/c+lUDILZp1TlfvTPg2fzng4M/
+YF6+f+wqM+xY6z1/IloOMHis5oALjARSO88ldrLU4DQp/6jTKJO/+I4uWhMnPkMW
++a3GLWl1CShReHKbWZTLFtdQATZXA8M6wQ8FAsLOmRLb0AlEQ28A8fHrBCCdU2xj
+tSG++U1ZUo64cMYQmIMsvIApnkTh7qCkDjaVBP1to3qc83YHncxorydz9ERpuDvP
+d1IOHlJyUSM4+sLkCPvH9QyTaJn/x7D/VraznEiptGON7G6G9AgyAzIgYamm1Kwh
+UDhbQDFDhLLvUSDGzO3l
+=kwo9
+-----END PGP SIGNATURE-----
diff --git a/var/spack/gpg.mock/keys/external.key b/var/spack/gpg.mock/keys/external.key
new file mode 100644
index 0000000000..d08c90ea7f
--- /dev/null
+++ b/var/spack/gpg.mock/keys/external.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2
+
+mQINBFkQuFIBEAC7DiUM7jQ01kaGX+4nguzVeYquBRYoEUiObl5UIVSavMn4I7Oy
+aytG+qR26tUpunjEB6ftIQMJSyPKueclUJBaQ9lzQ3WpFC3ItpBNkMxHpiqPa9DX
+ddMk2QtJt4TlCWJEdnhR/92mMF+vf7B5/OvFvKOi0P+AwzBHC8IKTxml/UosmeVI
+Cs69FzRDXyqQxQAkATmuDmHXPaC6RkDmpVRe3ej+Kr+Xu4vcb/EBHg/vcZkFdSmi
+hyOj21/8LQZzcwTg4TSgHzKqbjPtIEQM3NNksvcFYlq2X0ad4cBcxa1Hj5xV8oS/
+bdYOFSdsh3QRROcEeKYVQZhvCR12qS93P4b2egbamBxCQK0Sn6QPIjlR6+Ya2/6p
+/hHddF+YVA6HJ22QZjaORf9lImYfYMs1ka2GtgkczOeaFEfcJ96nIa8Qb1jcrOon
+/3k/l+Ae09HRCcGB2DgKXw7S+CXKt46Oadp3bIDAyceotGnrG3cVA6A9Lwqy6U/5
+ywry8ETu3wlIR3EAIwM0a/3xCPg3cC/bt9rSqsFcmXyxltGI2CBTWcTqcyjW4VAw
+nVI8otBd4yNdimhpxLfx6AaMjA+D+OSltnAZUrp1fSFVhWLpTxLbcTv+HJ/g4U+x
++PAsQ79Hzmzvy/8nOvIprGzY4LCmBPbLUB47Yu761HhYQhkuJiYP1R/GzQARAQAB
+tDpTcGFjayB0ZXN0aW5nIChTcGFjayB0ZXN0aW5nIGtleSkgPHNwYWNrQGdvb2ds
+ZWdyb3Vwcy5jb20+iQI3BBMBCAAhBQJZELhSAhsDBQsJCAcCBhUICQoLAgQWAgMB
+Ah4BAheAAAoJENygJBhApdriOnUP/iLC1ZxyBP3STSVgBBTS1L6FnRAc9ya6eXNT
+EwLLoSL0I0srs0sThmhyW38ZamsXYDhggaetShxemcO0BoNAii/oNK9yQoXNF4f6
+7wg2ZxCDuDjp/3VsbiI+kNlH2kj1tQ/M53ak9nYhmwLJFfKzjQBWJiyTwYZwO3MB
+QvXBvLIKj6IDS20o+7jbOq8F243vo5/uNHc/6C9eC3i4jzXWVlln2+iN/e5sVt+X
+ZiggLK2Goj5CZ7ZjZQvdoH4wKbSPLBg0Lh5FYSih9p0wx0UTEoi0jPqFUDw81duz
+IyxjbGASSaUxoz16C2U/olPEAAXeBe4266jRQwTrn+sEIX5FD+RGoryXQ97pV5up
+I9wb2anVAMHOf20iYep3vYTjnFG/81ykODm8+I4D/Jj0EEe1E2b0D+7RQ9xKNYxC
+fDgY3isXBFzmS6O4h8N27P06yfzQX+zvjPrrHRB7ka2pmDT3M421p2wN0n9aCq1J
+8+M5UdpF98A38oosyE53KcItoCUFLgEP3KrWPwvpDUC2sNQAOFiHeitzc+v1iwmD
+RScdefCQ8qc2JJdCqMG6M0tlFy6Tw1o0eBYOhhDGa0rq/PQ4NewR2dj+yDXXBGJy
+ElR0VChqniMCyd2Q4SDPnhcVrWPTYSKL1MpsL0lXED8TGOdoAHHmQNU8MWhqmdBy
+zcWArNUY
+=yVqw
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/var/spack/gpg/README.md b/var/spack/gpg/README.md
new file mode 100644
index 0000000000..122d24f841
--- /dev/null
+++ b/var/spack/gpg/README.md
@@ -0,0 +1,5 @@
+# GPG Keys
+
+This directory contains keys that should be trusted by this installation of
+Spack. They are imported when running `spack gpg init`, but may also be
+imported manually with `spack gpg trust path/to/key`.