summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2021-06-01 19:05:42 +0200
committerGitHub <noreply@github.com>2021-06-01 10:05:42 -0700
commit707a3f7df8990a9319671d567580723ec7eb6dac (patch)
treec568726b46bb35dba0a28ee4824bdc860677d57c
parentc6d21fa154049241435aec0e5dcf929cfd8d4cc7 (diff)
downloadspack-707a3f7df8990a9319671d567580723ec7eb6dac.tar.gz
spack-707a3f7df8990a9319671d567580723ec7eb6dac.tar.bz2
spack-707a3f7df8990a9319671d567580723ec7eb6dac.tar.xz
spack-707a3f7df8990a9319671d567580723ec7eb6dac.zip
Simplified the spack.util.gpg implementation (#23889)
* Simplified the spack.util.gpg implementation All the classes defined in this Python module, which were previously used to construct singleton instances, have been removed in favor of four global variables. These variables are initialized lazily, like before. The API of the module has been unchanged for the most part. A few tests have been modified to use the new global names.
-rw-r--r--lib/spack/spack/cmd/tutorial.py2
-rw-r--r--lib/spack/spack/test/bindist.py6
-rw-r--r--lib/spack/spack/test/ci.py2
-rw-r--r--lib/spack/spack/test/cmd/buildcache.py2
-rw-r--r--lib/spack/spack/test/cmd/ci.py4
-rw-r--r--lib/spack/spack/test/cmd/gpg.py14
-rw-r--r--lib/spack/spack/test/conftest.py8
-rw-r--r--lib/spack/spack/test/packaging.py2
-rw-r--r--lib/spack/spack/test/util/util_gpg.py33
-rw-r--r--lib/spack/spack/util/gpg.py599
10 files changed, 304 insertions, 368 deletions
diff --git a/lib/spack/spack/cmd/tutorial.py b/lib/spack/spack/cmd/tutorial.py
index a8bc1f1ddd..10dd8f84c3 100644
--- a/lib/spack/spack/cmd/tutorial.py
+++ b/lib/spack/spack/cmd/tutorial.py
@@ -73,7 +73,7 @@ def tutorial(parser, args):
tty.msg("Ensuring that we trust tutorial binaries",
"spack gpg trust %s" % tutorial_key)
- spack.util.gpg.Gpg().trust(tutorial_key)
+ spack.util.gpg.trust(tutorial_key)
# Note that checkout MUST be last. It changes Spack under our feet.
# If you don't put this last, you'll get import errors for the code
diff --git a/lib/spack/spack/test/bindist.py b/lib/spack/spack/test/bindist.py
index fb2db23365..d7e4ddb50f 100644
--- a/lib/spack/spack/test/bindist.py
+++ b/lib/spack/spack/test/bindist.py
@@ -318,8 +318,6 @@ def test_relative_rpaths_install_nondefault(mirror_dir):
buildcache_cmd('install', '-auf', cspec.name)
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
def test_push_and_fetch_keys(mock_gnupghome):
testpath = str(mock_gnupghome)
@@ -333,7 +331,7 @@ def test_push_and_fetch_keys(mock_gnupghome):
# dir 1: create a new key, record its fingerprint, and push it to a new
# mirror
- with spack.util.gpg.gnupg_home_override(gpg_dir1):
+ with spack.util.gpg.gnupghome_override(gpg_dir1):
spack.util.gpg.create(name='test-key',
email='fake@test.key',
expires='0',
@@ -347,7 +345,7 @@ def test_push_and_fetch_keys(mock_gnupghome):
# dir 2: import the key from the mirror, and confirm that its fingerprint
# matches the one created above
- with spack.util.gpg.gnupg_home_override(gpg_dir2):
+ with spack.util.gpg.gnupghome_override(gpg_dir2):
assert len(spack.util.gpg.public_keys()) == 0
bindist.get_keys(mirrors=mirrors, install=True, trust=True, force=True)
diff --git a/lib/spack/spack/test/ci.py b/lib/spack/spack/test/ci.py
index 5599d18d5b..d9c337e4f8 100644
--- a/lib/spack/spack/test/ci.py
+++ b/lib/spack/spack/test/ci.py
@@ -56,8 +56,6 @@ def test_urlencode_string():
assert(s_enc == 'Spack+Test+Project')
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
def test_import_signing_key(mock_gnupghome):
signing_key_dir = spack_paths.mock_gpg_keys_path
signing_key_path = os.path.join(signing_key_dir, 'package-signing-key')
diff --git a/lib/spack/spack/test/cmd/buildcache.py b/lib/spack/spack/test/cmd/buildcache.py
index 5c056fb33b..addb286a41 100644
--- a/lib/spack/spack/test/cmd/buildcache.py
+++ b/lib/spack/spack/test/cmd/buildcache.py
@@ -138,8 +138,6 @@ def test_buildcache_create_fail_on_perm_denied(
tmpdir.chmod(0o700)
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
def test_update_key_index(tmpdir, mutable_mock_env_path,
install_mockery, mock_packages, mock_fetch,
mock_stage, mock_gnupghome):
diff --git a/lib/spack/spack/test/cmd/ci.py b/lib/spack/spack/test/cmd/ci.py
index 64ab6d13b9..a6fe1b4c9d 100644
--- a/lib/spack/spack/test/cmd/ci.py
+++ b/lib/spack/spack/test/cmd/ci.py
@@ -647,8 +647,6 @@ spack:
assert not any('externaltool' in key for key in yaml_contents)
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
def test_ci_rebuild(tmpdir, mutable_mock_env_path, env_deactivate,
install_mockery, mock_packages, monkeypatch,
mock_gnupghome, mock_fetch):
@@ -864,8 +862,6 @@ spack:
@pytest.mark.disable_clean_stage_check
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
def test_push_mirror_contents(tmpdir, mutable_mock_env_path, env_deactivate,
install_mockery, mock_packages, mock_fetch,
mock_stage, mock_gnupghome):
diff --git a/lib/spack/spack/test/cmd/gpg.py b/lib/spack/spack/test/cmd/gpg.py
index eb7eab2734..3834876f58 100644
--- a/lib/spack/spack/test/cmd/gpg.py
+++ b/lib/spack/spack/test/cmd/gpg.py
@@ -41,24 +41,20 @@ def test_find_gpg(cmd_name, version, tmpdir, mock_gnupghome, monkeypatch):
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
if version == 'undetectable' or version.endswith('1.3.4'):
with pytest.raises(spack.util.gpg.SpackGPGError):
- spack.util.gpg.ensure_gpg(reevaluate=True)
+ spack.util.gpg.init(force=True)
else:
- spack.util.gpg.ensure_gpg(reevaluate=True)
- gpg_exe = spack.util.gpg.get_global_gpg_instance().gpg_exe
- assert isinstance(gpg_exe, spack.util.executable.Executable)
- gpgconf_exe = spack.util.gpg.get_global_gpg_instance().gpgconf_exe
- assert isinstance(gpgconf_exe, spack.util.executable.Executable)
+ spack.util.gpg.init(force=True)
+ assert spack.util.gpg.GPG is not None
+ assert spack.util.gpg.GPGCONF is not None
def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch):
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
with pytest.raises(spack.util.gpg.SpackGPGError):
- spack.util.gpg.ensure_gpg(reevaluate=True)
+ spack.util.gpg.init(force=True)
@pytest.mark.maybeslow
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='These tests require gnupg2')
def test_gpg(tmpdir, mock_gnupghome):
# Verify a file with an empty keyring.
with pytest.raises(ProcessError):
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index 8fe566fee7..6e003dd870 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -842,8 +842,14 @@ def mock_gnupghome(monkeypatch):
# have to make our own tmpdir with a shorter name than pytest's.
# This comes up because tmp paths on macOS are already long-ish, and
# pytest makes them longer.
+ try:
+ spack.util.gpg.init()
+ except spack.util.gpg.SpackGPGError:
+ if not spack.util.gpg.GPG:
+ pytest.skip('This test requires gpg')
+
short_name_tmpdir = tempfile.mkdtemp()
- with spack.util.gpg.gnupg_home_override(short_name_tmpdir):
+ with spack.util.gpg.gnupghome_override(short_name_tmpdir):
yield short_name_tmpdir
# clean up, since we are doing this manually
diff --git a/lib/spack/spack/test/packaging.py b/lib/spack/spack/test/packaging.py
index 57446b6e2d..bd02214512 100644
--- a/lib/spack/spack/test/packaging.py
+++ b/lib/spack/spack/test/packaging.py
@@ -39,8 +39,6 @@ def fake_fetchify(url, pkg):
pkg.fetcher = fetcher
-@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
- reason='This test requires gpg')
@pytest.mark.usefixtures('install_mockery', 'mock_gnupghome')
def test_buildcache(mock_archive, tmpdir):
# tweak patchelf to only do a download
diff --git a/lib/spack/spack/test/util/util_gpg.py b/lib/spack/spack/test/util/util_gpg.py
index 987dcf7f0e..82a410561c 100644
--- a/lib/spack/spack/test/util/util_gpg.py
+++ b/lib/spack/spack/test/util/util_gpg.py
@@ -9,6 +9,12 @@ import pytest
import spack.util.gpg
+@pytest.fixture()
+def has_socket_dir():
+ spack.util.gpg.init()
+ return bool(spack.util.gpg.SOCKET_DIR)
+
+
def test_parse_gpg_output_case_one():
# Two keys, fingerprint for primary keys, but not subkeys
output = """sec::2048:1:AAAAAAAAAAAAAAAA:AAAAAAAAAA:AAAAAAAAAA:::::::::
@@ -20,7 +26,7 @@ fpr:::::::::YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY:
uid:::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::Joe (Test) <j.s@s.com>:
ssb::2048:1:AAAAAAAAAAAAAAAA:AAAAAAAAAA::::::::::
"""
- keys = spack.util.gpg.parse_secret_keys_output(output)
+ keys = spack.util.gpg._parse_secret_keys_output(output)
assert len(keys) == 2
assert keys[0] == 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
@@ -37,7 +43,7 @@ ssb:-:2048:1:AAAAAAAAA::::::esa:::+:::23:
fpr:::::::::YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY:
grp:::::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
"""
- keys = spack.util.gpg.parse_secret_keys_output(output)
+ keys = spack.util.gpg._parse_secret_keys_output(output)
assert len(keys) == 1
assert keys[0] == 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
@@ -56,19 +62,19 @@ uid:::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::Joe (Test) <j.s@s.com>:
ssb::2048:1:AAAAAAAAAAAAAAAA:AAAAAAAAAA::::::::::
fpr:::::::::ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ:"""
- keys = spack.util.gpg.parse_secret_keys_output(output)
+ keys = spack.util.gpg._parse_secret_keys_output(output)
assert len(keys) == 2
assert keys[0] == 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
assert keys[1] == 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY'
-@pytest.mark.skipif(not spack.util.gpg.GpgConstants.user_run_dir,
- reason='This test requires /var/run/user/$(id -u)')
@pytest.mark.requires_executables('gpg2')
-def test_really_long_gnupg_home_dir(tmpdir):
- N = 960
+def test_really_long_gnupghome_dir(tmpdir, has_socket_dir):
+ if not has_socket_dir:
+ pytest.skip('This test requires /var/run/user/$(id -u)')
+ N = 960
tdir = str(tmpdir)
while len(tdir) < N:
tdir = os.path.join(tdir, 'filler')
@@ -76,10 +82,11 @@ def test_really_long_gnupg_home_dir(tmpdir):
tdir = tdir[:N].rstrip(os.sep)
tdir += '0' * (N - len(tdir))
- with spack.util.gpg.gnupg_home_override(tdir):
- spack.util.gpg.create(name='Spack testing 1',
- email='test@spack.io',
- comment='Spack testing key',
- expires='0')
-
+ with spack.util.gpg.gnupghome_override(tdir):
+ spack.util.gpg.create(
+ name='Spack testing 1',
+ email='test@spack.io',
+ comment='Spack testing key',
+ expires='0'
+ )
spack.util.gpg.list(True, True)
diff --git a/lib/spack/spack/util/gpg.py b/lib/spack/spack/util/gpg.py
index 03b33af234..e224c2978f 100644
--- a/lib/spack/spack/util/gpg.py
+++ b/lib/spack/spack/util/gpg.py
@@ -2,74 +2,123 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
-
import contextlib
import errno
import functools
import os
import re
-import llnl.util.lang
-
import spack.error
import spack.paths
import spack.util.executable
import spack.version
-_gnupg_version_re = r"^gpg(conf)? \(GnuPG\) (.*)$"
-_gnupg_home_override = None
-_global_gpg_instance = None
+#: Executable instance for "gpg", initialized lazily
+GPG = None
+#: Executable instance for "gpgconf", initialized lazily
+GPGCONF = None
+#: Socket directory required if a non default home directory is used
+SOCKET_DIR = None
+#: GNUPGHOME environment variable in the context of this Python module
+GNUPGHOME = None
-def get_gnupg_home(gnupg_home=None):
- """Returns the directory that should be used as the GNUPGHOME environment
- variable when calling gpg.
+def clear():
+ """Reset the global state to uninitialized."""
+ global GPG, GPGCONF, SOCKET_DIR, GNUPGHOME
+ GPG, GPGCONF, SOCKET_DIR, GNUPGHOME = None, None, None, None
- If a [gnupg_home] is passed directly (and not None), that value will be
- used.
- Otherwise, if there is an override set (and it is not None), then that
- value will be used.
+def init(gnupghome=None, force=False):
+ """Initialize the global objects in the module, if not set.
- Otherwise, if the environment variable "SPACK_GNUPGHOME" is set, then that
- value will be used.
+ When calling any gpg executable, the GNUPGHOME environment
+ variable is set to:
- Otherwise, the default gpg path for Spack will be used.
+ 1. The value of the `gnupghome` argument, if not None
+ 2. The value of the "SPACK_GNUPGHOME" environment variable, if set
+ 3. The default gpg path for Spack otherwise
- See also: gnupg_home_override()
+ Args:
+ gnupghome (str): value to be used for GNUPGHOME when calling
+ GnuPG executables
+ force (bool): if True forces the re-initialization even if the
+ global objects are set already
+ """
+ global GPG, GPGCONF, SOCKET_DIR, GNUPGHOME
+ if force:
+ clear()
+
+ # If the executables are already set, there's nothing to do
+ if GPG and GNUPGHOME:
+ return
+
+ # Set the value of GNUPGHOME to be used in this module
+ GNUPGHOME = (gnupghome or
+ os.getenv('SPACK_GNUPGHOME') or
+ spack.paths.gpg_path)
+
+ # Set the executable objects for "gpg" and "gpgconf"
+ GPG, GPGCONF = _gpg(), _gpgconf()
+ GPG.add_default_env('GNUPGHOME', GNUPGHOME)
+ if GPGCONF:
+ GPGCONF.add_default_env('GNUPGHOME', GNUPGHOME)
+ # Set the socket dir if not using GnuPG defaults
+ SOCKET_DIR = _socket_dir(GPGCONF)
+
+ # Make sure that the GNUPGHOME exists
+ if not os.path.exists(GNUPGHOME):
+ os.makedirs(GNUPGHOME)
+ os.chmod(GNUPGHOME, 0o700)
+
+ if not os.path.isdir(GNUPGHOME):
+ msg = 'GNUPGHOME "{0}" exists and is not a directory'.format(GNUPGHOME)
+ raise SpackGPGError(msg)
+
+ if SOCKET_DIR is not None:
+ GPGCONF('--create-socketdir')
+
+
+def _autoinit(func):
+ """Decorator to ensure that global variables have been initialized before
+ running the decorated function.
+
+ Args:
+ func (callable): decorated function
"""
- return (gnupg_home or
- _gnupg_home_override or
- os.getenv('SPACK_GNUPGHOME') or
- spack.paths.gpg_path)
+ @functools.wraps(func)
+ def _wrapped(*args, **kwargs):
+ init()
+ return func(*args, **kwargs)
+ return _wrapped
@contextlib.contextmanager
-def gnupg_home_override(new_gnupg_home):
- global _gnupg_home_override
- global _global_gpg_instance
+def gnupghome_override(dir):
+ """Set the GNUPGHOME to a new location for this context.
- old_gnupg_home_override = _gnupg_home_override
- old_global_gpg_instance = _global_gpg_instance
-
- _gnupg_home_override = new_gnupg_home
- _global_gpg_instance = None
+ Args:
+ dir (str): new value for GNUPGHOME
+ """
+ global GPG, GPGCONF, SOCKET_DIR, GNUPGHOME
- yield
+ # Store backup values
+ _GPG, _GPGCONF = GPG, GPGCONF
+ _SOCKET_DIR, _GNUPGHOME = SOCKET_DIR, GNUPGHOME
+ clear()
- _gnupg_home_override = old_gnupg_home_override
- _global_gpg_instance = old_global_gpg_instance
+ # Clear global state
+ init(gnupghome=dir, force=True)
+ yield
-def get_global_gpg_instance():
- global _global_gpg_instance
- if _global_gpg_instance is None:
- _global_gpg_instance = Gpg()
- return _global_gpg_instance
+ clear()
+ GPG, GPGCONF = _GPG, _GPGCONF
+ SOCKET_DIR, GNUPGHOME = _SOCKET_DIR, _GNUPGHOME
-def parse_secret_keys_output(output):
+def _parse_secret_keys_output(output):
keys = []
found_sec = False
for line in output.split('\n'):
@@ -84,7 +133,7 @@ def parse_secret_keys_output(output):
return keys
-def parse_public_keys_output(output):
+def _parse_public_keys_output(output):
keys = []
found_pub = False
for line in output.split('\n'):
@@ -99,334 +148,224 @@ def parse_public_keys_output(output):
return keys
-cached_property = getattr(functools, 'cached_property', None)
-
-# If older python version has no cached_property, emulate it here.
-# TODO(opadron): maybe this shim should be moved to llnl.util.lang?
-if not cached_property:
- def cached_property(*args, **kwargs):
- result = property(llnl.util.lang.memoized(*args, **kwargs))
- attr = result.fget.__name__
-
- @result.deleter
- def result(self):
- getattr(type(self), attr).fget.cache.pop((self,), None)
-
- return result
-
-
-class _GpgConstants(object):
- @cached_property
- def target_version(self):
- return spack.version.Version('2')
-
- @cached_property
- def gpgconf_string(self):
- exe_str = spack.util.executable.which_string(
- 'gpgconf', 'gpg2conf', 'gpgconf2')
-
- no_gpgconf_msg = (
- 'Spack requires gpgconf version >= 2\n'
- ' To install a suitable version using Spack, run\n'
- ' spack install gnupg@2:\n'
- ' and load it by running\n'
- ' spack load gnupg@2:')
-
- if not exe_str:
- raise SpackGPGError(no_gpgconf_msg)
-
- exe = spack.util.executable.Executable(exe_str)
- output = exe('--version', output=str)
- match = re.search(_gnupg_version_re, output, re.M)
-
- if not match:
- raise SpackGPGError('Could not determine gpgconf version')
-
- if spack.version.Version(match.group(2)) < self.target_version:
- raise SpackGPGError(no_gpgconf_msg)
-
- # ensure that the gpgconf we found can run "gpgconf --create-socketdir"
- try:
- exe('--dry-run', '--create-socketdir')
- except spack.util.executable.ProcessError:
- # no dice
- exe_str = None
-
- return exe_str
+class SpackGPGError(spack.error.SpackError):
+ """Class raised when GPG errors are detected."""
- @cached_property
- def gpg_string(self):
- exe_str = spack.util.executable.which_string('gpg2', 'gpg')
- no_gpg_msg = (
- 'Spack requires gpg version >= 2\n'
- ' To install a suitable version using Spack, run\n'
- ' spack install gnupg@2:\n'
- ' and load it by running\n'
- ' spack load gnupg@2:')
+@_autoinit
+def create(**kwargs):
+ """Create a new key pair."""
+ r, w = os.pipe()
+ with contextlib.closing(os.fdopen(r, 'r')) as r:
+ with contextlib.closing(os.fdopen(w, 'w')) as 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)
+ GPG('--gen-key', '--batch', input=r)
+
+
+@_autoinit
+def signing_keys(*args):
+ """Return the keys that can be used to sign binaries."""
+ output = GPG(
+ '--list-secret-keys', '--with-colons', '--fingerprint',
+ *args, output=str
+ )
+ return _parse_secret_keys_output(output)
+
+
+@_autoinit
+def public_keys(*args):
+ """Return the keys that can be used to verify binaries."""
+ output = GPG(
+ '--list-public-keys', '--with-colons', '--fingerprint',
+ *args, output=str
+ )
+ return _parse_public_keys_output(output)
+
+
+@_autoinit
+def export_keys(location, keys, secret=False):
+ """Export public keys to a location passed as argument.
+
+ Args:
+ location (str): where to export the keys
+ keys (list): keys to be exported
+ secret (bool): whether to export secret keys or not
+ """
+ if secret:
+ GPG("--export-secret-keys", "--armor", "--output", location, *keys)
+ else:
+ GPG("--batch", "--yes", "--armor", "--export", "--output", location, *keys)
- if not exe_str:
- raise SpackGPGError(no_gpg_msg)
- exe = spack.util.executable.Executable(exe_str)
- output = exe('--version', output=str)
- match = re.search(_gnupg_version_re, output, re.M)
+@_autoinit
+def trust(keyfile):
+ """Import a public key from a file.
- if not match:
- raise SpackGPGError('Could not determine gpg version')
+ Args:
+ keyfile (str): file with the public key
+ """
+ GPG('--import', keyfile)
- if spack.version.Version(match.group(2)) < self.target_version:
- raise SpackGPGError(no_gpg_msg)
- return exe_str
+@_autoinit
+def untrust(signing, *keys):
+ """Delete known keys.
- @cached_property
- def user_run_dir(self):
- # Try to ensure that (/var)/run/user/$(id -u) exists so that
- # `gpgconf --create-socketdir` can be run later.
- #
- # NOTE(opadron): This action helps prevent a large class of
- # "file-name-too-long" errors in gpg.
+ Args:
+ signing (bool): if True deletes the secret keys
+ *keys: keys to be deleted
+ """
+ if signing:
+ skeys = signing_keys(*keys)
+ GPG('--batch', '--yes', '--delete-secret-keys', *skeys)
- try:
- has_suitable_gpgconf = bool(GpgConstants.gpgconf_string)
- except SpackGPGError:
- has_suitable_gpgconf = False
+ pkeys = public_keys(*keys)
+ GPG('--batch', '--yes', '--delete-keys', *pkeys)
- # If there is no suitable gpgconf, don't even bother trying to
- # precreate a user run dir.
- if not has_suitable_gpgconf:
- return None
- result = None
- for var_run in ('/run', '/var/run'):
- if not os.path.exists(var_run):
- continue
+@_autoinit
+def sign(key, file, output, clearsign=False):
+ """Sign a file with a key.
- var_run_user = os.path.join(var_run, 'user')
- try:
- if not os.path.exists(var_run_user):
- os.mkdir(var_run_user)
- os.chmod(var_run_user, 0o777)
+ Args:
+ key: key to be used to sign
+ file (str): file to be signed
+ output (str): output file (either the clearsigned file or
+ the detached signature)
+ clearsign (bool): if True wraps the document in an ASCII-armored
+ signature, if False creates a detached signature
+ """
+ signopt = '--clearsign' if clearsign else '--detach-sign'
+ GPG(signopt, '--armor', '--default-key', key, '--output', output, file)
- user_dir = os.path.join(var_run_user, str(os.getuid()))
- if not os.path.exists(user_dir):
- os.mkdir(user_dir)
- os.chmod(user_dir, 0o700)
+@_autoinit
+def verify(signature, file, suppress_warnings=False):
+ """Verify the signature on a file.
- # If the above operation fails due to lack of permissions, then
- # just carry on without running gpgconf and hope for the best.
- #
- # NOTE(opadron): Without a dir in which to create a socket for IPC,
- # gnupg may fail if GNUPGHOME is set to a path that
- # is too long, where "too long" in this context is
- # actually quite short; somewhere in the
- # neighborhood of more than 100 characters.
- #
- # TODO(opadron): Maybe a warning should be printed in this case?
- except OSError as exc:
- if exc.errno not in (errno.EPERM, errno.EACCES):
- raise
- user_dir = None
+ Args:
+ signature (str): signature of the file
+ file (str): file to be verified
+ suppress_warnings (bool): whether or not to suppress warnings
+ from GnuPG
+ """
+ kwargs = {'error': str} if suppress_warnings else {}
+ GPG('--verify', signature, file, **kwargs)
- # return the last iteration that provides a usable user run dir
- if user_dir is not None:
- result = user_dir
- return result
+@_autoinit
+def list(trusted, signing):
+ """List known keys.
- def clear(self):
- for attr in ('gpgconf_string', 'gpg_string', 'user_run_dir'):
- try:
- delattr(self, attr)
- except AttributeError:
- pass
+ Args:
+ trusted (bool): if True list public keys
+ signing (bool): if True list private keys
+ """
+ if trusted:
+ GPG('--list-public-keys')
+ if signing:
+ GPG('--list-secret-keys')
-GpgConstants = _GpgConstants()
+def _verify_exe_or_raise(exe):
+ msg = (
+ 'Spack requires gpgconf version >= 2\n'
+ ' To install a suitable version using Spack, run\n'
+ ' spack install gnupg@2:\n'
+ ' and load it by running\n'
+ ' spack load gnupg@2:'
+ )
+ if not exe:
+ raise SpackGPGError(msg)
-def ensure_gpg(reevaluate=False):
- if reevaluate:
- GpgConstants.clear()
+ output = exe('--version', output=str)
+ match = re.search(r"^gpg(conf)? \(GnuPG\) (.*)$", output, re.M)
+ if not match:
+ raise SpackGPGError(
+ 'Could not determine "{0}" version'.format(exe.name)
+ )
- if GpgConstants.user_run_dir is not None:
- GpgConstants.gpgconf_string
+ if spack.version.Version(match.group(2)) < spack.version.Version('2'):
+ raise SpackGPGError(msg)
- GpgConstants.gpg_string
- return True
+def _gpgconf():
+ exe = spack.util.executable.which('gpgconf', 'gpg2conf', 'gpgconf2')
+ _verify_exe_or_raise(exe)
-def has_gpg(*args, **kwargs):
+ # ensure that the gpgconf we found can run "gpgconf --create-socketdir"
try:
- return ensure_gpg(*args, **kwargs)
- except SpackGPGError:
- return False
-
-
-# NOTE(opadron): When adding methods to this class, consider adding convenience
-# wrapper functions further down in this file.
-class Gpg(object):
- def __init__(self, gnupg_home=None):
- self.gnupg_home = get_gnupg_home(gnupg_home)
-
- @cached_property
- def prep(self):
- # Make sure that suitable versions of gpgconf and gpg are available
- ensure_gpg()
-
- # Make sure that the GNUPGHOME exists
- if not os.path.exists(self.gnupg_home):
- os.makedirs(self.gnupg_home)
- os.chmod(self.gnupg_home, 0o700)
-
- if not os.path.isdir(self.gnupg_home):
- raise SpackGPGError(
- 'GNUPGHOME "{0}" exists and is not a directory'.format(
- self.gnupg_home))
-
- if GpgConstants.user_run_dir is not None:
- self.gpgconf_exe('--create-socketdir')
-
- return True
-
- @cached_property
- def gpgconf_exe(self):
- exe = spack.util.executable.Executable(GpgConstants.gpgconf_string)
- exe.add_default_env('GNUPGHOME', self.gnupg_home)
- return exe
-
- @cached_property
- def gpg_exe(self):
- exe = spack.util.executable.Executable(GpgConstants.gpg_string)
- exe.add_default_env('GNUPGHOME', self.gnupg_home)
- return exe
-
- def __call__(self, *args, **kwargs):
- if self.prep:
- return self.gpg_exe(*args, **kwargs)
-
- def create(self, **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()
- self('--gen-key', '--batch', input=r)
- r.close()
-
- def signing_keys(self, *args):
- output = self('--list-secret-keys', '--with-colons', '--fingerprint',
- *args, output=str)
- return parse_secret_keys_output(output)
-
- def public_keys(self, *args):
- output = self('--list-public-keys', '--with-colons', '--fingerprint',
- *args, output=str)
- return parse_public_keys_output(output)
-
- def export_keys(self, location, keys, secret=False):
- if secret:
- self("--export-secret-keys", "--armor", "--output", location, *keys)
- else:
- self('--batch', '--yes', '--armor', '--export', '--output',
- location, *keys)
-
- def trust(self, keyfile):
- self('--import', keyfile)
-
- def untrust(self, signing, *keys):
- if signing:
- skeys = self.signing_keys(*keys)
- self('--batch', '--yes', '--delete-secret-keys', *skeys)
-
- pkeys = self.public_keys(*keys)
- self('--batch', '--yes', '--delete-keys', *pkeys)
-
- def sign(self, key, file, output, clearsign=False):
- self(('--clearsign' if clearsign else '--detach-sign'),
- '--armor', '--default-key', key,
- '--output', output, file)
-
- def verify(self, signature, file, suppress_warnings=False):
- self('--verify', signature, file,
- **({'error': str} if suppress_warnings else {}))
-
- def list(self, trusted, signing):
- if trusted:
- self('--list-public-keys')
-
- if signing:
- self('--list-secret-keys')
-
-
-class SpackGPGError(spack.error.SpackError):
- """Class raised when GPG errors are detected."""
-
+ exe('--dry-run', '--create-socketdir')
+ except spack.util.executable.ProcessError:
+ # no dice
+ exe = None
-# Convenience wrappers for methods of the Gpg class
+ return exe
-# __call__ is a bit of a special case, since the Gpg instance is, itself, the
-# "thing" that is being called.
-@functools.wraps(Gpg.__call__)
-def gpg(*args, **kwargs):
- return get_global_gpg_instance()(*args, **kwargs)
+def _gpg():
+ exe = spack.util.executable.which('gpg2', 'gpg')
+ _verify_exe_or_raise(exe)
+ return exe
-gpg.name = 'gpg' # type: ignore[attr-defined]
+def _socket_dir(gpgconf):
+ # Try to ensure that (/var)/run/user/$(id -u) exists so that
+ # `gpgconf --create-socketdir` can be run later.
+ #
+ # NOTE(opadron): This action helps prevent a large class of
+ # "file-name-too-long" errors in gpg.
-@functools.wraps(Gpg.create)
-def create(*args, **kwargs):
- return get_global_gpg_instance().create(*args, **kwargs)
+ # If there is no suitable gpgconf, don't even bother trying to
+ # pre-create a user run dir.
+ if not gpgconf:
+ return None
+ result = None
+ for var_run in ('/run', '/var/run'):
+ if not os.path.exists(var_run):
+ continue
-@functools.wraps(Gpg.signing_keys)
-def signing_keys(*args, **kwargs):
- return get_global_gpg_instance().signing_keys(*args, **kwargs)
-
-
-@functools.wraps(Gpg.public_keys)
-def public_keys(*args, **kwargs):
- return get_global_gpg_instance().public_keys(*args, **kwargs)
-
-
-@functools.wraps(Gpg.export_keys)
-def export_keys(*args, **kwargs):
- return get_global_gpg_instance().export_keys(*args, **kwargs)
-
-
-@functools.wraps(Gpg.trust)
-def trust(*args, **kwargs):
- return get_global_gpg_instance().trust(*args, **kwargs)
-
-
-@functools.wraps(Gpg.untrust)
-def untrust(*args, **kwargs):
- return get_global_gpg_instance().untrust(*args, **kwargs)
-
+ var_run_user = os.path.join(var_run, 'user')
+ try:
+ if not os.path.exists(var_run_user):
+ os.mkdir(var_run_user)
+ os.chmod(var_run_user, 0o777)
-@functools.wraps(Gpg.sign)
-def sign(*args, **kwargs):
- return get_global_gpg_instance().sign(*args, **kwargs)
+ user_dir = os.path.join(var_run_user, str(os.getuid()))
+ if not os.path.exists(user_dir):
+ os.mkdir(user_dir)
+ os.chmod(user_dir, 0o700)
-@functools.wraps(Gpg.verify)
-def verify(*args, **kwargs):
- return get_global_gpg_instance().verify(*args, **kwargs)
+ # If the above operation fails due to lack of permissions, then
+ # just carry on without running gpgconf and hope for the best.
+ #
+ # NOTE(opadron): Without a dir in which to create a socket for IPC,
+ # gnupg may fail if GNUPGHOME is set to a path that
+ # is too long, where "too long" in this context is
+ # actually quite short; somewhere in the
+ # neighborhood of more than 100 characters.
+ #
+ # TODO(opadron): Maybe a warning should be printed in this case?
+ except OSError as exc:
+ if exc.errno not in (errno.EPERM, errno.EACCES):
+ raise
+ user_dir = None
+ # return the last iteration that provides a usable user run dir
+ if user_dir is not None:
+ result = user_dir
-@functools.wraps(Gpg.list)
-def list(*args, **kwargs):
- return get_global_gpg_instance().list(*args, **kwargs)
+ return result