sign.py revision 3447
660N/A#!/usr/bin/python2.7
660N/A#
660N/A# CDDL HEADER START
660N/A#
660N/A# The contents of this file are subject to the terms of the
660N/A# Common Development and Distribution License (the "License").
660N/A# You may not use this file except in compliance with the License.
660N/A#
660N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
660N/A# or http://www.opensolaris.org/os/licensing.
660N/A# See the License for the specific language governing permissions
660N/A# and limitations under the License.
660N/A#
660N/A# When distributing Covered Code, include this CDDL HEADER in each
660N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
660N/A# If applicable, add the following below this CDDL HEADER, with the
660N/A# fields enclosed by brackets "[]" replaced with your own identifying
679N/A# information: Portions Copyright [yyyy] [name of copyright owner]
678N/A#
678N/A# CDDL HEADER END
678N/A#
679N/A
678N/A#
679N/A# Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
678N/A#
679N/A
678N/Aimport getopt
678N/Aimport gettext
678N/Aimport hashlib
679N/Aimport locale
678N/Aimport os
679N/Aimport shutil
678N/Aimport sys
679N/Aimport tempfile
678N/Aimport traceback
678N/Aif sys.version_info[:2] >= (3, 4):
678N/A from importlib import reload
679N/Aelse:
678N/A from imp import reload
678N/A
678N/Afrom cryptography import x509
679N/Afrom cryptography.hazmat.backends import default_backend
678N/Afrom cryptography.hazmat.primitives import serialization
679N/Afrom imp import reload
678N/A
679N/Aimport pkg
678N/Aimport pkg.actions as actions
678N/Aimport pkg.client.api_errors as api_errors
678N/Aimport pkg.client.transport.transport as transport
679N/Aimport pkg.digest as digest
678N/Aimport pkg.fmri as fmri
678N/Aimport pkg.manifest as manifest
678N/Aimport pkg.misc as misc
679N/Aimport pkg.publish.transaction as trans
678N/Afrom pkg.client import global_settings
678N/Afrom pkg.client.debugvalues import DebugValues
678N/Afrom pkg.misc import emsg, msg, PipeError
679N/A
678N/APKG_CLIENT_NAME = "pkgsign"
678N/A
678N/A# pkg exit codes
679N/AEXIT_OK = 0
679N/AEXIT_OOPS = 1
678N/AEXIT_BADOPT = 2
678N/AEXIT_PARTIAL = 3
678N/A
679N/Arepo_cache = {}
679N/A
679N/Adef error(text, cmd=None):
679N/A """Emit an error message prefixed by the command name """
679N/A
679N/A if cmd:
679N/A text = "{0}: {1}".format(cmd, text)
679N/A
678N/A else:
679N/A text = "{0}: {1}".format(PKG_CLIENT_NAME, text)
679N/A
660N/A
679N/A # If the message starts with whitespace, assume that it should come
679N/A # *before* the command-name prefix.
679N/A text_nows = text.lstrip()
679N/A ws = text[:len(text) - len(text_nows)]
679N/A
679N/A # This has to be a constant value as we can't reliably get our actual
679N/A # program name on all platforms.
679N/A emsg(ws + text_nows)
679N/A
679N/Adef usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT):
679N/A """Emit a usage message and optionally prefix it with a more specific
679N/A error message. Causes program to exit."""
679N/A
679N/A if usage_error:
679N/A error(usage_error, cmd=cmd)
660N/A emsg (_("""\
679N/AUsage:
679N/A pkgsign -s path_or_uri [-acikn] [--no-index] [--no-catalog]
679N/A (fmri|pattern) ...
679N/A"""))
679N/A
679N/A sys.exit(retcode)
679N/A
679N/Adef fetch_catalog(src_pub, xport, temp_root):
679N/A """Fetch the catalog from src_uri."""
679N/A
679N/A if not src_pub.meta_root:
679N/A # Create a temporary directory for catalog.
679N/A cat_dir = tempfile.mkdtemp(dir=temp_root)
679N/A src_pub.meta_root = cat_dir
679N/A
679N/A src_pub.transport = xport
679N/A src_pub.refresh(True, True)
679N/A
679N/A return src_pub.catalog
679N/A
679N/Adef __make_tmp_cert(d, pth):
679N/A try:
679N/A with open(pth, "rb") as f:
679N/A cert = x509.load_pem_x509_certificate(f.read(),
679N/A default_backend())
679N/A except (ValueError, IOError) as e:
679N/A raise api_errors.BadFileFormat(_("The file {0} was expected to "
679N/A "be a PEM certificate but it could not be read.").format(
679N/A pth))
679N/A fd, fp = tempfile.mkstemp(dir=d)
679N/A with os.fdopen(fd, "wb") as fh:
678N/A fh.write(cert.public_bytes(serialization.Encoding.PEM))
679N/A return fp
679N/A
679N/Adef main_func():
679N/A global_settings.client_name = "pkgsign"
679N/A
679N/A try:
679N/A opts, pargs = getopt.getopt(sys.argv[1:], "a:c:i:k:ns:D:",
679N/A ["help", "no-index", "no-catalog"])
679N/A except getopt.GetoptError as e:
679N/A usage(_("illegal global option -- {0}").format(e.opt))
679N/A
679N/A show_usage = False
679N/A sig_alg = "rsa-sha256"
679N/A cert_path = None
679N/A key_path = None
679N/A chain_certs = []
679N/A add_to_catalog = True
679N/A set_alg = False
679N/A dry_run = False
679N/A
679N/A repo_uri = os.getenv("PKG_REPO", None)
679N/A for opt, arg in opts:
679N/A if opt == "-a":
679N/A sig_alg = arg
679N/A set_alg = True
679N/A elif opt == "-c":
679N/A cert_path = os.path.abspath(arg)
679N/A if not os.path.isfile(cert_path):
679N/A usage(_("{0} was expected to be a certificate "
679N/A "but isn't a file.").format(cert_path))
679N/A elif opt == "-i":
679N/A p = os.path.abspath(arg)
679N/A if not os.path.isfile(p):
679N/A usage(_("{0} was expected to be a certificate "
679N/A "but isn't a file.").format(p))
679N/A chain_certs.append(p)
679N/A elif opt == "-k":
679N/A key_path = os.path.abspath(arg)
679N/A if not os.path.isfile(key_path):
679N/A usage(_("{0} was expected to be a key file "
679N/A "but isn't a file.").format(key_path))
679N/A elif opt == "-n":
679N/A dry_run = True
679N/A elif opt == "-s":
679N/A repo_uri = misc.parse_uri(arg)
679N/A elif opt == "--help":
679N/A show_usage = True
660N/A elif opt == "--no-catalog":
660N/A add_to_catalog = False
660N/A elif opt == "-D":
660N/A try:
660N/A key, value = arg.split("=", 1)
660N/A DebugValues.set_value(key, value)
679N/A except (AttributeError, ValueError):
679N/A error(_("{opt} takes argument of form "
660N/A "name=value, not {arg}").format(
660N/A opt=opt, arg=arg))
660N/A if show_usage:
679N/A usage(retcode=EXIT_OK)
679N/A
679N/A if not repo_uri:
678N/A usage(_("a repository must be provided"))
679N/A
660N/A if key_path and not cert_path:
660N/A usage(_("If a key is given to sign with, its associated "
660N/A "certificate must be given."))
679N/A
660N/A if cert_path and not key_path:
660N/A usage(_("If a certificate is given, its associated key must be "
660N/A "given."))
679N/A
660N/A if chain_certs and not cert_path:
660N/A usage(_("Intermediate certificates are only valid if a key "
660N/A "and certificate are also provided."))
679N/A
660N/A if not pargs:
660N/A usage(_("At least one fmri or pattern must be provided to "
660N/A "sign."))
679N/A
679N/A if not set_alg and not key_path:
679N/A sig_alg = "sha256"
679N/A
679N/A s, h = actions.signature.SignatureAction.decompose_sig_alg(sig_alg)
679N/A if h is None:
679N/A usage(_("{0} is not a recognized signature algorithm.").format(
679N/A sig_alg))
679N/A if s and not key_path:
679N/A usage(_("Using {0} as the signature algorithm requires that a "
679N/A "key and certificate pair be presented using the -k and -c "
679N/A "options.").format(sig_alg))
679N/A if not s and key_path:
679N/A usage(_("The {0} hash algorithm does not use a key or "
679N/A "certificate. Do not use the -k or -c options with this "
679N/A "algorithm.").format(sig_alg))
679N/A
679N/A if DebugValues:
679N/A reload(digest)
679N/A
679N/A errors = []
679N/A
679N/A t = misc.config_temp_root()
679N/A temp_root = tempfile.mkdtemp(dir=t)
679N/A del t
679N/A
679N/A cache_dir = tempfile.mkdtemp(dir=temp_root)
679N/A incoming_dir = tempfile.mkdtemp(dir=temp_root)
679N/A chash_dir = tempfile.mkdtemp(dir=temp_root)
679N/A cert_dir = tempfile.mkdtemp(dir=temp_root)
679N/A
679N/A try:
679N/A chain_certs = [
679N/A __make_tmp_cert(cert_dir, c) for c in chain_certs
679N/A ]
679N/A if cert_path is not None:
679N/A cert_path = __make_tmp_cert(cert_dir, cert_path)
679N/A
679N/A xport, xport_cfg = transport.setup_transport()
679N/A xport_cfg.add_cache(cache_dir, readonly=False)
679N/A xport_cfg.incoming_root = incoming_dir
679N/A
679N/A # Configure publisher(s)
679N/A transport.setup_publisher(repo_uri, "source", xport,
679N/A xport_cfg, remote_prefix=True)
679N/A pats = pargs
679N/A successful_publish = False
679N/A
679N/A concrete_fmris = []
679N/A unmatched_pats = set(pats)
679N/A all_pats = frozenset(pats)
679N/A get_all_pubs = False
679N/A pub_prefs = set()
679N/A # Gather the publishers whose catalogs will be needed.
679N/A for pat in pats:
679N/A try:
679N/A p_obj = fmri.MatchingPkgFmri(pat)
679N/A except fmri.IllegalMatchingFmri as e:
679N/A errors.append(e)
679N/A continue
679N/A pub_prefix = p_obj.get_publisher()
679N/A if pub_prefix:
679N/A pub_prefs.add(pub_prefix)
679N/A else:
679N/A get_all_pubs = True
679N/A # Check each publisher for matches to our patterns.
679N/A for p in xport_cfg.gen_publishers():
679N/A if not get_all_pubs and p.prefix not in pub_prefs:
679N/A continue
679N/A cat = fetch_catalog(p, xport, temp_root)
679N/A ms, tmp1, u = cat.get_matching_fmris(pats)
679N/A # Find which patterns matched.
679N/A matched_pats = all_pats - u
679N/A # Remove those patterns from the unmatched set.
679N/A unmatched_pats -= matched_pats
679N/A for v_list in ms.values():
679N/A concrete_fmris.extend([(v, p) for v in v_list])
679N/A if unmatched_pats:
679N/A raise api_errors.PackageMatchErrors(
679N/A unmatched_fmris=unmatched_pats)
679N/A
679N/A for pfmri, src_pub in sorted(set(concrete_fmris)):
679N/A try:
679N/A # Get the existing manifest for the package to
679N/A # be signed.
679N/A m_str = xport.get_manifest(pfmri,
679N/A content_only=True, pub=src_pub)
679N/A m = manifest.Manifest()
679N/A m.set_content(content=m_str)
679N/A
679N/A # Construct the base signature action.
679N/A attrs = { "algorithm": sig_alg }
679N/A a = actions.signature.SignatureAction(cert_path,
679N/A **attrs)
679N/A a.hash = cert_path
679N/A
679N/A # Add the action to the manifest to be signed
679N/A # since the action signs itself.
679N/A m.add_action(a, misc.EmptyI)
679N/A
679N/A # Set the signature value and certificate
679N/A # information for the signature action.
679N/A a.set_signature(m.gen_actions(),
679N/A key_path=key_path, chain_paths=chain_certs,
679N/A chash_dir=chash_dir)
679N/A
679N/A # The hash of 'a' is currently a path, we need
679N/A # to find the hash of that file to allow
679N/A # comparison to existing signatures.
679N/A hsh = None
679N/A if cert_path:
679N/A # Action identity still uses the 'hash'
679N/A # member of the action, so we need to
679N/A # stay with the sha1 hash.
679N/A hsh, _dummy = \
679N/A misc.get_data_digest(cert_path,
679N/A hash_func=hashlib.sha1)
679N/A
679N/A # Check whether the signature about to be added
679N/A # is identical, or almost identical, to existing
679N/A # signatures on the package. Because 'a' has
679N/A # already been added to the manifest, it is
679N/A # generated by gen_actions_by_type, so the cnt
679N/A # must be 2 or higher to be an issue.
679N/A cnt = 0
679N/A almost_identical = False
679N/A for a2 in m.gen_actions_by_type("signature"):
679N/A try:
679N/A if a.identical(a2, hsh):
679N/A cnt += 1
679N/A except api_errors.AlmostIdentical as e:
679N/A e.pkg = pfmri
679N/A errors.append(e)
679N/A almost_identical = True
679N/A if almost_identical:
679N/A continue
679N/A if cnt == 2:
679N/A continue
679N/A elif cnt > 2:
679N/A raise api_errors.DuplicateSignaturesAlreadyExist(pfmri)
679N/A assert cnt == 1, "Cnt was:{0}".format(cnt)
679N/A
679N/A if not dry_run:
679N/A # Append the finished signature action
679N/A # to the published manifest.
679N/A t = trans.Transaction(repo_uri,
679N/A pkg_name=str(pfmri), xport=xport,
679N/A pub=src_pub)
679N/A try:
679N/A t.append()
679N/A t.add(a)
679N/A for c in chain_certs:
679N/A t.add_file(c)
679N/A t.close(add_to_catalog=
679N/A add_to_catalog)
679N/A except:
679N/A if t.trans_id:
679N/A t.close(abandon=True)
679N/A raise
679N/A msg(_("Signed {0}").format(pfmri.get_fmri(
679N/A include_build=False)))
679N/A successful_publish = True
679N/A except (api_errors.ApiException, fmri.FmriError,
679N/A trans.TransactionError) as e:
679N/A errors.append(e)
679N/A if errors:
679N/A error("\n".join([str(e) for e in errors]))
679N/A if successful_publish:
679N/A return EXIT_PARTIAL
679N/A else:
679N/A return EXIT_OOPS
679N/A return EXIT_OK
679N/A except api_errors.ApiException as e:
679N/A error(e)
679N/A return EXIT_OOPS
679N/A finally:
679N/A shutil.rmtree(temp_root)
679N/A
679N/A#
679N/A# Establish a specific exit status which means: "python barfed an exception"
679N/A# so that we can more easily detect these in testing of the CLI commands.
679N/A#
679N/Aif __name__ == "__main__":
679N/A misc.setlocale(locale.LC_ALL, "", error)
679N/A gettext.install("pkg", "/usr/share/locale",
679N/A codeset=locale.getpreferredencoding())
679N/A misc.set_fd_limits(printer=error)
679N/A
679N/A try:
679N/A __ret = main_func()
679N/A except (PipeError, KeyboardInterrupt):
679N/A # We don't want to display any messages here to prevent
679N/A # possible further broken pipe (EPIPE) errors.
679N/A __ret = EXIT_OOPS
679N/A except SystemExit as _e:
679N/A raise _e
679N/A except:
679N/A traceback.print_exc()
679N/A error(misc.get_traceback_message())
679N/A __ret = 99
679N/A sys.exit(__ret)
679N/A