#!/bin/bash

# This script calls into an external signing server to perform signing of some
# packages in the system.  The old packages (which are typically generated by
# the build system and signed by placeholder keys) are overwritten by the new
# packages.
#
# Three types of packages are signed:
# kernels (both std and lowlatency, aka "rt", kernels)
# grub
# shim
#
# Kernels and grub are generated by producing (under the normal build system)
# two packages -- a package containing the unsigned binaries, and a package
# containing binaries signed with temporary keys.  All the "accessories" (files,
# scripts, etc) are included in the package containing the signed-with-temp-keys
# files.  The signing server will take both packages, sign the unsigned
# binaries, and replace the files in the signed package with the newly signed
# ones.
#
# Typical flow/artifacts
# kernel.src.rpm      -> produces kernel.rpm and kernel-unsigned.rpm
# kernel.rpm          -> initially contains binaries signed with a temporary key
#                     -> contains all files used by the kernel
#                     -> can be installed and used in a system (it just won't
#                        secure boot since the key is just a temp key)
# kernel-unsigned.rpm -> contains just unsigned kernel binaries
#
# The signing server will take both packages, sign the binaries in
# kernel-unsigned.rpm with our private key, and replace the binaries in
# kernel.rpm with the new binaries.  The kernel.rpm can then be replaced by the
# version generated by the signing server.
#
# Shim is a bit of a different beast.
#
# There are two source packages - shim and shim-signed.  Frustratingly, "shim"
# source produces a "shim-unsigned" binary output.  "shim-signed" produces a
# "shim" binary output. 
#
# The "shim-signed" source RPM doesn't contain source code -- it just contains
# instructions to take the "shim-unsigned" binaries, sign them, and package the
# output.  We've modified the shim-signed RPM to (rather than sign with a temp
# key) use "presigned" binaries from shim-unsigned if the files exist.  (It will
# still use a temp key of no presigned files are found, which is how the build
# system normally runs).
#
# The signing server will unpack the shim-unsigned package, sign the binaries
# (as "presigned") and repack the package.
#
# A rebuild of shim-signed by the build server is then required.  
#
# Thanks for bearing with me in the convoluted discussion, above.


# Script flow:
# - call signing server to sign kernels (if they exist and are new, as with
#   other RPMs)
# - replace old kernel packages with newly signed ones
# - call signing server to sign grub (and replace old version with the newly
#   signed one)
# - call signing server to sign shim-unsigned (replace old version)
# - rebuild shim-signed 
# - update our repos to advertize all newly replaced packages

# check_if_pkg_needs_signing <path/to/filename.rpm>
#
# Checks to see if a given package needs to be signed.  We maintain a list of
# MD5 sums for RPMs we have signed.  Thus, we can easily see if we've already
# signed a package.
#
# Returns 1 if the package does need signing, or 0 if package does not
#
# This function expects the package specified to exist.
function check_if_pkg_needs_signing
{
    local PKG_TO_CHECK=$1

    if [ ! -e ${SIGNED_PKG_DB} ]; then
        # We haven't signed anything before, so this package needs signing
        return 1
    fi

    local SIGNED_PKG_MD5=`grep ${PKG_TO_CHECK} ${SIGNED_PKG_DB} | cut -d ' ' -f 1`
    if [ "x${SIGNED_PKG_MD5}" == "x" ]; then
        # We have no record of having signed the package -- needs signing
        return 1
    fi

    local CURRENT_MD5=`md5sum ${PKG_TO_CHECK} | cut -d ' ' -f 1`
    if [ "${CURRENT_MD5}" != "${SIGNED_PKG_MD5}" ]; then
        # The package has been regenerated since we last signed it -- needs
        # signing again
        return 1
    fi

    # The package md5 sum matches the md5sum of the package when it was last
    # signed.
    return 0
}

