Skip to content

Commit 56d41f2

Browse files
ycoheNvidiaisabelmsft
authored andcommitted
Secure upgrade (sonic-net#2337)
#### 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. sonic-buildimage PR: sonic-net/sonic-buildimage#11862
1 parent 0744b19 commit 56d41f2

12 files changed

+408
-2
lines changed

scripts/verify_image_sign.sh

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/bin/sh
2+
image_file="${1}"
3+
cms_sig_file="sig.cms"
4+
lines_for_lookup=50
5+
SECURE_UPGRADE_ENABLED=0
6+
DIR="$(dirname "$0")"
7+
if [ -d "/sys/firmware/efi/efivars" ]; then
8+
if ! [ -n "$(ls -A /sys/firmware/efi/efivars 2>/dev/null)" ]; then
9+
mount -t efivarfs none /sys/firmware/efi/efivars 2>/dev/null
10+
fi
11+
SECURE_UPGRADE_ENABLED=$(bootctl status 2>/dev/null | grep -c "Secure Boot: enabled")
12+
else
13+
echo "efi not supported - exiting without verification"
14+
exit 0
15+
fi
16+
17+
. /usr/local/bin/verify_image_sign_common.sh
18+
19+
if [ ${SECURE_UPGRADE_ENABLED} -eq 0 ]; then
20+
echo "secure boot not enabled - exiting without image verification"
21+
exit 0
22+
fi
23+
24+
clean_up ()
25+
{
26+
if [ -d ${EFI_CERTS_DIR} ]; then rm -rf ${EFI_CERTS_DIR}; fi
27+
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
28+
exit $1
29+
}
30+
31+
TMP_DIR=$(mktemp -d)
32+
DATA_FILE="${TMP_DIR}/data.bin"
33+
CMS_SIG_FILE="${TMP_DIR}/${cms_sig_file}"
34+
TAR_SIZE=$(head -n $lines_for_lookup $image_file | grep "payload_image_size=" | cut -d"=" -f2- )
35+
SHARCH_SIZE=$(sed '/^exit_marker$/q' $image_file | wc -c)
36+
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
37+
# Extract cms signature from signed file
38+
# Add extra byte for payload
39+
sed -e '1,/^exit_marker$/d' $image_file | tail -c +$(( $TAR_SIZE + 1 )) > $CMS_SIG_FILE
40+
# Extract image from signed file
41+
head -c $SIG_PAYLOAD_SIZE $image_file > $DATA_FILE
42+
# verify signature with certificate fetched with efi tools
43+
EFI_CERTS_DIR=/tmp/efi_certs
44+
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
45+
mkdir $EFI_CERTS_DIR
46+
efi-readvar -v db -o $EFI_CERTS_DIR/db_efi >/dev/null ||
47+
{
48+
echo "Error: unable to read certs from efi db: $?"
49+
clean_up 1
50+
}
51+
# Convert one file to der certificates
52+
sig-list-to-certs $EFI_CERTS_DIR/db_efi $EFI_CERTS_DIR/db >/dev/null||
53+
{
54+
echo "Error: convert sig list to certs: $?"
55+
clean_up 1
56+
}
57+
for file in $(ls $EFI_CERTS_DIR | grep "db-"); do
58+
LOG=$(openssl x509 -in $EFI_CERTS_DIR/$file -inform der -out $EFI_CERTS_DIR/cert.pem 2>&1)
59+
if [ $? -ne 0 ]; then
60+
logger "cms_validation: $LOG"
61+
fi
62+
# Verify detached signature
63+
LOG=$(verify_image_sign_common $image_file $DATA_FILE $CMS_SIG_FILE)
64+
VALIDATION_RES=$?
65+
if [ $VALIDATION_RES -eq 0 ]; then
66+
RESULT="CMS Verified OK using efi keys"
67+
echo "verification ok:$RESULT"
68+
# No need to continue.
69+
# Exit without error if any success signature verification.
70+
clean_up 0
71+
fi
72+
done
73+
echo "Failure: CMS signature Verification Failed: $LOG"
74+
75+
clean_up 1

scripts/verify_image_sign_common.sh

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
verify_image_sign_common() {
3+
image_file="${1}"
4+
cms_sig_file="sig.cms"
5+
TMP_DIR=$(mktemp -d)
6+
DATA_FILE="${2}"
7+
CMS_SIG_FILE="${3}"
8+
9+
openssl version | awk '$2 ~ /(^0\.)|(^1\.(0\.|1\.0))/ { exit 1 }'
10+
if [ $? -eq 0 ]; then
11+
# for version 1.1.1 and later
12+
no_check_time="-no_check_time"
13+
else
14+
# for version older than 1.1.1 use noattr
15+
no_check_time="-noattr"
16+
fi
17+
18+
# making sure image verification is supported
19+
EFI_CERTS_DIR=/tmp/efi_certs
20+
RESULT="CMS Verification Failure"
21+
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 )
22+
VALIDATION_RES=$?
23+
if [ $VALIDATION_RES -eq 0 ]; then
24+
RESULT="CMS Verified OK"
25+
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
26+
echo "verification ok:$RESULT"
27+
# No need to continue.
28+
# Exit without error if any success signature verification.
29+
return 0
30+
fi
31+
32+
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
33+
return 1
34+
}

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@
154154
'scripts/memory_threshold_check_handler.py',
155155
'scripts/techsupport_cleanup.py',
156156
'scripts/storm_control.py',
157+
'scripts/verify_image_sign.sh',
158+
'scripts/verify_image_sign_common.sh',
157159
'scripts/check_db_integrity.py',
158160
'scripts/sysreadyshow'
159161
],

