root/build-tools/sign-secure-boot
Scott Little b20ac0164d Build Avoidance
Purpose:
   Reduce build times after a repo sync by pulling in pre-generated
srpms and rpms and other build products created by a local reference build.

Usage:
  repo sync
  generate-cgcs-centos-repo.sh ...
  populate_downloads.sh ...
  build-pkgs --build-avoidance [--build-avoidance-user <user> \
     --build-avoidance-host <addr> --build-avoidance-dir <dir>]

Reference builds:
- A server performs a regular (daily?), automated builds using
  existing methods. Call these the reference builds.

- The builds are timestamped, and preserved for some time. (weeks?)
  The MY_WORKSPACE directory for the build shall have a common root
  directory, and a leaf directory that is a UTC time stamp of format
  YYYYMMDDThhmmssZ.
  e.g.
  MY_WORKSPACE=/localdisk/loadbuild/jenkins/StarlingX/20180719T113021Z

  Alternative formats are possible by setting values in ...
  "$MY_REPO/local-build-data/build_avoidance_source"
  e.g.
  BUILD_AVOIDANCE_DATE_FORMAT="%Y-%m-%d"
  BUILD_AVOIDANCE_TIME_FORMAT="%H-%M-%S"
  BUILD_AVOIDANCE_DATE_TIME_DELIM="_"
  BUILD_AVOIDANCE_DATE_TIME_POSTFIX=""
  BUILD_AVOIDANCE_DATE_UTC=0

  Which results in YYYY-MM-DD_hh-mm-ss format using local time.
  The one property that the timestamp must have is that they
  are sortable, and that the reference build and the consumer of
  the reference builds agree on the format.

- A build CONTEXT is captured, consisting of the SHA of each and every
  git that contributed to the build.

- For each package built, a file shall capture he md5sums of all the
  source code inputs to the build of that package.

- All these build products are accessible locally (e.g. a regional
  office) via rsync (other protocols can be added later).  ssh
  is also required to run remote query commands on the reference build.

  Initial ground work to support a selection variable ....
  BUILD_AVOIDANCE_FILE_TRANSFER="my-transfer-protocol"
  in $MY_REPO/local-build-data/build_avoidance_source"
  has been created, but "rsync" is the only valid value at this time.

- Location of the reference build can be specified via command line, or
  defaults can be put in $MY_REPO/local-build-data/build_avoidance_source.
  The local-build-data directory is gitignored by stx-root and so can be
  customized for local needs.
  e.g.
  cat $MY_REPO/local-build-data/build_avoidance_source
  BUILD_AVOIDANCE_USR="jenkins"
  BUILD_AVOIDANCE_HOST="stx-build-server.myco.com"
  BUILD_AVOIDANCE_DIR="/localdisk/loadbuild/jenkins/StarlingX"

Notes:
- Build avoidance is only used if requested.
- Build avoidance does not necessarily use the latest reference build.
  It compares the git context of all available reference builds vs your
  own git context, and chooses the most recent for which you gits have
  all the conent.  i.e. all your gits will be same or newer than that
  used by the reference build.  This also meens that some packages might
  still need to be rebuilt after the download step.
- Normally build avoidance remembers the last download context and will only
  consider reference builds newer than the last download.   You can reset
  using 'build-pkgs --build-avoidance --clear' to erase the download history.
  When might this matter to me?  If you change to an old branch that
  hasn't been synced recently and want to build in that context.
- The primary assumtion of Build Avoidance is that it is faster to
  download packages than to build them.  This is typically true of a
  good LAN, but likely not true of a WAN. This is why we emphasize the
  local nature of your reference build server.

Also in this update:
- reworked context generation to be relative to 'dirname $MY_REPO'
- Moved md5sum calculation to a common file, and fixed case where
  symlinks where canonacalized to paths outside of $MY_REPO.
  We'll make an exception to canonacalization to keep paths
  relative to $MY_REPO.
- In future other functions could be moved to the common file.

Story: 2002835
Task: 22754
Change-Id: I757780190cc6063d0a2d3ad9d0a6020ab5169e99
Signed-off-by: Scott Little <scott.little@windriver.com>
2018-09-17 16:41:31 -04:00

519 lines
17 KiB
Bash
Executable File

#!/bin/bash
#
# Copyright (c) 2018 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# 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 --append-log $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_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