src/sign.py
author Brock Pytlik <bpytlik@sun.com>
Mon, 16 Aug 2010 16:48:50 -0700
changeset 2026 d1b30615bc99
child 2028 b2c674e6ee28
permissions -rwxr-xr-x
9196 pkg(5) should have support for cryptographic manifest signatures 11611 pkg5 should provide for hash validation on manifests 16654 Expose ability to upload by file path

#!/usr/bin/python2.6
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#

#
# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
#

import getopt
import gettext
import locale
import os
import shutil
import sys
import tempfile
import traceback
import urllib
import urlparse

import pkg
import pkg.actions as actions
import pkg.client.api_errors as api_errors
import pkg.client.transport.transport as transport
import pkg.config as cfg
import pkg.fmri as fmri
import pkg.manifest as manifest
import pkg.misc as misc
import pkg.publish.transaction as trans
import pkg.server.repository as sr
from pkg.client import global_settings
from pkg.misc import emsg, msg, PipeError

PKG_CLIENT_NAME = "pkgsign"

# pkg exit codes
EXIT_OK      = 0
EXIT_OOPS    = 1
EXIT_BADOPT  = 2
EXIT_PARTIAL = 3

repo_cache = {}

def error(text, cmd=None):
        """Emit an error message prefixed by the command name """

        if cmd:
                text = "%s: %s" % (cmd, text)
                
        else:
                text = "%s: %s" % (PKG_CLIENT_NAME, text)


        # If the message starts with whitespace, assume that it should come
        # *before* the command-name prefix.
        text_nows = text.lstrip()
        ws = text[:len(text) - len(text_nows)]

        # This has to be a constant value as we can't reliably get our actual
        # program name on all platforms.
        emsg(ws + text_nows)

def usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT):
        """Emit a usage message and optionally prefix it with a more specific
        error message.  Causes program to exit."""

        if usage_error:
                error(usage_error, cmd=cmd)
        emsg (_("""\
Usage:
        pkgsign [-aciks] [--no-index] [--no-catalog]
            [--sign-all | fmri-to-sign ...]
"""))

        sys.exit(retcode)

def fetch_catalog(src_pub, xport, temp_root, list_packages=False):
        """Fetch the catalog from src_uri."""
        global complete_catalog

        src_uri = src_pub.selected_repository.origins[0].uri
        # tracker.catalog_start(src_uri)

        if not src_pub.meta_root:
                # Create a temporary directory for catalog.
                cat_dir = tempfile.mkdtemp(dir=temp_root)
                src_pub.meta_root = cat_dir

        src_pub.transport = xport
        src_pub.refresh(True, True)

        if not list_packages:
                return
        
        cat = src_pub.catalog

        d = {}
        fmri_list = []
        for f in cat.fmris():
                fmri_list.append(f)
                d.setdefault(f.pkg_name, [f]).append(f)
        for k in d.keys():
                d[k].sort(reverse=True)

        complete_catalog = d
        return fmri_list