# update_signed_pkg_list <path/to/filename.rpm>
#
# Updated our list of signed packages with the md5 sum of a recently signed
# package.
#
# This function expects the package to exist.
function update_signed_pkg_list
{
    local PKG_TO_ADD=$1

    if [ ! -e ${SIGNED_PKG_DB} ]; then
        touch ${SIGNED_PKG_DB}
    fi

    # remove current entry for package
    local TMPFILE=`mktemp`
    grep -v $(basename ${PKG_TO_ADD}) ${SIGNED_PKG_DB} > ${TMPFILE}
    mv ${TMPFILE} ${SIGNED_PKG_DB}

    # add MD5 for package to the package list
    md5sum ${PKG_TO_ADD} >> ${SIGNED_PKG_DB}
}


# update_repo <std|rt>
#
# Updates either the standard or rt repo with latest packages
# Checks that you specified a repo, and that the path exists.
#
# There are actually now two places we need to update -- the
# rpmbuild/RPMS/ path, as well as the results/.../ path
function update_repo
{
	local BUILD_TYPE=$1
	local EXTRA_PARAMS=""
	local RETCODE=0
	local repopath=""

	if [ "x$BUILD_TYPE" == "x" ]; then
		return 1
	fi

	if [ "x$MY_BUILD_ENVIRONMENT_TOP" == "x" ]; then
		return 1
	fi

	for repopath in "$MY_WORKSPACE/$BUILD_TYPE/rpmbuild/RPMS" "$MY_WORKSPACE/$BUILD_TYPE/results/${MY_BUILD_ENVIRONMENT_TOP}-$BUILD_TYPE"; do
		if [ ! -d "$repopath" ]; then
			echo "Error - cannot find path $repopath"
			return 1
		fi

		cd $repopath
		if [ -f comps.xml ]; then
			EXTRA_PARAMS="-g comps.xml"
		fi
		createrepo --update $EXTRA_PARAMS . > /dev/null
		RETCODE=$?
		cd - > /dev/null
		if [ 0$RETCODE -ne 0 ]; then
			return $RETCODE
		fi
	done

	return $RETCODE
}

# sign_shims - find and sign any shim package that we need
#              Note that shim might produce a "shim-unsigned-[verison-release]
#              package (old shim) or shim-unsigned-x64-[v-r] &
#              shim-unsigned-ia32 package (new shim).  In the case of new shim,
#              we must do x64 only, and not ia32.
#
function sign_shims
{
	SHIM=`find $MY_WORKSPACE/std/rpmbuild/RPMS -name "shim-unsigned-x64-*.$ARCH.rpm" | grep -v debuginfo`
	if [ -z "$SHIM" ]; then
		SHIM=`find $MY_WORKSPACE/std/rpmbuild/RPMS -name "shim-unsigned-*.$ARCH.rpm" | grep -v debuginfo`
	fi
	if [ -z "${SHIM}" ]; then
	    echo "Warning -- cannot find shim package to sign"
	    return 0
	fi
	sign shim $SHIM

	return $?
}

# sign_grubs - find and sign any grub package that we need to.
#              Grub (and kernel) are initially signed with temporary keys, so
#              we need to upload both the complete package, as well as the
#              unsigned binaries
#
function sign_grubs
{
	GRUB=`find $MY_WORKSPACE/std/rpmbuild/RPMS -name "grub2-efi-x64-[1-9]*.$ARCH.rpm"`
	UNSIGNED_GRUB=`find $MY_WORKSPACE/std/rpmbuild/RPMS -name "grub2-efi-x64-unsigned*.$ARCH.rpm"`
	if [ "x${GRUB}" == "x" ]; then
	    echo "Warning -- cannot find GRUB package to sign"
	    return 0
	fi
	if [ "x${UNSIGNED_GRUB}" == "x" ]; then
	    echo "Warning -- cannot find unsigned GRUB package to sign"
	    return 0
	fi

	sign grub2 $GRUB $UNSIGNED_GRUB
	return $?
}

# sign_kernels - find and sign any kernel package that we need to.
#
function sign_kernels
{
    sign_kernel "std" ""
    sign_kernel "rt" "-rt"
}

