Skip to content

Commit

Permalink
implemented secure upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
ycoheNvidia committed Aug 15, 2022
1 parent 37eb2b3 commit b972cde
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 0 deletions.
40 changes: 40 additions & 0 deletions scripts/create_mock_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
repo_dir=$1
input_image=$2
output_file=$3
cert_file=$4
key_file=$5
tmp_dir=
clean_up()
{
sudo rm -rf $tmp_dir
sudo rm -rf $output_file
exit $1
}

DIR="$(dirname "$0")"

tmp_dir=$(mktemp -d)
sha1=$(cat $input_image | sha1sum | awk '{print $1}')
echo -n "."
cp $repo_dir/installer/sharch_body.sh $output_file || {
echo "Error: Problems copying sharch_body.sh"
clean_up 1
}
# Replace variables in the sharch template
sed -i -e "s/%%IMAGE_SHA1%%/$sha1/" $output_file
echo -n "."
tar_size="$(wc -c < "${input_image}")"
cat $input_image >> $output_file
sed -i -e "s|%%PAYLOAD_IMAGE_SIZE%%|${tar_size}|" ${output_file}
CMS_SIG="${tmp_dir}/signature.sig"

echo "$0 CMS signing ${input_image} with ${key_file}. Output file ${output_file}"
. $repo_dir/scripts/sign_image_dev.sh
sign_image_dev ${cert_file} ${key_file} $output_file ${CMS_SIG} || clean_up 1

cat ${CMS_SIG} >> ${output_file}
echo "Signature done."
# append signature to binary
sudo rm -rf ${CMS_SIG}
sudo rm -rf $tmp_dir
exit 0
93 changes: 93 additions & 0 deletions scripts/create_sign_and_verify_test_files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
repo_dir=$1
out_dir=$2
mock_image="mock_img.bin"
output_file=$out_dir/output_file.bin
cert_file=$3
other_cert_file=$4
tmp_dir=
clean_up()
{
sudo rm -rf $tmp_dir
sudo rm -rf $mock_image
exit $1
}
DIR="$(dirname "$0")"
[ -d $out_dir ] || rm -rf $out_dir
mkdir $out_dir
tmp_dir=$(mktemp -d)
#generate self signed keys and certificate
key_file=$tmp_dir/private-key.pem
pub_key_file=$tmp_dir/public-key.pem
openssl ecparam -name secp256r1 -genkey -noout -out $key_file
openssl ec -in $key_file -pubout -out $pub_key_file
openssl req -new -x509 -key $key_file -out $cert_file -days 360 -subj "/C=US/ST=Test/L=Test/O=Test/CN=Test"
alt_key_file=$tmp_dir/alt-private-key.pem
alt_pub_key_file=$tmp_dir/alt-public-key.pem
openssl ecparam -name secp256r1 -genkey -noout -out $alt_key_file
openssl ec -in $alt_key_file -pubout -out $alt_pub_key_file
openssl req -new -x509 -key $alt_key_file -out $other_cert_file -days 360 -subj "/C=US/ST=Test/L=Test/O=Test/CN=Test"

echo "this is a mock image\nThis is another line !2#4%6\n" > $mock_image
echo "Created a mock image with following text:"
cat $mock_image
# create signed mock image

sh $DIR/create_mock_image.sh $repo_dir $mock_image $output_file $cert_file $key_file || {
echo "Error: unable to create mock image"
clean_up 1
}

[ -f "$output_file" ] || {
echo "signed mock image not created - exiting without testing"
clean_up 1
}

test_image_1=$out_dir/test_image_1.bin
cp -v $output_file $test_image_1 || {
echo "Error: Problems copying image"
clean_up 1
}

# test_image_1 = modified image size to something else - should fail on signature verification
image_size=$(sed -n 's/^payload_image_size=\(.*\)/\1/p' < $test_image_1)
sed -i "/payload_image_size=/c\payload_image_size=$(($image_size - 5))" $test_image_1

test_image_2=$out_dir/test_image_2.bin
cp -v $output_file $test_image_2 || {
echo "Error: Problems copying image"
clean_up 1
}