sonic_installer/bootloader/grub.py

+11
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ def verify_image_platform(self, image_path):
153153
# Check if platform is inside image's target platforms
154154
return self.platform_in_platforms_asic(platform, image_path)
155155

156+
def verify_image_sign(self, image_path):
157+
click.echo('Verifying image signature')
158+
verification_script_name = 'verify_image_sign.sh'
159+
script_path = os.path.join('/usr', 'local', 'bin', verification_script_name)
160+
if not os.path.exists(script_path):
161+
click.echo("Unable to find verification script in path " + script_path)
162+
return False
163+
verification_result = subprocess.run([script_path, image_path], capture_output=True)
164+
click.echo(str(verification_result.stdout) + " " + str(verification_result.stderr))
165+
return verification_result.returncode == 0
166+
156167
@classmethod
157168
def detect(cls):
158169
return os.path.isfile(os.path.join(HOST_PATH, 'grub/grub.cfg'))

sonic_installer/main.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,8 @@ def sonic_installer():
511511
@click.option('-y', '--yes', is_flag=True, callback=abort_if_false,
512512
expose_value=False, prompt='New image will be installed, continue?')
513513
@click.option('-f', '--force', '--skip-secure-check', is_flag=True,
514-
help="Force installation of an image of a non-secure type than secure running image")
514+
help="Force installation of an image of a non-secure type than secure running " +
515+
" image, this flag does not affect secure upgrade image verification")
515516
@click.option('--skip-platform-check', is_flag=True,
516517
help="Force installation of an image of a type which is not of the same platform")
517518
@click.option('--skip_migration', is_flag=True,
@@ -576,6 +577,14 @@ def install(url, force, skip_platform_check=False, skip_migration=False, skip_pa
576577
"Aborting...", LOG_ERR)
577578
raise click.Abort()
578579

580+
# Calling verification script by default - signature will be checked if enabled in bios
581+
echo_and_log("Verifing image {} signature...".format(binary_image_version))
582+
if not bootloader.verify_image_sign(image_path):
583+
echo_and_log('Error: Failed verify image signature', LOG_ERR)
584+
raise click.Abort()
585+
else:
586+
echo_and_log('Verification successful')
587+
579588
echo_and_log("Installing image {} and setting it as default...".format(binary_image_version))
580589
with SWAPAllocator(not skip_setup_swap, swap_mem_size, total_mem_threshold, available_mem_threshold):
581590
bootloader.install_image(image_path)
@@ -958,5 +967,6 @@ def verify_next_image():
958967
sys.exit(1)
959968
click.echo('Image successfully verified')
960969

970+
961971
if __name__ == '__main__':
962972
sonic_installer()

tests/installer_bootloader_grub_test.py

+8
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ def test_set_fips_grub():
5353

5454
# Cleanup the _tmp_host folder
5555
shutil.rmtree(tmp_host_path)
56+
57+
def test_verify_image():
58+
59+
bootloader = grub.GrubBootloader()
60+
image = f'{grub.IMAGE_PREFIX}expeliarmus-{grub.IMAGE_PREFIX}abcde'
61+
62+
# command should fail
63+
assert not bootloader.verify_image_sign(image)

tests/scripts/create_mock_image.sh

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
repo_dir=$1
2+
input_image=$2
3+
output_file=$3
4+
cert_file=$4
5+
key_file=$5
6+
tmp_dir=
7+
clean_up()
8+
{
9+
sudo rm -rf $tmp_dir
10+
sudo rm -rf $output_file
11+
exit $1
12+
}
13+
14+
DIR="$(dirname "$0")"
15+
16+
tmp_dir=$(mktemp -d)
17+
sha1=$(cat $input_image | sha1sum | awk '{print $1}')
18+
echo -n "."
19+
cp $repo_dir/installer/sharch_body.sh $output_file || {
20+
echo "Error: Problems copying sharch_body.sh"
21+
clean_up 1
22+
}
23+
# Replace variables in the sharch template
24+
sed -i -e "s/%%IMAGE_SHA1%%/$sha1/" $output_file
25+
echo -n "."
26+
tar_size="$(wc -c < "${input_image}")"
27+
cat $input_image >> $output_file
28+
sed -i -e "s|%%PAYLOAD_IMAGE_SIZE%%|${tar_size}|" ${output_file}
29+
CMS_SIG="${tmp_dir}/signature.sig"
30+
31+
echo "$0 CMS signing ${input_image} with ${key_file}. Output file ${output_file}"
32+
. $repo_dir/scripts/sign_image_dev.sh
33+
sign_image_dev ${cert_file} ${key_file} $output_file ${CMS_SIG} || clean_up 1
34+
35+
cat ${CMS_SIG} >> ${output_file}
36+
echo "Signature done."
37+
# append signature to binary
38+
sudo rm -rf ${CMS_SIG}
39+
sudo rm -rf $tmp_dir
40+
exit 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
repo_dir=$1
2+
out_dir=$2
3+
mock_image="mock_img.bin"
4+
output_file=$out_dir/output_file.bin
5+
cert_file=$3
6+
other_cert_file=$4
7+
tmp_dir=
8+
clean_up()
9+
{
10+
sudo rm -rf $tmp_dir
11+
sudo rm -rf $mock_image
12+
exit $1
13+
}
14+
DIR="$(dirname "$0")"
15+
[ -d $out_dir ] || rm -rf $out_dir
16+
mkdir $out_dir
17+
tmp_dir=$(mktemp -d)
18+
#generate self signed keys and certificate
19+
key_file=$tmp_dir/private-key.pem
20+
pub_key_file=$tmp_dir/public-key.pem
21+
openssl ecparam -name secp256r1 -genkey -noout -out $key_file
22+
openssl ec -in $key_file -pubout -out $pub_key_file
23+
openssl req -new -x509 -key $key_file -out $cert_file -days 360 -subj "/C=US/ST=Test/L=Test/O=Test/CN=Test"
24+
alt_key_file=$tmp_dir/alt-private-key.pem
25+
alt_pub_key_file=$tmp_dir/alt-public-key.pem
26+
openssl ecparam -name secp256r1 -genkey -noout -out $alt_key_file
27+
openssl ec -in $alt_key_file -pubout -out $alt_pub_key_file
28+
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"
29+
30+
echo "this is a mock image\nThis is another line !2#4%6\n" > $mock_image
31+
echo "Created a mock image with following text:"
32+
cat $mock_image
33+
# create signed mock image
34+
35+
sh $DIR/create_mock_image.sh $repo_dir $mock_image $output_file $cert_file $key_file || {
36+
echo "Error: unable to create mock image"
37+
clean_up 1
38+
}
39+
40+
[ -f "$output_file" ] || {
41+
echo "signed mock image not created - exiting without testing"
42+
clean_up 1
43+
}
44+
45+
test_image_1=$out_dir/test_image_1.bin
46+
cp -v $output_file $test_image_1 || {
47+
echo "Error: Problems copying image"
48+
clean_up 1
49+
}
50+
51+
# test_image_1 = modified image size to something else - should fail on signature verification
52+
image_size=$(sed -n 's/^payload_image_size=\(.*\)/\1/p' < $test_image_1)
53+
sed -i "/payload_image_size=/c\payload_image_size=$(($image_size - 5))" $test_image_1
54+
55+
test_image_2=$out_dir/test_image_2.bin
56+
cp -v $output_file $test_image_2 || {
57+
echo "Error: Problems copying image"
58+
clean_up 1
59+
}
60+
61+
# test_image_2 = modified image sha1 to other sha1 value - should fail on signature verification
62+
im_sha=$(sed -n 's/^payload_sha1=\(.*\)/\1/p' < $test_image_2)
63+
sed -i "/payload_sha1=/c\payload_sha1=2f1bbd5a0d411253103e688e4e66c00c94bedd40" $test_image_2
64+
65+
tmp_image=$tmp_dir/"tmp_image.bin"
66+
echo "this is a different image now" >> $mock_image
67+
sh $DIR/create_mock_image.sh $repo_dir $mock_image $tmp_image $cert_file $key_file || {
68+
echo "Error: unable to create mock image"
69+
clean_up 1
70+
}
71+
# test_image_3 = original mock image with wrong signature
72+
# Extract cms signature from signed file
73+
test_image_3=$out_dir/"test_image_3.bin"
74+
tmp_sig="${tmp_dir}/tmp_sig.sig"
75+
TMP_TAR_SIZE=$(head -n 50 $tmp_image | grep "payload_image_size=" | cut -d"=" -f2- )
76+
sed -e '1,/^exit_marker$/d' $tmp_image | tail -c +$(( $TMP_TAR_SIZE + 1 )) > $tmp_sig
77+
78+
TAR_SIZE=$(head -n 50 $output_file | grep "payload_image_size=" | cut -d"=" -f2- )
79+
SHARCH_SIZE=$(sed '/^exit_marker$/q' $output_file | wc -c)
80+
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
81+
head -c $SIG_PAYLOAD_SIZE $output_file > $test_image_3
82+
sudo rm -rf $tmp_image
83+
84+
cat ${tmp_sig} >> ${test_image_3}
85+
86+
# test_image_4 = modified image with original mock image signature
87+
test_image_4=$out_dir/"test_image_4.bin"
88+
head -c $SIG_PAYLOAD_SIZE $output_file > $test_image_4
89+
echo "this is additional line" >> $test_image_4
90+
cat ${tmp_sig} >> ${test_image_4}
91+
clean_up 0
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
image_file="${1}"
3+
cert_path="${2}"
4+
cms_sig_file="sig.cms"
5+
TMP_DIR=$(mktemp -d)
6+
DATA_FILE="${TMP_DIR}/data.bin"
7+
CMS_SIG_FILE="${TMP_DIR}/${cms_sig_file}"
8+
lines_for_lookup=50
9+
10+
TAR_SIZE=$(head -n $lines_for_lookup $image_file | grep "payload_image_size=" | cut -d"=" -f2- )
11+
SHARCH_SIZE=$(sed '/^exit_marker$/q' $image_file | wc -c)
12+
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
13+
# Extract cms signature from signed file - exit marker marks last sharch prefix + number of image lines + 1 for next linel
14+
# Add extra byte for payload - extracting image signature from line after data file
15+
sed -e '1,/^exit_marker$/d' $image_file | tail -c +$(( $TAR_SIZE + 1 )) > $CMS_SIG_FILE
16+
# Extract image from signed file
17+
head -c $SIG_PAYLOAD_SIZE $image_file > $DATA_FILE
18+
EFI_CERTS_DIR=/tmp/efi_certs
19+
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
20+
mkdir $EFI_CERTS_DIR
21+
cp $cert_path $EFI_CERTS_DIR/cert.pem
22+
23+
DIR="$(dirname "$0")"
24+
. $DIR/verify_image_sign_common.sh
25+
verify_image_sign_common $image_file $DATA_FILE $CMS_SIG_FILE
26+
VERIFICATION_RES=$?
27+
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
28+
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
29+
exit $VERIFICATION_RES

0 commit comments

Comments
 (0)