#!/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 # # 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 # # 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 # # 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 [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_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