# test_image_2 = modified image sha1 to other sha1 value - should fail on signature verification
im_sha=$(sed -n 's/^payload_sha1=\(.*\)/\1/p' < $test_image_2)
sed -i "/payload_sha1=/c\payload_sha1=2f1bbd5a0d411253103e688e4e66c00c94bedd40" $test_image_2

tmp_image=$tmp_dir/"tmp_image.bin"
echo "this is a different image now" >> $mock_image
sh $DIR/create_mock_image.sh $repo_dir $mock_image $tmp_image $cert_file $key_file || { #TODO modify mock image instead of adding new signature
echo "Error: unable to create mock image"
clean_up 1
}
# test_image_3 = original mock image with wrong signature
# Extract cms signature from signed file
test_image_3=$out_dir/"test_image_3.bin"
tmp_sig="${tmp_dir}/tmp_sig.sig"
TMP_TAR_SIZE=$(head -n 50 $tmp_image | grep "payload_image_size=" | cut -d"=" -f2- )
sed -e '1,/^exit_marker$/d' $tmp_image | tail -c +$(( $TMP_TAR_SIZE + 1 )) > $tmp_sig

TAR_SIZE=$(head -n 50 $output_file | grep "payload_image_size=" | cut -d"=" -f2- )
SHARCH_SIZE=$(sed '/^exit_marker$/q' $output_file | wc -c)
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
head -c $SIG_PAYLOAD_SIZE $output_file > $test_image_3
sudo rm -rf $tmp_image

cat ${tmp_sig} >> ${test_image_3}

# test_image_4 = modified image with original mock image signature
test_image_4=$out_dir/"test_image_4.bin"
tmp_sig2="${tmp_dir}/tmp_sig2.sig"
head -c $SIG_PAYLOAD_SIZE $output_file > $test_image_4
echo "this is additional line" >> $test_image_4
sed -e '1,/^exit_marker$/d' $output_file | tail -c +$(( $TAR_SIZE + 1 )) > $tmp_sig2
cat ${tmp_sig} >> ${test_image_4}
clean_up 0
75 changes: 75 additions & 0 deletions scripts/verify_image_sign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/bin/sh
image_file="${1}"
cms_sig_file="sig.cms"
lines_for_lookup=50
SECURE_UPGRADE_ENABLED=0
DIR="$(dirname "$0")"
if [ -d "/sys/firmware/efi/efivars" ]; then
if ! [ -n "$(ls -A /sys/firmware/efi/efivars 2>/dev/null)" ]; then
mount -t efivarfs none /sys/firmware/efi/efivars 2>/dev/null
fi
SECURE_UPGRADE_ENABLED=$(bootctl status 2>/dev/null | grep -c "Secure Boot: enabled")
else
echo "efi not supported - exiting without verification"
exit 0
fi

. /usr/local/bin/verify_image_common.sh

if [ ${SECURE_UPGRADE_ENABLED} -eq 0 ]; then
echo "secure boot not enabled - exiting without image verification"
exit 0
fi

clean_up ()
{
if [ -d ${EFI_CERTS_DIR} ]; then rm -rf ${EFI_CERTS_DIR}; fi
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
exit $?
}

TMP_DIR=$(mktemp -d)
DATA_FILE="${TMP_DIR}/data.bin"
CMS_SIG_FILE="${TMP_DIR}/${cms_sig_file}"
TAR_SIZE=$(head -n $lines_for_lookup $image_file | grep "payload_image_size=" | cut -d"=" -f2- )
SHARCH_SIZE=$(sed '/^exit_marker$/q' $image_file | wc -c)
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
# Extract cms signature from signed file
# Add extra byte for payload
sed -e '1,/^exit_marker$/d' $image_file | tail -c +$(( $TAR_SIZE + 1 )) > $CMS_SIG_FILE
# Extract image from signed file
head -c $SIG_PAYLOAD_SIZE $image_file > $DATA_FILE
# verify signature with certificate fetched with efi tools
EFI_CERTS_DIR=/tmp/efi_certs
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
mkdir $EFI_CERTS_DIR
efi-readvar -v db -o $EFI_CERTS_DIR/db_efi >/dev/null ||
{
echo "Error: unable to read certs from efi db: $?"
clean_up 1
}
# Convert one file to der certificates
sig-list-to-certs $EFI_CERTS_DIR/db_efi $EFI_CERTS_DIR/db >/dev/null||
{
echo "Error: convert sig list to certs: $?"
clean_up 1
}
for file in $(ls $EFI_CERTS_DIR | grep "db-"); do
LOG=$(openssl x509 -in $EFI_CERTS_DIR/$file -inform der -out $EFI_CERTS_DIR/cert.pem 2>&1)
if [ $? -ne 0 ]; then
logger "cms_validation: $LOG"
fi
# Verify detached signature
LOG=$(verify_image_sign_common $image_file $DATA_FILE $CMS_SIG_FILE)
VALIDATION_RES=$?
if [ $VALIDATION_RES -eq 0 ]; then
RESULT="CMS Verified OK this is using efi keys"
echo "verification ok:$RESULT"
# No need to continue.
# Exit without error if any success signature verification.
clean_up 0
fi
done
echo "Error: image not verified $LOG"

