From 5e50a4af1617f74f25053352f2ef0c7a87965bf0 Mon Sep 17 00:00:00 2001 From: ycoheNvidia <99744138+ycoheNvidia@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:03:39 +0300 Subject: [PATCH] Add support for secure upgrade (#2698) - What I did Added support for secure upgrade - How I did it It includes image signing during build (in sonic buildimage repo) and verification during image install (in sonic-utilities). HLD can be found in the following PR: sonic-net/SONiC#1024 - How to verify it Feature is used to allow image was not modified since built from vendor. During installation, image can be verified with a signature attached to it. In order for image verification - image must be signed - need to provide signing key and certificate (paths in SECURE_UPGRADE_DEV_SIGNING_KEY and SECURE_UPGRADE_DEV_SIGNING_CERT in rules/config) during build , and during image install, need to enable secure boot flag in bios, and signing_certificate should be available in bios. - Feature dependencies In order for this feature to work smoothly, need to have secure boot feature implemented as well. The Secure boot feature will be merged in the near future. --- scripts/verify_image_sign.sh | 60 ++++++++++++ scripts/verify_image_sign_common.sh | 34 +++++++ setup.py | 2 + sonic_installer/bootloader/bootloader.py | 7 ++ sonic_installer/bootloader/grub.py | 35 +++++++ sonic_installer/main.py | 11 ++- tests/installer_bootloader_aboot_test.py | 11 +++ tests/installer_bootloader_grub_test.py | 8 ++ tests/installer_bootloader_onie_test.py | 11 +++ tests/installer_bootloader_uboot_test.py | 12 +++ tests/scripts/create_mock_image.sh | 40 ++++++++ .../create_sign_and_verify_test_files.sh | 91 +++++++++++++++++++ tests/scripts/verify_image_sign_test.sh | 29 ++++++ tests/sign_and_verify_test.py | 70 ++++++++++++++ tests/test_sonic_installer.py | 8 +- tests/verify_image_sign_test.sh | 29 ++++++ 16 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 scripts/verify_image_sign.sh create mode 100755 scripts/verify_image_sign_common.sh create mode 100755 tests/scripts/create_mock_image.sh create mode 100755 tests/scripts/create_sign_and_verify_test_files.sh create mode 100755 tests/scripts/verify_image_sign_test.sh create mode 100644 tests/sign_and_verify_test.py create mode 100755 tests/verify_image_sign_test.sh diff --git a/scripts/verify_image_sign.sh b/scripts/verify_image_sign.sh new file mode 100644 index 0000000000..c23d4b711d --- /dev/null +++ b/scripts/verify_image_sign.sh @@ -0,0 +1,60 @@ +#!/bin/sh +image_file="${1}" +cms_sig_file="sig.cms" +lines_for_lookup=50 +DIR="$(dirname "$0")" + +. /usr/local/bin/verify_image_sign_common.sh + +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 $1 +} + +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 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 "Failure: CMS signature Verification Failed: $LOG" + +clean_up 1 \ No newline at end of file diff --git a/scripts/verify_image_sign_common.sh b/scripts/verify_image_sign_common.sh new file mode 100755 index 0000000000..ec6511bc6d --- /dev/null +++ b/scripts/verify_image_sign_common.sh @@ -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" + 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 +} diff --git a/setup.py b/setup.py index 9bcfedaf11..277665c9ec 100644 --- a/setup.py +++ b/setup.py @@ -156,6 +156,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' ], diff --git a/sonic_installer/bootloader/bootloader.py b/sonic_installer/bootloader/bootloader.py index 91dcaf4665..3b9e548352 100644 --- a/sonic_installer/bootloader/bootloader.py +++ b/sonic_installer/bootloader/bootloader.py @@ -75,6 +75,13 @@ def supports_package_migration(self, image): """tells if the image supports package migration""" return True + def verify_image_sign(self, image_path): + """verify image signature is valid""" + raise NotImplementedError + + def is_secure_upgrade_image_verification_supported(self): + return False + @classmethod def detect(cls): """returns True if the bootloader is in use""" diff --git a/sonic_installer/bootloader/grub.py b/sonic_installer/bootloader/grub.py index 7ab5c6c0bc..27ce5fcbdf 100644 --- a/sonic_installer/bootloader/grub.py +++ b/sonic_installer/bootloader/grub.py @@ -153,6 +153,41 @@ def verify_image_platform(self, image_path): # Check if platform is inside image's target platforms return self.platform_in_platforms_asic(platform, image_path) + def is_secure_upgrade_image_verification_supported(self): + + check_if_verification_is_enabled_and_supported_code = ''' + SECURE_UPGRADE_ENABLED=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 1 + fi + + if [ ${SECURE_UPGRADE_ENABLED} -eq 0 ]; then + echo "secure boot not enabled - exiting without image verification" + exit 1 + fi + exit 0 + ''' + verification_result = subprocess.run(['bash', '-c', check_if_verification_is_enabled_and_supported_code], capture_output=True) + click.echo(verification_result.stdout.decode()) + return verification_result.returncode == 0 + + def verify_image_sign(self, image_path): + click.echo('Verifying image signature') + verification_script_name = 'verify_image_sign.sh' + script_path = os.path.join('/usr', 'local', 'bin', verification_script_name) + if not os.path.exists(script_path): + click.echo("Unable to find verification script in path " + script_path) + return False + verification_result = subprocess.run([script_path, image_path], capture_output=True) + click.echo(verification_result.stdout.decode()) + return verification_result.returncode == 0 + @classmethod def detect(cls): return os.path.isfile(os.path.join(HOST_PATH, 'grub/grub.cfg')) diff --git a/sonic_installer/main.py b/sonic_installer/main.py index ce1c15866d..f672b4cd19 100644 --- a/sonic_installer/main.py +++ b/sonic_installer/main.py @@ -511,7 +511,8 @@ def sonic_installer(): @click.option('-y', '--yes', is_flag=True, callback=abort_if_false, expose_value=False, prompt='New image will be installed, continue?') @click.option('-f', '--force', '--skip-secure-check', is_flag=True, - help="Force installation of an image of a non-secure type than secure running image") + help="Force installation of an image of a non-secure type than secure running " + + " image, this flag does not affect secure upgrade image verification") @click.option('--skip-platform-check', is_flag=True, help="Force installation of an image of a type which is not of the same platform") @click.option('--skip_migration', is_flag=True, @@ -576,6 +577,14 @@ def install(url, force, skip_platform_check=False, skip_migration=False, skip_pa "Aborting...", LOG_ERR) raise click.Abort() + if bootloader.is_secure_upgrade_image_verification_supported(): + echo_and_log("Verifing image {} signature...".format(binary_image_version)) + if not bootloader.verify_image_sign(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) diff --git a/tests/installer_bootloader_aboot_test.py b/tests/installer_bootloader_aboot_test.py index 56eee4872e..5ebdfc2423 100644 --- a/tests/installer_bootloader_aboot_test.py +++ b/tests/installer_bootloader_aboot_test.py @@ -73,3 +73,14 @@ def test_set_fips_aboot(): # Cleanup shutil.rmtree(dirpath) + +def test_verify_image_sign(): + bootloader = aboot.AbootBootloader() + return_value = None + is_supported = bootloader.is_secure_upgrade_image_verification_supported() + try: + return_value = bootloader.verify_image_sign(exp_image) + except NotImplementedError: + assert not is_supported + else: + assert False, "Wrong return value from verify_image_sign, returned" + str(return_value) diff --git a/tests/installer_bootloader_grub_test.py b/tests/installer_bootloader_grub_test.py index ff35e13b37..a72e0f7914 100644 --- a/tests/installer_bootloader_grub_test.py +++ b/tests/installer_bootloader_grub_test.py @@ -53,3 +53,11 @@ def test_set_fips_grub(): # Cleanup the _tmp_host folder shutil.rmtree(tmp_host_path) + +def test_verify_image(): + + bootloader = grub.GrubBootloader() + image = f'{grub.IMAGE_PREFIX}expeliarmus-{grub.IMAGE_PREFIX}abcde' + assert not bootloader.is_secure_upgrade_image_verification_supported() + # command should fail + assert not bootloader.verify_image_sign(image) diff --git a/tests/installer_bootloader_onie_test.py b/tests/installer_bootloader_onie_test.py index f5c2d96f7d..5e63cc867d 100644 --- a/tests/installer_bootloader_onie_test.py +++ b/tests/installer_bootloader_onie_test.py @@ -15,3 +15,14 @@ def test_get_current_image(re_search): # Test image dir conversion onie.re.search().group = Mock(return_value=image) assert bootloader.get_current_image() == exp_image + +def test_verify_image_sign(): + bootloader = onie.OnieInstallerBootloader() + return_value = None + is_supported = bootloader.is_secure_upgrade_image_verification_supported() + try: + return_value = bootloader.verify_image_sign('some_path.path') + except NotImplementedError: + assert not is_supported + else: + assert False, "Wrong return value from verify_image_sign, returned" + str(return_value) \ No newline at end of file diff --git a/tests/installer_bootloader_uboot_test.py b/tests/installer_bootloader_uboot_test.py index 49973558d0..60267336e9 100644 --- a/tests/installer_bootloader_uboot_test.py +++ b/tests/installer_bootloader_uboot_test.py @@ -94,3 +94,15 @@ def mock_run_command(cmd): # Test fips disabled bootloader.set_fips(image, False) assert not bootloader.get_fips(image) + +def test_verify_image_sign(): + bootloader = uboot.UbootBootloader() + image = 'test-image' + return_value = None + is_supported = bootloader.is_secure_upgrade_image_verification_supported() + try: + return_value = bootloader.verify_image_sign(image) + except NotImplementedError: + assert not is_supported + else: + assert False, "Wrong return value from verify_image_sign, returned" + str(return_value) diff --git a/tests/scripts/create_mock_image.sh b/tests/scripts/create_mock_image.sh new file mode 100755 index 0000000000..f23032af0d --- /dev/null +++ b/tests/scripts/create_mock_image.sh @@ -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 diff --git a/tests/scripts/create_sign_and_verify_test_files.sh b/tests/scripts/create_sign_and_verify_test_files.sh new file mode 100755 index 0000000000..0040c04a7a --- /dev/null +++ b/tests/scripts/create_sign_and_verify_test_files.sh @@ -0,0 +1,91 @@ +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 || { + 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" +head -c $SIG_PAYLOAD_SIZE $output_file > $test_image_4 +echo "this is additional line" >> $test_image_4 +cat ${tmp_sig} >> ${test_image_4} +clean_up 0 \ No newline at end of file diff --git a/tests/scripts/verify_image_sign_test.sh b/tests/scripts/verify_image_sign_test.sh new file mode 100755 index 0000000000..f4abd2584f --- /dev/null +++ b/tests/scripts/verify_image_sign_test.sh @@ -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 \ No newline at end of file diff --git a/tests/sign_and_verify_test.py b/tests/sign_and_verify_test.py new file mode 100644 index 0000000000..77d58a4ac9 --- /dev/null +++ b/tests/sign_and_verify_test.py @@ -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._test_scripts_path = os.path.join(self._test_path, "scripts") + sys.path.insert(0, self._test_path) + sys.path.insert(0, self._modules_path) + sys.path.insert(0, self._test_scripts_path) + script_path = os.path.join(self._test_scripts_path, 'create_sign_and_verify_test_files.sh') + self._verification_script = os.path.join(self._test_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() diff --git a/tests/test_sonic_installer.py b/tests/test_sonic_installer.py index c445dfb6e3..816f44bab6 100644 --- a/tests/test_sonic_installer.py +++ b/tests/test_sonic_installer.py @@ -31,7 +31,7 @@ def test_install(run_command, run_command_or_raise, get_bootloader, swap, fs): mock_bootloader.get_binary_image_version = Mock(return_value=new_image_version) mock_bootloader.get_installed_images = Mock(return_value=[current_image_version]) mock_bootloader.get_image_path = Mock(return_value=new_image_folder) - + mock_bootloader.verify_image_sign = Mock(return_value=True) @contextmanager def rootfs_path_mock(path): yield mounted_image_folder @@ -45,7 +45,13 @@ def rootfs_path_mock(path): print(result.output) assert result.exit_code == 0 + mock_bootloader_verify_image_sign_fail = mock_bootloader + mock_bootloader_verify_image_sign_fail.verify_image_sign = Mock(return_value=False) + get_bootloader.return_value=mock_bootloader_verify_image_sign_fail + result = runner.invoke(sonic_installer.commands["install"], [sonic_image_filename, "-y"]) + print(result.output) + assert result.exit_code != 0 # Assert bootloader install API was called mock_bootloader.install_image.assert_called_with(f"./{sonic_image_filename}") # Assert all below commands were called, so we ensure that diff --git a/tests/verify_image_sign_test.sh b/tests/verify_image_sign_test.sh new file mode 100755 index 0000000000..f4abd2584f --- /dev/null +++ b/tests/verify_image_sign_test.sh @@ -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 \ No newline at end of file