Skip to content

Commit f263c5f

Browse files
committed
[202211_SPC4] Secure upgrade v2 sonic-net#2698
1 parent 984983e commit f263c5f

16 files changed

Lines changed: 427 additions & 2 deletions

scripts/verify_image_sign.sh

Lines changed: 75 additions & 0 deletions
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
Lines changed: 34 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@
156156
'scripts/memory_threshold_check_handler.py',
157157
'scripts/techsupport_cleanup.py',
158158
'scripts/storm_control.py',
159+
'scripts/verify_image_sign.sh',
160+
'scripts/verify_image_sign_common.sh',
159161
'scripts/check_db_integrity.py',
160162
'scripts/sysreadyshow'
161163
],

sonic_installer/bootloader/bootloader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def supports_package_migration(self, image):
7575
"""tells if the image supports package migration"""
7676
return True
7777

78+
def verify_image_sign(self, image_path):
79+
"""verify image signature is valid"""
80+
return True
81+
7882
@classmethod
7983
def detect(cls):
8084
"""returns True if the bootloader is in use"""

sonic_installer/bootloader/grub.py

Lines changed: 11 additions & 0 deletions
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

Lines changed: 11 additions & 1 deletion
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_aboot_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,8 @@ def test_set_fips_aboot():
7373

7474
# Cleanup
7575
shutil.rmtree(dirpath)
76+
77+
def test_verify_image_sign():
78+
bootloader = aboot.AbootBootloader()
79+
80+
assert bootloader.verify_image_sign(exp_image) == True

tests/installer_bootloader_grub_test.py

Lines changed: 8 additions & 0 deletions
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/installer_bootloader_onie_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ def test_get_current_image(re_search):
1515
# Test image dir conversion
1616
onie.re.search().group = Mock(return_value=image)
1717
assert bootloader.get_current_image() == exp_image
18+
19+
def test_verify_image_sign():
20+
bootloader = onie.OnieInstallerBootloader()
21+
assert bootloader.verify_image_sign('some_path.path') == True

tests/installer_bootloader_uboot_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,9 @@ def mock_run_command(cmd):
9494
# Test fips disabled
9595
bootloader.set_fips(image, False)
9696
assert not bootloader.get_fips(image)
97+
98+
def test_verify_image_sign():
99+
bootloader = uboot.UbootBootloader()
100+
image = 'test-image'
101+
# Test convertion image dir to image name
102+
assert bootloader.verify_image_sign(image) == True

0 commit comments

Comments
 (0)