# sign_kernel - find and sign kernel package if we need to.
#              Kernels (and grub) are initially signed with temporary keys, so
#              we need to upload both the complete package, as well as the
#              unsigned binaries
function sign_kernel
{
	local KERNEL_PATH=$1
	local KERNEL_EXTRA=$2
	KERNEL=`find $MY_WORKSPACE/${KERNEL_PATH}/rpmbuild/RPMS -name "kernel${KERNEL_EXTRA}-[1-9]*.$ARCH.rpm"`
	UNSIGNED_KERNEL=`find $MY_WORKSPACE/${KERNEL_PATH}/rpmbuild/RPMS -name "kernel${KERNEL_EXTRA}-unsigned-[1-9]*.$ARCH.rpm"`
	if [ "x${KERNEL}" == "x" ]; then
	    echo "Warning -- cannot find kernel package to sign in ${KERNEL_PATH}"
	    return 0
	fi
	if [ "x${UNSIGNED_KERNEL}" == "x" ]; then
	    echo "Warning -- cannot find unsigned kernel package to sign in ${KERNEL_PATH}"
	    return 0
	fi

	sign kernel $KERNEL $UNSIGNED_KERNEL
	if [ $? -ne 0 ]; then
		return $?
	fi
}

# rebuild_pkgs - rebuild any packages that need to be updated from the newly
# signed binaries
#
function rebuild_pkgs
{
	local LOGFILE="$MY_WORKSPACE/export/signed-rebuild.log"
	local PKGS_TO_REBUILD=${REBUILD_LIST}

	if [ "x${PKGS_TO_REBUILD}" == "x" ]; then
	    # No rebuilds required, return cleanly
	    return 0
	fi

	# If we reach this point, then we have one or more packages to be rebuilt

	# first, update the repo so it is aware of the "latest" binaries
	update_repo std
	if [ $? -ne 0 ]; then
		echo "Could not update signed packages -- could not update repo"
		return 1
	fi

        echo "Performing rebuild of packages: $PKGS_TO_REBUILD"
        FORMAL_BUILD=0 build-pkgs --no-descendants --no-build-info --no-required --careful $PKGS_TO_REBUILD > $LOGFILE 2>&1

	if [ $? -ne 0 ]; then
		echo "Could not rebuild packages: $PKGS_TO_REBUILD -- see $LOGFILE for details"
		return 1
	fi

	echo "Done"
	return 0
}