def main_func():
        misc.setlocale(locale.LC_ALL, "", error)
        gettext.install("pkg", "/usr/share/locale")
        global_settings.client_name = "pkgsign"

        try:
                opts, pargs = getopt.getopt(sys.argv[1:], "a:c:i:k:s:",
                    ["help", "no-index", "no-catalog", "sign-all"])
        except getopt.GetoptError, e:
                usage(_("illegal global option -- %s") % e.opt)

        show_usage = False
        sig_alg = "rsa-sha256"
        cert_path = None
        key_path = None
        chain_certs = []
        refresh_index = True
        add_to_catalog = True
        set_alg = False
        sign_all = False

        try:
                repo_uri = os.environ["PKG_REPO"]
        except KeyError:
                repo_uri = "http://localhost:10000"
        
        for opt, arg in opts:
                if opt == "-a":
                        sig_alg = arg
                        set_alg = True
                elif opt == "-c":
                        cert_path = os.path.abspath(arg)
                        if not os.path.isfile(cert_path):
                                usage(_("%s was expected to be a certificate "
                                    "but isn't a file.") % cert_path)
                elif opt == "-i":
                        p = os.path.abspath(arg)
                        if not os.path.isfile(p):
                                usage(_("%s was expected to be a certificate "
                                    "but isn't a file.") % p)
                        chain_certs.append(p)
                elif opt == "-k":
                        key_path = os.path.abspath(arg)
                        if not os.path.isfile(key_path):
                                usage(_("%s was expected to be a key file "
                                    "but isn't a file.") % key_path)
                elif opt == "-s":
                        repo_uri = arg
                elif opt == "--help":
                        show_usage = True
                elif opt == "--no-index":
                        refresh_index = False
                elif opt == "--no-catalog":
                        add_to_catalog = False
                elif opt == "--sign-all":
                        sign_all = True

        if show_usage:
                usage(retcode=EXIT_OK)

        if key_path and not cert_path:
                usage(_("If a key is given to sign with, its associated "
                    "certificate must be given."))

        if cert_path and not key_path:
                usage(_("If a certificate is given, its associated key must be "
                    "given."))

        if chain_certs and not cert_path:
                usage(_("Intermediate certificates are only valid if a key "
                    "and certificate are also provided."))

        if not pargs and not sign_all:
                usage(_("At least one fmri must be provided for signing."))

        if pargs and sign_all:
                usage(_("No fmris may be provided if the sign-all option is "
                    "set."))

        if not set_alg and not key_path:
                sig_alg = "sha256"

        s, h = actions.signature.SignatureAction.decompose_sig_alg(sig_alg)
        if h is None:
                usage(_("%s is not a recognized signature algorithm.") %
                    sig_alg)
        if s and not key_path:
                usage(_("Using %s as the signature algorithm requires that a "
                    "key and certificate pair be presented using the -k and -c "
                    "options.") % sig_alg)
        if not s and key_path:
                usage(_("The %s hash algorithm does not use a key or "
                    "certificate.  Do not use the -k or -c options with this "
                    "algorithm.") % sig_alg)

        errors = []

        t = misc.config_temp_root()
        temp_root = tempfile.mkdtemp(dir=t)
        del t
        
        cache_dir = tempfile.mkdtemp(dir=temp_root)
        incoming_dir = tempfile.mkdtemp(dir=temp_root)

        try:
                xport, xport_cfg = transport.setup_transport()
                xport_cfg.cached_download_dir = cache_dir
                xport_cfg.incoming_download_dir = incoming_dir

                # Configure src publisher
                src_pub = transport.setup_publisher(repo_uri, "source", xport,
                    xport_cfg, remote_prefix=True)
                fmris = fetch_catalog(src_pub, xport, temp_root,
                    list_packages=sign_all)
                if not sign_all:
                        fmris = pargs
                succesful_publish = False

                for pfmri in fmris:
                        try:
                                if isinstance(pfmri, basestring):
                                        pfmri = fmri.PkgFmri(pfmri)

                                # Get the existing manifest for the package to
                                # be sign.
                                m_str = xport.get_manifest(pfmri,
                                    content_only=True, pub=src_pub)
                                m = manifest.Manifest()
                                m.set_content(m_str)

                                # Construct the base signature action.
                                attrs = { "algorithm": sig_alg }
                                a = actions.signature.SignatureAction(cert_path,
                                    **attrs)
                                a.hash = cert_path

                                # Add the action to the manifest to be signed
                                # since the action signs itself.
                                m.add_action(a, misc.EmptyI)

                                # Set the signature value and certificate
                                # information for the signature action.
                                a.set_signature(m.gen_actions(),
                                    key_path=key_path, chain_paths=chain_certs)

                                # Append the finished signature action to the
                                # published manifest.
                                t = trans.Transaction(repo_uri,
                                    pkg_name=str(pfmri), xport=xport,
                                    pub=src_pub, refresh_index=refresh_index)
                                t.append()
                                try:
                                        t.add(a)
                                        for c in chain_certs:
                                                t.add_file(c)
                                        t.close(refresh_index=refresh_index,
                                            add_to_catalog=add_to_catalog)
                                except:
                                        t.close(abandon=True)
                                        raise
                                msg(_("Signed %s") % pfmri)
                                succesful_publish = True
                        except (api_errors.ApiException, fmri.FmriError,
                            trans.TransactionError), e:
                                errors.append(e)
                if errors:
                        error("\n".join([str(e) for e in errors]))
                        if succesful_publish:
                                return EXIT_PARTIAL
                        else:
                                return EXIT_OOPS
                return EXIT_OK
        except (api_errors.TransportError,
            api_errors.UnsupportedRepositoryURI),e:
                error(e)
                return EXIT_OOPS
        finally:
                shutil.rmtree(cache_dir)
                shutil.rmtree(incoming_dir)

#
# 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\nThis is an internal error.  Please let the "
                    "developers know about this\nproblem by filing a bug at "
                    "http://defect.opensolaris.org and including the\nabove "
                    "traceback and this message.  The version of pkg(5) is "
                    "'%s'.") % pkg.VERSION)
                __ret = 99
        sys.exit(__ret)