root/build-tools/sign-secure-boot

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