Skip to content

Commit 5e50a4a

Browse files
ycoheNvidiaStormLiangMS
authored andcommitted
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.
1 parent 0f0ec14 commit 5e50a4a

16 files changed

Lines changed: 456 additions & 2 deletions

scripts/verify_image_sign.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/sh
2+
image_file="${1}"
3+
cms_sig_file="sig.cms"
4+
lines_for_lookup=50
5+
DIR="$(dirname "$0")"
6+
7+
. /usr/local/bin/verify_image_sign_common.sh
8+
9+
clean_up ()
10+
{
11+
if [ -d ${EFI_CERTS_DIR} ]; then rm -rf ${EFI_CERTS_DIR}; fi
12+
if [ -d "${TMP_DIR}" ]; then rm -rf ${TMP_DIR}; fi
13+
exit $1
14+
}
15+
16+
TMP_DIR=$(mktemp -d)
17+
DATA_FILE="${TMP_DIR}/data.bin"
18+
CMS_SIG_FILE="${TMP_DIR}/${cms_sig_file}"
19+
TAR_SIZE=$(head -n $lines_for_lookup $image_file | grep "payload_image_size=" | cut -d"=" -f2- )
20+
SHARCH_SIZE=$(sed '/^exit_marker$/q' $image_file | wc -c)
21+
SIG_PAYLOAD_SIZE=$(($TAR_SIZE + $SHARCH_SIZE ))
22+
# Extract cms signature from signed file
23+
# Add extra byte for payload
24+
sed -e '1,/^exit_marker$/d' $image_file | tail -c +$(( $TAR_SIZE + 1 )) > $CMS_SIG_FILE
25+
# Extract image from signed file
26+
head -c $SIG_PAYLOAD_SIZE $image_file > $DATA_FILE
27+
# verify signature with certificate fetched with efi tools
28+
EFI_CERTS_DIR=/tmp/efi_certs
29+
[ -d $EFI_CERTS_DIR ] && rm -rf $EFI_CERTS_DIR
30+
mkdir $EFI_CERTS_DIR
31+
efi-readvar -v db -o $EFI_CERTS_DIR/db_efi >/dev/null ||
32+
{
33+
echo "Error: unable to read certs from efi db: $?"
34+
clean_up 1
35+
}
36+
# Convert one file to der certificates
37+
sig-list-to-certs $EFI_CERTS_DIR/db_efi $EFI_CERTS_DIR/db >/dev/null||
38+
{
39+
echo "Error: convert sig list to certs: $?"
40+
clean_up 1
41+
}
42+
for file in $(ls $EFI_CERTS_DIR | grep "db-"); do
43+
LOG=$(openssl x509 -in $EFI_CERTS_DIR/$file -inform der -out $EFI_CERTS_DIR/cert.pem 2>&1)
44+
if [ $? -ne 0 ]; then
45+
logger "cms_validation: $LOG"
46+
fi
47+
# Verify detached signature
48+
LOG=$(verify_image_sign_common $image_file $DATA_FILE $CMS_SIG_FILE)
49+
VALIDATION_RES=$?
50+
if [ $VALIDATION_RES -eq 0 ]; then
51+
RESULT="CMS Verified OK using efi keys"
52+
echo "verification ok:$RESULT"
53+
# No need to continue.
54+
# Exit without error if any success signature verification.
55+
clean_up 0
56+
fi
57+
done
58+
echo "Failure: CMS signature Verification Failed: $LOG"
59+
60+
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ 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+
raise NotImplementedError
81+
82+
def is_secure_upgrade_image_verification_supported(self):
83+
return False
84+
7885
@classmethod
7986
def detect(cls):
8087
"""returns True if the bootloader is in use"""

sonic_installer/bootloader/grub.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,41 @@ 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 is_secure_upgrade_image_verification_supported(self):
157+
158+
check_if_verification_is_enabled_and_supported_code = '''
159+
SECURE_UPGRADE_ENABLED=0
160+
if [ -d "/sys/firmware/efi/efivars" ]; then
161+
if ! [ -n "$(ls -A /sys/firmware/efi/efivars 2>/dev/null)" ]; then
162+
mount -t efivarfs none /sys/firmware/efi/efivars 2>/dev/null
163+
fi
164+
SECURE_UPGRADE_ENABLED=$(bootctl status 2>/dev/null | grep -c "Secure Boot: enabled")
165+
else
166+
echo "efi not supported - exiting without verification"
167+
exit 1
168+
fi
169+
170+
if [ ${SECURE_UPGRADE_ENABLED} -eq 0 ]; then
171+
echo "secure boot not enabled - exiting without image verification"
172+
exit 1
173+
fi
174+
exit 0
175+
'''
176+
verification_result = subprocess.run(['bash', '-c', check_if_verification_is_enabled_and_supported_code], capture_output=True)
177+
click.echo(verification_result.stdout.decode())
178+
return verification_result.returncode == 0
179+
180+
def verify_image_sign(self, image_path):
181+
click.echo('Verifying image signature')
182+
verification_script_name = 'verify_image_sign.sh'
183+
script_path = os.path.join('/usr', 'local', 'bin', verification_script_name)
184+
if not os.path.exists(script_path):
185+
click.echo("Unable to find verification script in path " + script_path)
186+
return False
187+
verification_result = subprocess.run([script_path, image_path], capture_output=True)
188+
click.echo(verification_result.stdout.decode())
189+
return verification_result.returncode == 0
190+
156191
@classmethod
157192
def detect(cls):
158193
return os.path.isfile(os.path.join(HOST_PATH, 'grub/grub.cfg'))

sonic_installer/main.py

Lines changed: 10 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+
if bootloader.is_secure_upgrade_image_verification_supported():
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)

tests/installer_bootloader_aboot_test.py

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

7474
# Cleanup
7575
shutil.rmtree(dirpath)
76+
77+
def test_verify_image_sign():
78+
bootloader = aboot.AbootBootloader()
79+
return_value = None
80+
is_supported = bootloader.is_secure_upgrade_image_verification_supported()
81+
try:
82+
return_value = bootloader.verify_image_sign(exp_image)
83+
except NotImplementedError:
84+
assert not is_supported
85+
else:
86+
assert False, "Wrong return value from verify_image_sign, returned" + str(return_value)

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+
assert not bootloader.is_secure_upgrade_image_verification_supported()
62+
# command should fail
63+
assert not bootloader.verify_image_sign(image)

tests/installer_bootloader_onie_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,14 @@ 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+
return_value = None
22+
is_supported = bootloader.is_secure_upgrade_image_verification_supported()
23+
try:
24+
return_value = bootloader.verify_image_sign('some_path.path')
25+
except NotImplementedError:
26+
assert not is_supported
27+
else:
28+
assert False, "Wrong return value from verify_image_sign, returned" + str(return_value)

tests/installer_bootloader_uboot_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,15 @@ 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+
return_value = None
102+
is_supported = bootloader.is_secure_upgrade_image_verification_supported()
103+
try:
104+
return_value = bootloader.verify_image_sign(image)
105+
except NotImplementedError:
106+
assert not is_supported
107+
else:
108+
assert False, "Wrong return value from verify_image_sign, returned" + str(return_value)

0 commit comments

Comments
 (0)