clean_up 1
34 changes: 34 additions & 0 deletions scripts/verify_image_sign_common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
verify_image_sign_common() {
image_file="${1}"
cms_sig_file="sig.cms"
TMP_DIR=$(mktemp -d)
DATA_FILE="${2}"
CMS_SIG_FILE="${3}"

openssl version | awk '$2 ~ /(^0\.)|(^1\.(0\.|1\.0))/ { exit 1 }'
if [ $? -eq 0 ]; then
# for version 1.1.1 and later
no_check_time="-no_check_time"
else
# for version older than 1.1.1 use noattr
no_check_time="-noattr"
fi

# making sure image verification is supported
EFI_CERTS_DIR=/tmp/efi_certs
RESULT="CMS Verification Failure"
LOG=$(openssl cms -verify $no_check_time -noout -CAfile $EFI_CERTS_DIR/cert.pem -binary -in ${CMS_SIG_FILE} -content ${DATA_FILE} -inform pem 2>&1 > /dev/null )
VALIDATION_RES=$?
if [ $VALIDATION_RES -eq 0 ]; then
RESULT="CMS Verified OK this is using efi keys"
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
echo "verification ok:$RESULT"
# No need to continue.
# Exit without error if any success signature verification.
return 0
fi

if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
return 1
}
29 changes: 29 additions & 0 deletions scripts/verify_image_sign_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
image_file="${1}"
cert_path="${2}"
cms_sig_file="sig.cms"
TMP_DIR=$(mktemp -d)
DATA_FILE="${TMP_DIR}/data.bin"
CMS_SIG_FILE="${TMP_DIR}/${cms_sig_file}"
lines_for_lookup=50

TAR_SIZE=$(head -n $lines_for_lookup $image_file | grep "payload_image_size=" | cut -d"=" -f2- )
SHARCH_SIZE=$(sed '/^exit_marker$/q' $image_file | wc -c)
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
# Extract cms signature from signed file - exit marker marks last sharch prefix + number of image lines + 1 for next linel
# Add extra byte for payload - extracting image signature from line after data file
sed -e '1,/^exit_marker$/d' $image_file | tail -c +$(( $TAR_SIZE + 1 )) > $CMS_SIG_FILE
# Extract image from signed file
head -c $SIG_PAYLOAD_SIZE $image_file > $DATA_FILE
EFI_CERTS_DIR=/tmp/efi_certs
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
mkdir $EFI_CERTS_DIR
cp $cert_path $EFI_CERTS_DIR/cert.pem

DIR="$(dirname "$0")"
. $DIR/verify_image_sign_common.sh
verify_image_sign_common $image_file $DATA_FILE $CMS_SIG_FILE
VERIFICATION_RES=$?
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
exit $VERIFICATION_RES
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@
'scripts/memory_threshold_check_handler.py',
'scripts/techsupport_cleanup.py',
'scripts/storm_control.py',
'scripts/verify_image_sign.sh',
'scripts/verify_image_sign_common.sh',
'scripts/check_db_integrity.py',
'scripts/sysreadyshow'
],
Expand Down
25 changes: 25 additions & 0 deletions sonic_installer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,14 @@ def install(url, force, skip_platform_check=False, skip_migration=False, skip_pa
"Aborting...", LOG_ERR)
raise click.Abort()

