summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorHarmen Stoppels <me@harmenstoppels.nl>2023-12-11 22:14:59 +0100
committerGitHub <noreply@github.com>2023-12-11 15:14:59 -0600
commit8c29e90fa9962f4a44f39f47217b46c85176af28 (patch)
tree6d1721b3576a7a891e536e1d77584cd3a410601c /lib
parent045f398f3da319dc099a776f95f162a614553740 (diff)
downloadspack-8c29e90fa9962f4a44f39f47217b46c85176af28.tar.gz
spack-8c29e90fa9962f4a44f39f47217b46c85176af28.tar.bz2
spack-8c29e90fa9962f4a44f39f47217b46c85176af28.tar.xz
spack-8c29e90fa9962f4a44f39f47217b46c85176af28.zip
Build cache: make signed/unsigned a mirror property (#41507)
* Add `signed` property to mirror config * make unsigned a tri-state: true/false overrides mirror config, none takes mirror config * test commands * Document this * add a test
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/binary_caches.rst38
-rw-r--r--lib/spack/spack/binary_distribution.py71
-rw-r--r--lib/spack/spack/cmd/buildcache.py38
-rw-r--r--lib/spack/spack/cmd/install.py4
-rw-r--r--lib/spack/spack/cmd/mirror.py39
-rw-r--r--lib/spack/spack/installer.py26
-rw-r--r--lib/spack/spack/mirror.py6
-rw-r--r--lib/spack/spack/schema/mirrors.py1
-rw-r--r--lib/spack/spack/test/cmd/buildcache.py34
-rw-r--r--lib/spack/spack/test/cmd/mirror.py9
10 files changed, 214 insertions, 52 deletions
diff --git a/lib/spack/docs/binary_caches.rst b/lib/spack/docs/binary_caches.rst
index 3215f9732f..ee3cc239f8 100644
--- a/lib/spack/docs/binary_caches.rst
+++ b/lib/spack/docs/binary_caches.rst
@@ -153,7 +153,43 @@ keyring, and trusting all downloaded keys.
List of popular build caches
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-* `Extreme-scale Scientific Software Stack (E4S) <https://e4s-project.github.io/>`_: `build cache <https://oaciss.uoregon.edu/e4s/inventory.html>`_
+* `Extreme-scale Scientific Software Stack (E4S) <https://e4s-project.github.io/>`_: `build cache <https://oaciss.uoregon.edu/e4s/inventory.html>`_'
+
+-------------------
+Build cache signing
+-------------------
+
+By default, Spack will add a cryptographic signature to each package pushed to
+a build cache, and verifies the signature when installing from a build cache.
+
+Keys for signing can be managed with the :ref:`spack gpg <cmd-spack-gpg>` command,
+as well as ``spack buildcache keys`` as mentioned above.
+
+You can disable signing when pushing with ``spack buildcache push --unsigned``,
+and disable verification when installing from any build cache with
+``spack install --no-check-signature``.
+
+Alternatively, signing and verification can be enabled or disabled on a per build cache
+basis:
+
+.. code-block:: console
+
+ $ spack mirror add --signed <name> <url> # enable signing and verification
+ $ spack mirror add --unsigned <name> <url> # disable signing and verification
+
+ $ spack mirror set --signed <name> # enable signing and verification for an existing mirror
+ $ spack mirror set --unsigned <name> # disable signing and verification for an existing mirror
+
+Or you can directly edit the ``mirrors.yaml`` configuration file:
+
+.. code-block:: yaml
+
+ mirrors:
+ <name>:
+ url: <url>
+ signed: false # disable signing and verification
+
+See also :ref:`mirrors`.
----------
Relocation
diff --git a/lib/spack/spack/binary_distribution.py b/lib/spack/spack/binary_distribution.py
index 159b07eb94..85c47e2337 100644
--- a/lib/spack/spack/binary_distribution.py
+++ b/lib/spack/spack/binary_distribution.py
@@ -25,7 +25,7 @@ import urllib.request
import warnings
from contextlib import closing, contextmanager
from gzip import GzipFile
-from typing import Dict, List, NamedTuple, Optional, Set, Tuple
+from typing import Dict, Iterable, List, NamedTuple, Optional, Set, Tuple
from urllib.error import HTTPError, URLError
import llnl.util.filesystem as fsys
@@ -1605,14 +1605,14 @@ def _get_valid_spec_file(path: str, max_supported_layout: int) -> Tuple[Dict, in
return spec_dict, layout_version
-def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
+def download_tarball(spec, unsigned: Optional[bool] = False, mirrors_for_spec=None):
"""
Download binary tarball for given package into stage area, returning
path to downloaded tarball if successful, None otherwise.
Args:
spec (spack.spec.Spec): Concrete spec
- unsigned (bool): Whether or not to require signed binaries
+ unsigned: if ``True`` or ``False`` override the mirror signature verification defaults
mirrors_for_spec (list): Optional list of concrete specs and mirrors
obtained by calling binary_distribution.get_mirrors_for_spec().
These will be checked in order first before looking in other
@@ -1633,7 +1633,9 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
"signature_verified": "true-if-binary-pkg-was-already-verified"
}
"""
- configured_mirrors = spack.mirror.MirrorCollection(binary=True).values()
+ configured_mirrors: Iterable[spack.mirror.Mirror] = spack.mirror.MirrorCollection(
+ binary=True
+ ).values()
if not configured_mirrors:
tty.die("Please add a spack mirror to allow download of pre-compiled packages.")
@@ -1651,8 +1653,16 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
# mirror for the spec twice though.
try_first = [i["mirror_url"] for i in mirrors_for_spec] if mirrors_for_spec else []
try_next = [i.fetch_url for i in configured_mirrors if i.fetch_url not in try_first]
+ mirror_urls = try_first + try_next
- mirrors = try_first + try_next
+ # TODO: turn `mirrors_for_spec` into a list of Mirror instances, instead of doing that here.
+ def fetch_url_to_mirror(url):
+ for mirror in configured_mirrors:
+ if mirror.fetch_url == url:
+ return mirror
+ return spack.mirror.Mirror(url)
+
+ mirrors = [fetch_url_to_mirror(url) for url in mirror_urls]
tried_to_verify_sigs = []
@@ -1661,14 +1671,17 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
# we remove support for deprecated spec formats and buildcache layouts.
for try_signed in (True, False):
for mirror in mirrors:
+ # Override mirror's default if
+ currently_unsigned = unsigned if unsigned is not None else not mirror.signed
+
# If it's an OCI index, do things differently, since we cannot compose URLs.
- parsed = urllib.parse.urlparse(mirror)
+ fetch_url = mirror.fetch_url
# TODO: refactor this to some "nice" place.
- if parsed.scheme == "oci":
- ref = spack.oci.image.ImageReference.from_string(mirror[len("oci://") :]).with_tag(
- spack.oci.image.default_tag(spec)
- )
+ if fetch_url.startswith("oci://"):
+ ref = spack.oci.image.ImageReference.from_string(
+ fetch_url[len("oci://") :]
+ ).with_tag(spack.oci.image.default_tag(spec))
# Fetch the manifest
try:
@@ -1705,7 +1718,7 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
except InvalidMetadataFile as e:
tty.warn(
f"Ignoring binary package for {spec.name}/{spec.dag_hash()[:7]} "
- f"from {mirror} due to invalid metadata file: {e}"
+ f"from {fetch_url} due to invalid metadata file: {e}"
)
local_specfile_stage.destroy()
continue
@@ -1727,13 +1740,16 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
"tarball_stage": tarball_stage,
"specfile_stage": local_specfile_stage,
"signature_verified": False,
+ "signature_required": not currently_unsigned,
}
else:
ext = "json.sig" if try_signed else "json"
- specfile_path = url_util.join(mirror, BUILD_CACHE_RELATIVE_PATH, specfile_prefix)
+ specfile_path = url_util.join(
+ fetch_url, BUILD_CACHE_RELATIVE_PATH, specfile_prefix
+ )
specfile_url = f"{specfile_path}.{ext}"
- spackfile_url = url_util.join(mirror, BUILD_CACHE_RELATIVE_PATH, tarball)
+ spackfile_url = url_util.join(fetch_url, BUILD_CACHE_RELATIVE_PATH, tarball)
local_specfile_stage = try_fetch(specfile_url)
if local_specfile_stage:
local_specfile_path = local_specfile_stage.save_filename
@@ -1746,21 +1762,21 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
except InvalidMetadataFile as e:
tty.warn(
f"Ignoring binary package for {spec.name}/{spec.dag_hash()[:7]} "
- f"from {mirror} due to invalid metadata file: {e}"
+ f"from {fetch_url} due to invalid metadata file: {e}"
)
local_specfile_stage.destroy()
continue
- if try_signed and not unsigned:
+ if try_signed and not currently_unsigned:
# If we found a signed specfile at the root, try to verify
# the signature immediately. We will not download the
# tarball if we could not verify the signature.
tried_to_verify_sigs.append(specfile_url)
signature_verified = try_verify(local_specfile_path)
if not signature_verified:
- tty.warn("Failed to verify: {0}".format(specfile_url))
+ tty.warn(f"Failed to verify: {specfile_url}")
- if unsigned or signature_verified or not try_signed:
+ if currently_unsigned or signature_verified or not try_signed:
# We will download the tarball in one of three cases:
# 1. user asked for --no-check-signature
# 2. user didn't ask for --no-check-signature, but we
@@ -1783,6 +1799,7 @@ def download_tarball(spec, unsigned=False, mirrors_for_spec=None):
"tarball_stage": tarball_stage,
"specfile_stage": local_specfile_stage,
"signature_verified": signature_verified,
+ "signature_required": not currently_unsigned,
}
local_specfile_stage.destroy()
@@ -1981,7 +1998,7 @@ def relocate_package(spec):
relocate.relocate_text(text_names, prefix_to_prefix_text)
-def _extract_inner_tarball(spec, filename, extract_to, unsigned, remote_checksum):
+def _extract_inner_tarball(spec, filename, extract_to, signature_required: bool, remote_checksum):
stagepath = os.path.dirname(filename)
spackfile_name = tarball_name(spec, ".spack")
spackfile_path = os.path.join(stagepath, spackfile_name)
@@ -2001,7 +2018,7 @@ def _extract_inner_tarball(spec, filename, extract_to, unsigned, remote_checksum
else:
raise ValueError("Cannot find spec file for {0}.".format(extract_to))
- if not unsigned:
+ if signature_required:
if os.path.exists("%s.asc" % specfile_path):
suppress = config.get("config:suppress_gpg_warnings", False)
try:
@@ -2050,7 +2067,7 @@ def _tar_strip_component(tar: tarfile.TarFile, prefix: str):
m.linkname = m.linkname[result.end() :]
-def extract_tarball(spec, download_result, unsigned=False, force=False, timer=timer.NULL_TIMER):
+def extract_tarball(spec, download_result, force=False, timer=timer.NULL_TIMER):
"""
extract binary tarball for given package into install area
"""
@@ -2076,7 +2093,8 @@ def extract_tarball(spec, download_result, unsigned=False, force=False, timer=ti
bchecksum = spec_dict["binary_cache_checksum"]
filename = download_result["tarball_stage"].save_filename
- signature_verified = download_result["signature_verified"]
+ signature_verified: bool = download_result["signature_verified"]
+ signature_required: bool = download_result["signature_required"]
tmpdir = None
if layout_version == 0:
@@ -2085,7 +2103,9 @@ def extract_tarball(spec, download_result, unsigned=False, force=False, timer=ti
# and another tarball containing the actual install tree.
tmpdir = tempfile.mkdtemp()
try:
- tarfile_path = _extract_inner_tarball(spec, filename, tmpdir, unsigned, bchecksum)
+ tarfile_path = _extract_inner_tarball(
+ spec, filename, tmpdir, signature_required, bchecksum
+ )
except Exception as e:
_delete_staged_downloads(download_result)
shutil.rmtree(tmpdir)
@@ -2098,9 +2118,10 @@ def extract_tarball(spec, download_result, unsigned=False, force=False, timer=ti
# the tarball.
tarfile_path = filename
- if not unsigned and not signature_verified:
+ if signature_required and not signature_verified:
raise UnsignedPackageException(
- "To install unsigned packages, use the --no-check-signature option."
+ "To install unsigned packages, use the --no-check-signature option, "
+ "or configure the mirror with signed: false."
)
# compute the sha256 checksum of the tarball
@@ -2213,7 +2234,7 @@ def install_root_node(spec, unsigned=False, force=False, sha256=None):
# don't print long padded paths while extracting/relocating binaries
with spack.util.path.filter_padding():
tty.msg('Installing "{0}" from a buildcache'.format(spec.format()))
- extract_tarball(spec, download_result, unsigned, force)
+ extract_tarball(spec, download_result, force)
spack.hooks.post_install(spec, False)
spack.store.STORE.db.add(spec, spack.store.STORE.layout)
diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py
index 76e2d0f61c..b591636ba2 100644
--- a/lib/spack/spack/cmd/buildcache.py
+++ b/lib/spack/spack/cmd/buildcache.py
@@ -76,7 +76,19 @@ def setup_parser(subparser: argparse.ArgumentParser):
)
push_sign = push.add_mutually_exclusive_group(required=False)
push_sign.add_argument(
- "--unsigned", "-u", action="store_true", help="push unsigned buildcache tarballs"
+ "--unsigned",
+ "-u",
+ action="store_false",
+ dest="signed",
+ default=None,
+ help="push unsigned buildcache tarballs",
+ )
+ push_sign.add_argument(
+ "--signed",
+ action="store_true",
+ dest="signed",
+ default=None,
+ help="push signed buildcache tarballs",
)
push_sign.add_argument(
"--key", "-k", metavar="key", type=str, default=None, help="key for signing"
@@ -328,17 +340,27 @@ def push_fn(args):
"The flag `--allow-root` is the default in Spack 0.21, will be removed in Spack 0.22"
)
+ mirror: spack.mirror.Mirror = args.mirror
+
# Check if this is an OCI image.
try:
- image_ref = spack.oci.oci.image_from_mirror(args.mirror)
+ image_ref = spack.oci.oci.image_from_mirror(mirror)
except ValueError:
image_ref = None
+ push_url = mirror.push_url
+
+ # When neither --signed, --unsigned nor --key are specified, use the mirror's default.
+ if args.signed is None and not args.key:
+ unsigned = not mirror.signed
+ else:
+ unsigned = not (args.key or args.signed)
+
# For OCI images, we require dependencies to be pushed for now.
if image_ref:
if "dependencies" not in args.things_to_install:
tty.die("Dependencies must be pushed for OCI images.")
- if not args.unsigned:
+ if not unsigned:
tty.warn(
"Code signing is currently not supported for OCI images. "
"Use --unsigned to silence this warning."
@@ -351,12 +373,10 @@ def push_fn(args):
dependencies="dependencies" in args.things_to_install,
)
- url = args.mirror.push_url
-
# When pushing multiple specs, print the url once ahead of time, as well as how
# many specs are being pushed.
if len(specs) > 1:
- tty.info(f"Selected {len(specs)} specs to push to {url}")
+ tty.info(f"Selected {len(specs)} specs to push to {push_url}")
failed = []
@@ -373,10 +393,10 @@ def push_fn(args):
try:
bindist.push_or_raise(
spec,
- url,
+ push_url,
bindist.PushOptions(
force=args.force,
- unsigned=args.unsigned,
+ unsigned=unsigned,
key=args.key,
regenerate_index=args.update_index,
),
@@ -384,7 +404,7 @@ def push_fn(args):
msg = f"{_progress(i, len(specs))}Pushed {_format_spec(spec)}"
if len(specs) == 1:
- msg += f" to {url}"
+ msg += f" to {push_url}"
tty.info(msg)
except bindist.NoOverwriteException:
diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py
index 5db30ba7b5..8d085298c1 100644
--- a/lib/spack/spack/cmd/install.py
+++ b/lib/spack/spack/cmd/install.py
@@ -162,8 +162,8 @@ def setup_parser(subparser):
"--no-check-signature",
action="store_true",
dest="unsigned",
- default=False,
- help="do not check signatures of binary packages",
+ default=None,
+ help="do not check signatures of binary packages (override mirror config)",
)
subparser.add_argument(
"--show-log-on-error",
diff --git a/lib/spack/spack/cmd/mirror.py b/lib/spack/spack/cmd/mirror.py
index 91a3f27931..498f26afb2 100644
--- a/lib/spack/spack/cmd/mirror.py
+++ b/lib/spack/spack/cmd/mirror.py
@@ -107,6 +107,23 @@ def setup_parser(subparser):
"and source use `--type binary --type source` (default)"
),
)
+ add_parser_signed = add_parser.add_mutually_exclusive_group(required=False)
+ add_parser_signed.add_argument(
+ "--unsigned",
+ help="do not require signing and signature verification when pushing and installing from "
+ "this build cache",
+ action="store_false",
+ default=None,
+ dest="signed",
+ )
+ add_parser_signed.add_argument(
+ "--signed",
+ help="require signing and signature verification when pushing and installing from this "
+ "build cache",
+ action="store_true",
+ default=None,
+ dest="signed",
+ )
arguments.add_connection_args(add_parser, False)
# Remove
remove_parser = sp.add_parser("remove", aliases=["rm"], help=mirror_remove.__doc__)
@@ -157,6 +174,23 @@ def setup_parser(subparser):
),
)
set_parser.add_argument("--url", help="url of mirror directory from 'spack mirror create'")
+ set_parser_unsigned = set_parser.add_mutually_exclusive_group(required=False)
+ set_parser_unsigned.add_argument(
+ "--unsigned",
+ help="do not require signing and signature verification when pushing and installing from "
+ "this build cache",
+ action="store_false",
+ default=None,
+ dest="signed",
+ )
+ set_parser_unsigned.add_argument(
+ "--signed",
+ help="require signing and signature verification when pushing and installing from this "
+ "build cache",
+ action="store_true",
+ default=None,
+ dest="signed",
+ )
set_parser.add_argument(
"--scope",
action=arguments.ConfigScope,
@@ -186,6 +220,7 @@ def mirror_add(args):
or args.type
or args.oci_username
or args.oci_password
+ or args.signed is not None
):
connection = {"url": args.url}
if args.s3_access_key_id and args.s3_access_key_secret:
@@ -201,6 +236,8 @@ def mirror_add(args):
if args.type:
connection["binary"] = "binary" in args.type
connection["source"] = "source" in args.type
+ if args.signed is not None:
+ connection["signed"] = args.signed
mirror = spack.mirror.Mirror(connection, name=args.name)
else:
mirror = spack.mirror.Mirror(args.url, name=args.name)
@@ -233,6 +270,8 @@ def _configure_mirror(args):
changes["endpoint_url"] = args.s3_endpoint_url
if args.oci_username and args.oci_password:
changes["access_pair"] = [args.oci_username, args.oci_password]
+ if getattr(args, "signed", None) is not None:
+ changes["signed"] = args.signed
# argparse cannot distinguish between --binary and --no-binary when same dest :(
# notice that set-url does not have these args, so getattr
diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py
index ef0f1f3b8b..915f80993b 100644
--- a/lib/spack/spack/installer.py
+++ b/lib/spack/spack/installer.py
@@ -381,7 +381,10 @@ def _print_timer(pre: str, pkg_id: str, timer: timer.BaseTimer) -> None:
def _install_from_cache(
- pkg: "spack.package_base.PackageBase", cache_only: bool, explicit: bool, unsigned: bool = False
+ pkg: "spack.package_base.PackageBase",
+ cache_only: bool,
+ explicit: bool,
+ unsigned: Optional[bool] = False,
) -> bool:
"""
Extract the package from binary cache
@@ -391,8 +394,7 @@ def _install_from_cache(
cache_only: only extract from binary cache
explicit: ``True`` if installing the package was explicitly
requested by the user, otherwise, ``False``
- unsigned: ``True`` if binary package signatures to be checked,
- otherwise, ``False``
+ unsigned: if ``True`` or ``False`` override the mirror signature verification defaults
Return: ``True`` if the package was extract from binary cache, ``False`` otherwise
"""
@@ -462,7 +464,7 @@ def _process_external_package(pkg: "spack.package_base.PackageBase", explicit: b
def _process_binary_cache_tarball(
pkg: "spack.package_base.PackageBase",
explicit: bool,
- unsigned: bool,
+ unsigned: Optional[bool],
mirrors_for_spec: Optional[list] = None,
timer: timer.BaseTimer = timer.NULL_TIMER,
) -> bool:
@@ -472,8 +474,7 @@ def _process_binary_cache_tarball(
Args:
pkg: the package being installed
explicit: the package was explicitly requested by the user
- unsigned: ``True`` if binary package signatures to be checked,
- otherwise, ``False``
+ unsigned: if ``True`` or ``False`` override the mirror signature verification defaults
mirrors_for_spec: Optional list of concrete specs and mirrors
obtained by calling binary_distribution.get_mirrors_for_spec().
timer: timer to keep track of binary install phases.
@@ -493,9 +494,7 @@ def _process_binary_cache_tarball(
tty.msg(f"Extracting {package_id(pkg)} from binary cache")
with timer.measure("install"), spack.util.path.filter_padding():
- binary_distribution.extract_tarball(
- pkg.spec, download_result, unsigned=unsigned, force=False, timer=timer
- )
+ binary_distribution.extract_tarball(pkg.spec, download_result, force=False, timer=timer)
pkg.installed_from_binary_cache = True
spack.store.STORE.db.add(pkg.spec, spack.store.STORE.layout, explicit=explicit)
@@ -505,7 +504,7 @@ def _process_binary_cache_tarball(
def _try_install_from_binary_cache(
pkg: "spack.package_base.PackageBase",
explicit: bool,
- unsigned: bool = False,
+ unsigned: Optional[bool] = None,
timer: timer.BaseTimer = timer.NULL_TIMER,
) -> bool:
"""
@@ -514,8 +513,7 @@ def _try_install_from_binary_cache(
Args:
pkg: package to be extracted from binary cache
explicit: the package was explicitly requested by the user
- unsigned: ``True`` if binary package signatures to be checked,
- otherwise, ``False``
+ unsigned: if ``True`` or ``False`` override the mirror signature verification defaults
timer: timer to keep track of binary install phases.
"""
# Early exit if no binary mirrors are configured.
@@ -825,7 +823,7 @@ class BuildRequest:
("restage", False),
("skip_patch", False),
("tests", False),
- ("unsigned", False),
+ ("unsigned", None),
("verbose", False),
]:
_ = self.install_args.setdefault(arg, default)
@@ -1663,7 +1661,7 @@ class PackageInstaller:
use_cache = task.use_cache
tests = install_args.get("tests", False)
assert isinstance(tests, (bool, list)) # make mypy happy.
- unsigned = bool(install_args.get("unsigned"))
+ unsigned: Optional[bool] = install_args.get("unsigned")
pkg, pkg_id = task.pkg, task.pkg_id
diff --git a/lib/spack/spack/mirror.py b/lib/spack/spack/mirror.py
index d5425772cd..b062f1c356 100644
--- a/lib/spack/spack/mirror.py
+++ b/lib/spack/spack/mirror.py
@@ -134,6 +134,10 @@ class Mirror:
return isinstance(self._data, str) or self._data.get("source", True)
@property
+ def signed(self) -> bool:
+ return isinstance(self._data, str) or self._data.get("signed", True)
+
+ @property
def fetch_url(self):
"""Get the valid, canonicalized fetch URL"""
return self.get_url("fetch")
@@ -146,7 +150,7 @@ class Mirror:
def _update_connection_dict(self, current_data: dict, new_data: dict, top_level: bool):
keys = ["url", "access_pair", "access_token", "profile", "endpoint_url"]
if top_level:
- keys += ["binary", "source"]
+ keys += ["binary", "source", "signed"]
changed = False
for key in keys:
if key in new_data and current_data.get(key) != new_data[key]:
diff --git a/lib/spack/spack/schema/mirrors.py b/lib/spack/spack/schema/mirrors.py
index 8001172afd..99e57b3450 100644
--- a/lib/spack/spack/schema/mirrors.py
+++ b/lib/spack/spack/schema/mirrors.py
@@ -42,6 +42,7 @@ mirror_entry = {
"properties": {
"source": {"type": "boolean"},
"binary": {"type": "boolean"},
+ "signed": {"type": "boolean"},
"fetch": fetch_and_push,
"push": fetch_and_push,
**connection, # type: ignore
diff --git a/lib/spack/spack/test/cmd/buildcache.py b/lib/spack/spack/test/cmd/buildcache.py
index 55ec605913..e335c6e3dd 100644
--- a/lib/spack/spack/test/cmd/buildcache.py
+++ b/lib/spack/spack/test/cmd/buildcache.py
@@ -331,3 +331,37 @@ def test_correct_specs_are_pushed(
# Ensure no duplicates
assert len(set(packages_to_push)) == len(packages_to_push)
+
+
+@pytest.mark.parametrize("signed", [True, False])
+def test_push_and_install_with_mirror_marked_unsigned_does_not_require_extra_flags(
+ tmp_path, mutable_database, mock_gnupghome, signed
+):
+ """Tests whether marking a mirror as unsigned makes it possible to push and install to/from
+ it without requiring extra flags on the command line (and no signing keys configured)."""
+
+ # Create a named mirror with signed set to True or False
+ add_flag = "--signed" if signed else "--unsigned"
+ mirror("add", add_flag, "my-mirror", str(tmp_path))
+ spec = mutable_database.query_local("libelf", installed=True)[0]
+
+ # Push
+ if signed:
+ # Need to pass "--unsigned" to override the mirror's default
+ args = ["push", "--update-index", "--unsigned", "my-mirror", f"/{spec.dag_hash()}"]
+ else:
+ # No need to pass "--unsigned" if the mirror is unsigned
+ args = ["push", "--update-index", "my-mirror", f"/{spec.dag_hash()}"]
+
+ buildcache(*args)
+
+ # Install
+ if signed:
+ # Need to pass "--no-check-signature" to avoid install errors
+ kwargs = {"cache_only": True, "unsigned": True}
+ else:
+ # No need to pass "--no-check-signature" if the mirror is unsigned
+ kwargs = {"cache_only": True}
+
+ spec.package.do_uninstall(force=True)
+ spec.package.do_install(**kwargs)
diff --git a/lib/spack/spack/test/cmd/mirror.py b/lib/spack/spack/test/cmd/mirror.py
index 029765df93..61021c3183 100644
--- a/lib/spack/spack/test/cmd/mirror.py
+++ b/lib/spack/spack/test/cmd/mirror.py
@@ -398,3 +398,12 @@ def test_mirror_set_2(mutable_config):
"url": "http://example.com",
"push": {"url": "http://example2.com", "access_pair": ["username", "password"]},
}
+
+
+def test_mirror_add_set_signed(mutable_config):
+ mirror("add", "--signed", "example", "http://example.com")
+ assert spack.config.get("mirrors:example") == {"url": "http://example.com", "signed": True}
+ mirror("set", "--unsigned", "example")
+ assert spack.config.get("mirrors:example") == {"url": "http://example.com", "signed": False}
+ mirror("set", "--signed", "example")
+ assert spack.config.get("mirrors:example") == {"url": "http://example.com", "signed": True}