# sign <type_of_pkg> <pkg> [pkg_containing_unsigned_bins]
#
# This routine uploads a package to the signing server, instructs the signing
# signing server to do its' magic, and downloads the updated (signed) package
# from the signing server.
#
# Accessing the signing server -- the signing server cannot just be logged
# into by anyone.  A small number of users (Jason McKenna, Scott Little, Greg
# Waines, etc) have permission to log in as themselves.  In addition, there is
# a user "signing" who is unique to the server.  The "jenkins" user on our
# build servers has permission to login/upload files as "signing" due to Jenkins'
# private SSH key being added to the signing user's list of keys.  This means
# that Jenkins can upload and run commands on the server as "signing".
#
# In addition to uploading files as signing, the signing user has permissions to
# run a single command (/opt/signing/sign.sh) as a sudo root user.  The signing
# user does not have access to modify the script or to run any other commands as
# root.  The sign.sh script will take inputs (the files that jenkins has
# uploaded), verify the contents, sign the images against private keys, and
# output a new .rpm contianing the signed version of the files.  Assuming all
# is successful, the filename of the signed output file is returned, and the
# jenkins user can then use that filename to download the file (the "signing"
# user does not have access to remove or modify the file once it's created).
#
# All operations done on the signing server are logged in muliple places, and
# the output RPM artifacts are timestamped to ensure that they are not
# overwritten by subsequent calls to sign.sh.
#
# kernel and grub package types require you to specify/upload the unsigned
# packages as well as the normal binary
function sign
{
	local TYPE=$1
	local FILE=$2
	local UNSIGNED=$3
	local UNSIGNED_OPTION=""
	local TMPFILE=`mktemp /tmp/sign.XXXXXXXX`

	# Don't sign if we've already signed it
	check_if_pkg_needs_signing ${FILE}
	if [ $? -eq 0 ]; then
		echo "Not signing ${FILE} as we previously signed it"
		return 0
	fi

	echo "Signing $FILE"

	# upload the original package
	scp -q $FILE $SIGNING_USER@$SIGNING_SERVER:$UPLOAD_PATH
	if [ $? -ne 0 ]; then
		echo "Failed to upload file $FILE"
		\rm -f $TMPFILE
		return 1
	fi
	
	# upload the unsigned package (if specified)
	if [ "x$UNSIGNED" != "x" ]; then
		echo "Uploading unsigned: $UNSIGNED"
		scp -q $UNSIGNED $SIGNING_USER@$SIGNING_SERVER:$UPLOAD_PATH
		if [ $? -ne 0 ]; then
			echo "Failed to upload file $UNSIGNED"
			\rm -f $TMPFILE
			return 1
		fi
		UNSIGNED=$(basename $UNSIGNED)
		UNSIGNED_OPTION="-u $UPLOAD_PATH/$UNSIGNED"
	fi

	# Call the magic script on the signing server.  Note that the user
	# ($SIGNING_USER) has sudo permissions but only to invoke this one script.
	# The signing user cannot make other sudo calls.
	#
	# We place output in $TMPFILE to extract the output file name later
	#
	ssh $SIGNING_USER@$SIGNING_SERVER sudo $SIGNING_SCRIPT -i $UPLOAD_PATH/$(basename $FILE) $UNSIGNED_OPTION -t $TYPE > $TMPFILE 2>&1
	if [ $? -ne 0 ]; then
		echo "Signing of $FILE failed"
		\rm -f $TMPFILE
		return 1
	fi
	
	# The signing server script will output the name by which the newly signed
	# RPM can be found.  This will be a unique filename (based on the unique
	# upload directory generated by the "-r" option above).
	#
	# The reason for this is so that we can archive all output files
	# and examine them later without them being overwriten.  File paths are
	# typically of the form
	#
	# /export/signed_images/XXXXXXX_grub2-efi-64-2.02-0.44.el7.centos.tis.3.x86_64.rpm
	#
	# Extract the output name, and copy the RPM back into our system
	# (Note that we overwrite our original version of the RPM)
	#
	# Note that some packages (like grub) may produce multiple output RPMs (i.e.
	# multiple lines list output files.
	OUTPUT=`grep "Output written:" $TMPFILE | sed "s/Output written: //"`
	
	# Check that we got something
	if [ "x$OUTPUT" == "x" ]; then
		echo "Could not determine output file -- check logs on signing server for errors"
		\cp $TMPFILE $MY_WORKSPACE/export/signing.log
		\rm -f $TMPFILE
		return 1
	fi

	# The signing script can return multiple output files, if appropriate for
	# the input RPM source type.  Copy each output RPM to our repo
	# Note that after we download the file we extract the base package name
	# from the RPM to find the name of the file that it *should* be named
	#
	# example:
	#   we'd download "Zrqyeuzw_kernel-3.10.0-514.2.2.el7.20.tis.x86_64.rpm"
	#   we'd figure out that the RPM name should be "kernel"
	#   we look for "kernel" in the RPM filename, and rename
	#     "Zrqyeuzw_kernel-3.10.0-514.2.2.el7.20.tis.x86_64.rpm" to
	#     "kernel-3.10.0-514.2.2.el7.20.tis.x86_64.rpm"
	while read OUTPUT_FILE; do

		# Download the file from the signing server
		local DOWNLOAD_FILENAME=$(basename $OUTPUT_FILE)
		scp -q $SIGNING_USER@$SIGNING_SERVER:$OUTPUT_FILE $(dirname $FILE)
		if [ $? -ne 0 ]; then
			\rm -f $TMPFILE
			echo "Copying file from signing server failed"
			return 1
		fi
		echo "Successfully retrieved $OUTPUT_FILE"

		# figure out what the file should be named (strip away leading chars)
		local RPM_NAME=`rpm -qp $(dirname $FILE)/$DOWNLOAD_FILENAME --qf="%{name}"`
		local CORRECT_OUTPUT_FILE_NAME=`echo $DOWNLOAD_FILENAME | sed "s/^.*$RPM_NAME/$RPM_NAME/"`

		# rename the file
		\mv -f $(dirname $FILE)/$DOWNLOAD_FILENAME $(dirname $FILE)/$CORRECT_OUTPUT_FILE_NAME

		# replace the version of the file in results
		#
		# Potential hiccup in future -- this code currenty replaces any output file in EITHER
		# std or rt results which matches the filename we just downloaded from the signing.
		# server.  This means there could be an issue where we sign something-ver-rel.arch.rpm
		# but we expect different versions of that RPM in std and in rt.  Currently, we do not
		# have any RPMs which have that problem (all produced RPMs in rt have the "-rt" suffix
		# let along any "signed" rpms) but it's something of which to be aware.
		#
		# Also, note that we do not expect multiple RPMs in each repo to have the same filename.
		# We use "head -n 1" to handle that, but again it shouldn't happen.
		# 
		for buildtype in std rt; do
			x=`find $MY_WORKSPACE/$buildtype/results/${MY_BUILD_ENVIRONMENT_TOP}-$buildtype -name $CORRECT_OUTPUT_FILE_NAME | head -n 1`
			if [ ! -z "$x" ]; then
				cp $(dirname $FILE)/$CORRECT_OUTPUT_FILE_NAME $x
			fi
		done

		echo "Have signed file $(dirname $FILE)/$CORRECT_OUTPUT_FILE_NAME"
	done <<< "$OUTPUT"

	\rm -f $TMPFILE

	# If we just signed a shim package, flag that shim needs to be rebuilt
	if [ "${TYPE}" == "shim" ]; then
		REBUILD_LIST="${REBUILD_LIST} shim-signed"
	fi

	echo "Done"
	update_signed_pkg_list ${FILE}

	return 0
}