# Verify image signature by default (in sonic there will be a flag here)
echo_and_log("Verifing image {} signature...".format(binary_image_version))
if not _verify_signature(image_path):
echo_and_log('Error: Failed verify image signature', LOG_ERR)
raise click.Abort()
else:
echo_and_log('Verification successful')

echo_and_log("Installing image {} and setting it as default...".format(binary_image_version))
with SWAPAllocator(not skip_setup_swap, swap_mem_size, total_mem_threshold, available_mem_threshold):
bootloader.install_image(image_path)
Expand Down Expand Up @@ -949,5 +957,22 @@ def verify_next_image():
sys.exit(1)
click.echo('Image successfully verified')


def _verify_signature(image_path):
script_path = os.path.join('usr', 'local', 'bin', 'verify_image_sign.sh')
if not os.path.exists(script_path):
echo_and_log("No need to verify mock image")
return True
verification_result = subprocess.Popen([script_path, image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = verification_result.communicate()
if verification_result.returncode != 0:
echo_and_log(stdout, LOG_ERR)
echo_and_log(stderr, LOG_ERR)
else:
echo_and_log(stdout)
echo_and_log(stderr)
return verification_result.returncode == 0


if __name__ == '__main__':
sonic_installer()
70 changes: 70 additions & 0 deletions tests/sign_and_verify_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

import subprocess
import os
import sys
import shutil


class TestSignVerify(object):
def _run_verification_script_and_check(self, image, cert_file_path, success_str, expected_value=0):
res = subprocess.run(['sh', self._verification_script, image, cert_file_path])
assert res.returncode == expected_value
print(success_str)

def test_basic_signature_verification(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'output_file.bin'),
self._cert_file_path, "test case 1 - basic verify signature - SUCCESS")

# change image size to something else - should fail on signature verification
def test_modified_image_size(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'test_image_1.bin'),
self._cert_file_path, "test case 2 - modified image size - SUCCESS", 1)

def test_modified_image_sha1(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'test_image_2.bin'),
self._cert_file_path, "test case 3 - modified image sha1 - SUCCESS", 1)

def test_modified_image_data(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'test_image_3.bin'),
self._cert_file_path, "test case 4 - modified image data - SUCCESS", 1)

def test_modified_image_signature(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'test_image_4.bin'),
self._cert_file_path, "test case 5 - modified image data - SUCCESS", 1)

def test_verify_image_with_wrong_certificate(self):
self._run_verification_script_and_check(os.path.join(self._out_dir_path, 'output_file.bin'),
self._alt_cert_path, "test case 6 - verify with wrong signature - SUCCESS", 1)

def __init__(self):
self._test_path = os.path.dirname(os.path.abspath(__file__))
self._modules_path = os.path.dirname(self._test_path)
self._repo_path = os.path.join(self._modules_path, '../..')
self._scripts_path = os.path.join(self._modules_path, "scripts")
sys.path.insert(0, self._test_path)
sys.path.insert(0, self._modules_path)
sys.path.insert(0, self._scripts_path)
script_path = os.path.join(self._scripts_path, 'create_sign_and_verify_test_files.sh')
self._verification_script = os.path.join(self._scripts_path, 'verify_image_sign_test.sh')
self._out_dir_path = '/tmp/sign_verify_test'
self._cert_file_path = os.path.join(self._out_dir_path, 'self_certificate.pem')
self._alt_cert_path = os.path.join(self._out_dir_path, 'alt_self_certificate.pem')
create_files_result = subprocess.run(['sh', script_path, self._repo_path, self._out_dir_path,
self._cert_file_path,
self._alt_cert_path])
print(create_files_result)
assert create_files_result.returncode == 0

def __del__(self):
shutil.rmtree(self._out_dir_path)


if __name__ == '__main__':
t = TestSignVerify()
t.test_basic_signature_verification()
subprocess.run(['ls', '/tmp/sign_verify_test'])
t.test_modified_image_data()
t.test_modified_image_sha1()
t.test_modified_image_signature()
t.test_modified_image_size()
t.test_verify_image_with_wrong_certificate()

0 comments on commit b972cde

Please sign in to comment.