diff --git a/scripts/create_mock_image.sh b/scripts/create_mock_image.sh new file mode 100755 index 0000000000..f23032af0d --- /dev/null +++ b/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/scripts/create_sign_and_verify_test_files.sh b/scripts/create_sign_and_verify_test_files.sh new file mode 100755 index 0000000000..33f8b12fcd --- /dev/null +++ b/scripts/create_sign_and_verify_test_files.sh @@ -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 \ No newline at end of file diff --git a/scripts/verify_image_sign.sh b/scripts/verify_image_sign.sh new file mode 100644 index 0000000000..4599abe148 --- /dev/null +++ b/scripts/verify_image_sign.sh @@ -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 \ 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..cf58875b62 --- /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 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 +} \ No newline at end of file diff --git a/scripts/verify_image_sign_test.sh b/scripts/verify_image_sign_test.sh new file mode 100755 index 0000000000..f4abd2584f --- /dev/null +++ b/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/setup.py b/setup.py index 7f617905da..8f9414a7f0 100644 --- a/setup.py +++ b/setup.py @@ -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' ], diff --git a/sonic_installer/main.py b/sonic_installer/main.py index db3fe49827..b59ae9055d 100644 --- a/sonic_installer/main.py +++ b/sonic_installer/main.py @@ -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) @@ -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() diff --git a/tests/sign_and_verify_test.py b/tests/sign_and_verify_test.py new file mode 100644 index 0000000000..2b95c9d4eb --- /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._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()