# Main script

if [ "x$MY_WORKSPACE" == "x" ]; then
	echo "Environment not set up -- abort"
	exit 1
fi

ARCH="x86_64"
SIGNING_SERVER=yow-tiks01
SIGNING_USER=signing
SIGNING_SCRIPT=/opt/signing/sign.sh
UPLOAD_PATH=`ssh $SIGNING_USER@$SIGNING_SERVER sudo $SIGNING_SCRIPT -r`
SIGNED_PKG_DB=${MY_WORKSPACE}/signed_pkg_list.txt
REBUILD_LIST=""
MY_BUILD_ENVIRONMENT_TOP=${MY_BUILD_ENVIRONMENT_TOP:-$MY_BUILD_ENVIRONMENT}

# Check that we were able to request a unique path for uploads
echo $UPLOAD_PATH | grep -q "^Upload:"
if [ $? -ne 0 ]; then
	echo "Failed to get upload path -- abort"
	exit 1
fi
UPLOAD_PATH=`echo $UPLOAD_PATH | sed "s%^Upload: %%"`

sign_kernels
if [ $? -ne 0 ]; then
	echo "Failed to sign kernels -- abort"
	exit 1
fi

sign_shims
if [ $? -ne 0 ]; then
	echo "Failed to sign shims -- abort"
	exit 1
fi

sign_grubs
if [ $? -ne 0 ]; then
	echo "Failed to sign grubs -- abort"
	exit 1
fi

update_repo std
if [ $? -ne 0 ]; then
	echo "Failed to update std repo -- abort"
	exit 1
fi

rebuild_pkgs
if [ $? -ne 0 ]; then
	echo "Failed to update builds with signed dependancies -- abort"
	exit 1
fi

update_repo std
if [ $? -ne 0 ]; then
	echo "Failed to update std repo -- abort"
	exit 1
fi

update_repo rt
if [ $? -ne 0 ]; then
	echo "Failed to update rt repo -- abort"
	exit 1
fi