# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import argparse
import os
import spack.binary_distribution
import spack.mirror
import spack.paths
import spack.util.gpg
import spack.util.url
from spack.cmd.common import arguments
description = "handle GPG actions for spack"
section = "packaging"
level = "long"
[docs]
def setup_parser(subparser):
setup_parser.parser = subparser
subparsers = subparser.add_subparsers(help="GPG sub-commands")
verify = subparsers.add_parser("verify", help=gpg_verify.__doc__)
arguments.add_common_arguments(verify, ["installed_spec"])
verify.add_argument("signature", type=str, nargs="?", help="the signature file")
verify.set_defaults(func=gpg_verify)
trust = subparsers.add_parser("trust", help=gpg_trust.__doc__)
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", help=gpg_untrust.__doc__)
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", help=gpg_sign.__doc__)
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"
)
arguments.add_common_arguments(sign, ["installed_spec"])
sign.set_defaults(func=gpg_sign)
create = subparsers.add_parser("create", help=gpg_create.__doc__)
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.add_argument(
"--export-secret",
metavar="DEST",
type=str,
dest="secret",
help="export the private key to a file",
)
create.set_defaults(func=gpg_create)
list = subparsers.add_parser("list", help=gpg_list.__doc__)
list.add_argument("--trusted", action="store_true", default=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", help=gpg_init.__doc__)
init.add_argument("--from", metavar="DIR", type=str, dest="import_dir", help=argparse.SUPPRESS)
init.set_defaults(func=gpg_init)
export = subparsers.add_parser("export", help=gpg_export.__doc__)
export.add_argument("location", type=str, help="where to export keys")
export.add_argument(
"keys", nargs="*", help="the keys to export (all public keys if unspecified)"
)
export.add_argument("--secret", action="store_true", help="export secret keys")
export.set_defaults(func=gpg_export)
publish = subparsers.add_parser("publish", help=gpg_publish.__doc__)
output = publish.add_mutually_exclusive_group(required=True)
output.add_argument(
"-d",
"--directory",
metavar="directory",
type=str,
help="local directory where keys will be published",
)
output.add_argument(
"-m",
"--mirror-name",
metavar="mirror-name",
type=str,
help="name of the mirror where keys will be published",
)
output.add_argument(
"--mirror-url",
metavar="mirror-url",
type=str,
help="URL of the mirror where keys will be published",
)
publish.add_argument(
"--rebuild-index",
action="store_true",
default=False,
help="regenerate buildcache key index after publishing key(s)",
)
publish.add_argument(
"keys", nargs="*", help="keys to publish (all public keys if unspecified)"
)
publish.set_defaults(func=gpg_publish)
[docs]
def gpg_create(args):
"""create a new key"""
if args.export or args.secret:
old_sec_keys = spack.util.gpg.signing_keys()
# Create the new key
spack.util.gpg.create(
name=args.name, email=args.email, comment=args.comment, expires=args.expires
)
if args.export or args.secret:
new_sec_keys = set(spack.util.gpg.signing_keys())
new_keys = new_sec_keys.difference(old_sec_keys)
if args.export:
spack.util.gpg.export_keys(args.export, new_keys)
if args.secret:
spack.util.gpg.export_keys(args.secret, new_keys, secret=True)
[docs]
def gpg_export(args):
"""export a gpg key, optionally including secret key"""
keys = args.keys
if not keys:
keys = spack.util.gpg.signing_keys()
spack.util.gpg.export_keys(args.location, keys, args.secret)
[docs]
def gpg_list(args):
"""list keys available in the keyring"""
spack.util.gpg.list(args.trusted, args.signing)
[docs]
def gpg_sign(args):
"""sign a package"""
key = args.key
if key is None:
keys = spack.util.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.spec[0] + ".asc"
# TODO: Support the package format Spack creates.
spack.util.gpg.sign(key, " ".join(args.spec), output, args.clearsign)
[docs]
def gpg_trust(args):
"""add a key to the keyring"""
spack.util.gpg.trust(args.keyfile)
[docs]
def gpg_init(args):
"""add the default keys to the keyring"""
import_dir = args.import_dir
if import_dir is None:
import_dir = spack.paths.gpg_keys_path
for root, _, filenames in os.walk(import_dir):
for filename in filenames:
if not filename.endswith(".key"):
continue
spack.util.gpg.trust(os.path.join(root, filename))
[docs]
def gpg_untrust(args):
"""remove a key from the keyring"""
spack.util.gpg.untrust(args.signing, *args.keys)
[docs]
def gpg_verify(args):
"""verify a signed package"""
# TODO: Support the package format Spack creates.
signature = args.signature
if signature is None:
signature = args.spec[0] + ".asc"
spack.util.gpg.verify(signature, " ".join(args.spec))
[docs]
def gpg_publish(args):
"""publish public keys to a build cache"""
mirror = None
if args.directory:
url = spack.util.url.path_to_file_url(args.directory)
mirror = spack.mirror.Mirror(url, url)
elif args.mirror_name:
mirror = spack.mirror.MirrorCollection(binary=True).lookup(args.mirror_name)
elif args.mirror_url:
mirror = spack.mirror.Mirror(args.mirror_url, args.mirror_url)
spack.binary_distribution.push_keys(
mirror, keys=args.keys, regenerate_index=args.rebuild_index
)
[docs]
def gpg(parser, args):
if args.func:
args.func(args)