sign.py revision 2286
2026N/A#!/usr/bin/python2.6
2026N/A#
2026N/A# CDDL HEADER START
2026N/A#
2026N/A# The contents of this file are subject to the terms of the
2026N/A# Common Development and Distribution License (the "License").
2026N/A# You may not use this file except in compliance with the License.
2026N/A#
2026N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
2026N/A# or http://www.opensolaris.org/os/licensing.
2026N/A# See the License for the specific language governing permissions
2026N/A# and limitations under the License.
2026N/A#
2026N/A# When distributing Covered Code, include this CDDL HEADER in each
2026N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
2026N/A# If applicable, add the following below this CDDL HEADER, with the
2026N/A# fields enclosed by brackets "[]" replaced with your own identifying
2026N/A# information: Portions Copyright [yyyy] [name of copyright owner]
2026N/A#
2026N/A# CDDL HEADER END
2026N/A#
2026N/A
2026N/A#
2245N/A# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
2026N/A#
2026N/A
2026N/Aimport getopt
2026N/Aimport gettext
2026N/Aimport locale
2026N/Aimport os
2026N/Aimport shutil
2026N/Aimport sys
2026N/Aimport tempfile
2026N/Aimport traceback
2026N/A
2026N/Aimport pkg
2026N/Aimport pkg.actions as actions
2026N/Aimport pkg.client.api_errors as api_errors
2026N/Aimport pkg.client.transport.transport as transport
2026N/Aimport pkg.fmri as fmri
2026N/Aimport pkg.manifest as manifest
2026N/Aimport pkg.misc as misc
2026N/Aimport pkg.publish.transaction as trans
2026N/Afrom pkg.client import global_settings
2026N/Afrom pkg.misc import emsg, msg, PipeError
2026N/A
2026N/APKG_CLIENT_NAME = "pkgsign"
2026N/A
2026N/A# pkg exit codes
2026N/AEXIT_OK = 0
2026N/AEXIT_OOPS = 1
2026N/AEXIT_BADOPT = 2
2026N/AEXIT_PARTIAL = 3
2026N/A
2026N/Arepo_cache = {}
2026N/A
2026N/Adef error(text, cmd=None):
2026N/A """Emit an error message prefixed by the command name """
2026N/A
2026N/A if cmd:
2026N/A text = "%s: %s" % (cmd, text)
2026N/A
2026N/A else:
2026N/A text = "%s: %s" % (PKG_CLIENT_NAME, text)
2026N/A
2026N/A
2026N/A # If the message starts with whitespace, assume that it should come
2026N/A # *before* the command-name prefix.
2026N/A text_nows = text.lstrip()
2026N/A ws = text[:len(text) - len(text_nows)]
2026N/A
2026N/A # This has to be a constant value as we can't reliably get our actual
2026N/A # program name on all platforms.
2026N/A emsg(ws + text_nows)
2026N/A
2026N/Adef usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT):
2026N/A """Emit a usage message and optionally prefix it with a more specific
2026N/A error message. Causes program to exit."""
2026N/A
2026N/A if usage_error:
2026N/A error(usage_error, cmd=cmd)
2026N/A emsg (_("""\
2026N/AUsage:
2026N/A pkgsign -s path_or_uri [-acik] [--no-index] [--no-catalog]
2026N/A [--sign-all | fmri-to-sign ...]
2026N/A"""))
2268N/A
2026N/A sys.exit(retcode)
2026N/A
2026N/Adef fetch_catalog(src_pub, xport, temp_root, list_packages=False):
2026N/A """Fetch the catalog from src_uri."""
2026N/A
2026N/A if not src_pub.meta_root:
2026N/A # Create a temporary directory for catalog.
2026N/A cat_dir = tempfile.mkdtemp(dir=temp_root)
2026N/A src_pub.meta_root = cat_dir
2026N/A
2026N/A src_pub.transport = xport
2026N/A src_pub.refresh(True, True)
2026N/A
2026N/A if not list_packages:
2026N/A return
2026N/A
2026N/A cat = src_pub.catalog
2026N/A
2026N/A d = {}
2026N/A fmri_list = []
2026N/A for f in cat.fmris():
2026N/A fmri_list.append(f)
2026N/A d.setdefault(f.pkg_name, [f]).append(f)
2026N/A for k in d.keys():
2026N/A d[k].sort(reverse=True)
2026N/A
2026N/A return fmri_list
2026N/A
2026N/Adef main_func():
2026N/A misc.setlocale(locale.LC_ALL, "", error)
2026N/A gettext.install("pkg", "/usr/share/locale")
2026N/A global_settings.client_name = "pkgsign"
2026N/A
2026N/A try:
2026N/A opts, pargs = getopt.getopt(sys.argv[1:], "a:c:i:k:s:",
2026N/A ["help", "no-index", "no-catalog", "sign-all"])
2026N/A except getopt.GetoptError, e:
2026N/A usage(_("illegal global option -- %s") % e.opt)
2026N/A
2026N/A show_usage = False
2026N/A sig_alg = "rsa-sha256"
2026N/A cert_path = None
2026N/A key_path = None
2026N/A chain_certs = []
2026N/A add_to_catalog = True
2026N/A set_alg = False
2026N/A sign_all = False
2026N/A
2026N/A repo_uri = os.getenv("PKG_REPO", None)
2026N/A for opt, arg in opts:
2026N/A if opt == "-a":
2026N/A sig_alg = arg
2026N/A set_alg = True
2026N/A elif opt == "-c":
2026N/A cert_path = os.path.abspath(arg)
2026N/A if not os.path.isfile(cert_path):
2268N/A usage(_("%s was expected to be a certificate "
2026N/A "but isn't a file.") % cert_path)
2026N/A elif opt == "-i":
2026N/A p = os.path.abspath(arg)
2026N/A if not os.path.isfile(p):
2026N/A usage(_("%s was expected to be a certificate "
2026N/A "but isn't a file.") % p)
2026N/A chain_certs.append(p)
2026N/A elif opt == "-k":
2026N/A key_path = os.path.abspath(arg)
2026N/A if not os.path.isfile(key_path):
2026N/A usage(_("%s was expected to be a key file "
2026N/A "but isn't a file.") % key_path)
2026N/A elif opt == "-s":
2026N/A repo_uri = misc.parse_uri(arg)
2026N/A elif opt == "--help":
2026N/A show_usage = True
2026N/A elif opt == "--no-catalog":
2026N/A add_to_catalog = False
2026N/A elif opt == "--sign-all":
2026N/A sign_all = True
2026N/A
2268N/A if show_usage:
2026N/A usage(retcode=EXIT_OK)
2026N/A
2026N/A if not repo_uri:
2026N/A usage(_("a repository must be provided"))
2026N/A
2026N/A if key_path and not cert_path:
2026N/A usage(_("If a key is given to sign with, its associated "
2026N/A "certificate must be given."))
2026N/A
2026N/A if cert_path and not key_path:
2268N/A usage(_("If a certificate is given, its associated key must be "
2268N/A "given."))
2268N/A
2026N/A if chain_certs and not cert_path:
2026N/A usage(_("Intermediate certificates are only valid if a key "
2026N/A "and certificate are also provided."))
2026N/A
2026N/A if not pargs and not sign_all:
2026N/A usage(_("At least one fmri must be provided for signing."))
2026N/A
2026N/A if pargs and sign_all:
2026N/A usage(_("No fmris may be provided if the sign-all option is "
2026N/A "set."))
2026N/A
2026N/A if not set_alg and not key_path:
2026N/A sig_alg = "sha256"
2026N/A
2026N/A s, h = actions.signature.SignatureAction.decompose_sig_alg(sig_alg)
2026N/A if h is None:
2026N/A usage(_("%s is not a recognized signature algorithm.") %
2026N/A sig_alg)
2026N/A if s and not key_path:
2026N/A usage(_("Using %s as the signature algorithm requires that a "
2026N/A "key and certificate pair be presented using the -k and -c "
2026N/A "options.") % sig_alg)
2026N/A if not s and key_path:
2026N/A usage(_("The %s hash algorithm does not use a key or "
2026N/A "certificate. Do not use the -k or -c options with this "
2026N/A "algorithm.") % sig_alg)
2026N/A
2026N/A errors = []
2026N/A
2026N/A t = misc.config_temp_root()
2026N/A temp_root = tempfile.mkdtemp(dir=t)
2026N/A del t
2026N/A
2026N/A cache_dir = tempfile.mkdtemp(dir=temp_root)
2026N/A incoming_dir = tempfile.mkdtemp(dir=temp_root)
2026N/A chash_dir = tempfile.mkdtemp(dir=temp_root)
2026N/A
2026N/A try:
2026N/A xport, xport_cfg = transport.setup_transport()
2026N/A xport_cfg.add_cache(cache_dir, readonly=False)
2026N/A xport_cfg.incoming_root = incoming_dir
2026N/A
2026N/A # Configure src publisher
2026N/A src_pub = transport.setup_publisher(repo_uri, "source", xport,
2026N/A xport_cfg, remote_prefix=True)
2026N/A fmris = fetch_catalog(src_pub, xport, temp_root,
2073N/A list_packages=sign_all)
2073N/A if not sign_all:
2026N/A fmris = pargs
2026N/A successful_publish = False
2026N/A
2026N/A for pfmri in fmris:
2026N/A try:
2026N/A if isinstance(pfmri, basestring):
2026N/A pfmri = fmri.PkgFmri(pfmri)
2026N/A
2028N/A # Get the existing manifest for the package to
2026N/A # be sign.
2026N/A m_str = xport.get_manifest(pfmri,
2026N/A content_only=True, pub=src_pub)
2026N/A m = manifest.Manifest()
2026N/A m.set_content(content=m_str)
2026N/A
2026N/A # Construct the base signature action.
2026N/A attrs = { "algorithm": sig_alg }
2026N/A a = actions.signature.SignatureAction(cert_path,
2026N/A **attrs)
2026N/A a.hash = cert_path
2073N/A
2026N/A # Add the action to the manifest to be signed
2026N/A # since the action signs itself.
2026N/A m.add_action(a, misc.EmptyI)
2026N/A
2026N/A # Set the signature value and certificate
2026N/A # information for the signature action.
2026N/A a.set_signature(m.gen_actions(),
2026N/A key_path=key_path, chain_paths=chain_certs,
2026N/A chash_dir=chash_dir)
2026N/A
2026N/A # The hash of 'a' is currently a path, we need
2026N/A # to find the hash of that file to allow
2026N/A # comparison to existing signatures.
2026N/A hsh = None
2026N/A if cert_path:
2026N/A hsh, _dummy = \
2026N/A misc.get_data_digest(cert_path)
2026N/A
2026N/A # Check whether the signature about to be added
2026N/A # is identical, or almost identical, to existing
2028N/A # signatures on the package. Because 'a' has
2026N/A # already been added to the manifest, it is
2026N/A # generated by gen_actions_by_type, so the cnt
2026N/A # must be 2 or higher to be an issue.
2026N/A cnt = 0
2026N/A almost_identical = False
2028N/A for a2 in m.gen_actions_by_type("signature"):
2026N/A try:
2026N/A if a.identical(a2, hsh):
2026N/A cnt += 1
2026N/A except api_errors.AlmostIdentical, e:
2028N/A e.pkg = pfmri
2026N/A errors.append(e)
2026N/A almost_identical = True
2026N/A if almost_identical:
2026N/A continue
2026N/A if cnt == 2:
2028N/A continue
2026N/A elif cnt > 2:
2026N/A raise api_errors.DuplicateSignaturesAlreadyExist(pfmri)
2026N/A assert cnt == 1, "Cnt was:%s" % cnt
2026N/A
2032N/A # Append the finished signature action to the
2026N/A # published manifest.
2026N/A t = trans.Transaction(repo_uri,
2026N/A pkg_name=str(pfmri), xport=xport,
2245N/A pub=src_pub)
2026N/A try:
2026N/A t.append()
2026N/A t.add(a)
2026N/A for c in chain_certs:
2026N/A t.add_file(c)
2026N/A t.close(add_to_catalog=add_to_catalog)
2026N/A except:
2026N/A if t.trans_id:
2026N/A t.close(abandon=True)
2026N/A raise
2026N/A msg(_("Signed %s") % pfmri)
2026N/A successful_publish = True
2026N/A except (api_errors.ApiException, fmri.FmriError,
2026N/A trans.TransactionError), e:
2026N/A errors.append(e)
2026N/A if errors:
2126N/A error("\n".join([str(e) for e in errors]))
2126N/A if successful_publish:
2126N/A return EXIT_PARTIAL
2126N/A else:
2126N/A return EXIT_OOPS
2126N/A return EXIT_OK
2026N/A except api_errors.ApiException, e:
2026N/A error(e)
return EXIT_OOPS
finally:
shutil.rmtree(temp_root)
#
# Establish a specific exit status which means: "python barfed an exception"
# so that we can more easily detect these in testing of the CLI commands.
#
if __name__ == "__main__":
try:
__ret = main_func()
except (PipeError, KeyboardInterrupt):
# We don't want to display any messages here to prevent
# possible further broken pipe (EPIPE) errors.
__ret = EXIT_OOPS
except SystemExit, _e:
raise _e
except:
traceback.print_exc()
error(_("""\n
This is an internal error in pkg(5) version %(version)s. Please let the
developers know about this problem by including the information above (and
this message) when filing a bug at:
%(bug_uri)s""") % { "version": pkg.VERSION, "bug_uri": misc.BUG_URI_CLI })
__ret = 99
sys.exit(__ret)