Skip to content

Commit 4aa7595

Browse files
colpanehuaqianli
authored andcommitted
feat(iot2050-firmware-update): Add optional firmware signature verification
This commit introduces an optional but highly recommended digital signature verification mechanism for firmware updates. Users can now enforce cryptographic checks on firmware images within an update tarball to ensure their integrity and authenticity before flashing. Key changes include: - New `--verify` argument: Enables signature verification during a regular update process. If specified, the tool will look for a `.sig` file corresponding to the firmware within the tarball. - Integrated verification logic: The `FirmwareTarball` class now contains a method to perform PKCS1v15 SHA512 signature verification using the `cryptography` library. This check occurs prior to any flashing operations. - Strict enforcement: When `--verify` is used, a missing signature file or a failed verification will result in an `UpgradeError`, aborting the update with a `BAD_SIGNATURE` or `INVALID_FIRMWARE` error code. This enhancement significantly improves the security posture of the firmware update process by preventing the installation of tampered or unauthorized firmware. Signed-off-by: Enes Colpan <enes.colpan@siemens.com>
1 parent b0fdbc3 commit 4aa7595

File tree

3 files changed

+117
-23
lines changed

3 files changed

+117
-23
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../meta/recipes-bsp/secure-boot-otp-provisioning/files/keys/custMpk.crt

meta-example/recipes-app/iot2050-firmware-update/files/iot2050-firmware-update.tmpl

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The <firmware-update-package>.tar.xz should contain:
1919
- firmware.bin: The firmware to update, could be more than one.
2020
- update.conf.json: The update criteria.
2121
- u-boot-initial-env: Builtin environment
22+
- firmware.bin.sig: (Optional)The signature file for the firmware.bin
2223
2324
Example of update.conf.json:
2425
{
@@ -123,6 +124,11 @@ from progress.bar import Bar
123124
from threading import Thread, Event
124125
from types import SimpleNamespace as Namespace
125126

127+
# Import cryptography modules for signature verification
128+
from cryptography.hazmat.primitives import hashes, serialization
129+
from cryptography.hazmat.primitives.asymmetric import padding
130+
from cryptography.exceptions import InvalidSignature
131+
126132
class ErrorCode(Enum):
127133
"""The ErrorCode class describes the return codes"""
128134
SUCCESS = 0
@@ -134,7 +140,9 @@ class ErrorCode(Enum):
134140
CANCELED = 6
135141
INVALID_FIRMWARE = 7
136142
FAILED = 8
137-
143+
MISSING_SIGNATURE = 9
144+
MISSING_PUBLIC_KEY = 10
145+
BAD_SIGNATURE = 11
138146

139147
class UpgradeError(Exception):
140148
def __init__(self, ErrorInfo, code=ErrorCode.FAILED.value):
@@ -438,11 +446,12 @@ class FirmwareUpdate():
438446
firmware update.
439447
"""
440448
def __init__(self, tarball, backup_path, interactor,
441-
rollback=False, reset=False):
449+
rollback=False, reset=False, verify_signature=False):
442450
self.back_fw_path = os.path.join("".join(backup_path), ".rollback_fw")
443451
self.rollback_fw_tar = os.path.join(self.back_fw_path,
444452
'rollback_backup_fw.tar')
445453
self.interactor = interactor
454+
self.verify_signature = verify_signature
446455
try:
447456
if rollback:
448457
if not os.path.exists(self.rollback_fw_tar) or \
@@ -451,7 +460,7 @@ class FirmwareUpdate():
451460
ErrorCode.ROLLBACK_FAILED.value)
452461

453462
tarball = open(self.rollback_fw_tar, "rb")
454-
self.tarball = FirmwareTarball(tarball, interactor, None)
463+
self.tarball = FirmwareTarball(tarball, interactor, None, False)
455464
else:
456465
self.tarball = tarball
457466

@@ -548,6 +557,19 @@ class FirmwareUpdate():
548557
print("===================================================")
549558

550559
try:
560+
# Perform signature verification before starting the actual flashing
561+
if self.verify_signature:
562+
print("Verifying firmware signatures...")
563+
# The primary firmware (uboot) is the one to be signed
564+
firmware_name_to_verify = self.tarball.get_file_name(self.tarball.FIRMWARE_TYPES[0])
565+
if firmware_name_to_verify:
566+
self.tarball.verify_firmware_signature(firmware_name_to_verify)
567+
print(f"Signature for {firmware_name_to_verify} verified successfully.")
568+
else:
569+
raise UpgradeError("Could not determine primary firmware name for signature verification.",
570+
ErrorCode.INVALID_FIRMWARE.value)
571+
print("Firmware signature verification complete. Proceeding with update.")
572+
551573
for firmware_type in self.firmwares:
552574
if firmware_type == self.tarball.FIRMWARE_TYPES[2]:
553575
continue
@@ -567,7 +589,7 @@ class FirmwareUpdate():
567589
raise UpgradeError("Firmware digest verification failed")
568590
except UpgradeError as e:
569591
self.interactor.progress_bar(start=False)
570-
raise UpgradeError(e.err, ErrorCode.FLASHING_FAILED.value)
592+
raise UpgradeError(e.err, e.code)
571593

572594
def __get_md5_digest(self, content):
573595
"""Verify the update integrity"""
@@ -582,6 +604,9 @@ class FirmwareTarball(object):
582604

583605
CONF_JSON = 'update.conf.json'
584606
UBOOT_ENV_FILE = 'u-boot-initial-env'
607+
FIRMWARE_SIG_EXT = '.sig'
608+
PUBLIC_KEY_PATH = '/usr/share/iot2050/fwu/public.key'
609+
585610
# "env" must be after "uboot" because uboot update will overwrite env
586611
# partition
587612
FIRMWARE_TYPES = [
@@ -590,10 +615,11 @@ class FirmwareTarball(object):
590615
"conf"
591616
]
592617

593-
def __init__(self, firmware_tarball, interactor, env_list):
618+
def __init__(self, firmware_tarball, interactor, env_list, verify_signature_flag=False):
594619
self.interactor = interactor
595620
self.firmware_tarball = firmware_tarball
596621
self.env_list = env_list
622+
self.verify_signature_flag = verify_signature_flag
597623

598624
# extract file path
599625
self.extract_path = "/tmp"
@@ -784,6 +810,53 @@ class FirmwareTarball(object):
784810

785811
return uboot_env_assemble_file, open(uboot_env_assemble_file, 'rb')
786812

813+
def verify_firmware_signature(self, firmware_name):
814+
"""
815+
Verifies the digital signature of a firmware file.
816+
Expects a signature file with the same name as the firmware
817+
but with a .sig extension in the tarball.
818+
The public key is expected at PUBLIC_KEY_PATH.
819+
"""
820+
firmware_path = self.get_file_path(firmware_name)
821+
signature_path = firmware_path + self.FIRMWARE_SIG_EXT
822+
823+
if not os.path.exists(firmware_path):
824+
raise UpgradeError(f"Firmware file not found: {firmware_path}",
825+
ErrorCode.INVALID_FIRMWARE.value)
826+
if not os.path.exists(signature_path):
827+
# If --verify is used, signature file is mandatory
828+
if self.verify_signature_flag:
829+
raise UpgradeError(f"Signature file not found {signature_path}. Signature verification is enabled, but the .sig file is missing.",
830+
ErrorCode.MISSING_SIGNATURE.value)
831+
else:
832+
# This case should ideally not be reached if verify_signature_flag is false
833+
# as this method would only be called if verify_signature_flag is true.
834+
# However, as a safeguard:
835+
print(f"Warning: Signature file not found for {firmware_name}. Skipping signature verification.")
836+
return
837+
838+
if not os.path.exists(self.PUBLIC_KEY_PATH):
839+
raise UpgradeError(f"Public key not found at {self.PUBLIC_KEY_PATH}. Cannot verify signature.",
840+
ErrorCode.MISSING_PUBLIC_KEY.value)
841+
842+
try:
843+
with open(self.PUBLIC_KEY_PATH, "rb") as f:
844+
pubkey = serialization.load_pem_public_key(f.read())
845+
846+
with open(firmware_path, "rb") as f:
847+
message = f.read()
848+
849+
with open(signature_path, "rb") as f:
850+
sig = f.read()
851+
852+
pubkey.verify(sig, message, padding.PKCS1v15(), hashes.SHA512())
853+
print(f"Signature for {firmware_name} is GOOD.")
854+
except InvalidSignature:
855+
raise UpgradeError(f"BAD SIGNATURE for {firmware_name}. Firmware is malicious or corrupted. Aborting update.",
856+
ErrorCode.BAD_SIGNATURE.value)
857+
except Exception as e:
858+
raise UpgradeError(f"Error during signature verification for {firmware_name}: {e}",
859+
ErrorCode.FAILED.value)
787860

788861
class BoardInfo(object):
789862
"""The BoardInfo represents the updating IOT2050 board information"""
@@ -899,6 +972,8 @@ def main(argv):
899972
using tarball format firmware, reset environment
900973
5. %(prog)s -b
901974
rollback the firmware to the version before the upgrade
975+
6. %(prog)s --verify file.tar.xz
976+
perform a regular update, but enforce signature verification for the firmware
902977
''')
903978
epilog=textwrap.dedent('''\
904979
Return Value:
@@ -913,6 +988,9 @@ def main(argv):
913988
| 6 | User canceled |
914989
| 7 | Invalid firmware |
915990
| 8 | Failed to update |
991+
| 9 | Missing firmware signature |
992+
| 10 | Missing public key |
993+
| 11 | Bad firmware signature |
916994
''')
917995
parser = argparse.ArgumentParser(
918996
description=description,
@@ -939,6 +1017,9 @@ def main(argv):
9391017
help='Rollback the firmware to the version before the \
9401018
upgrade',
9411019
action='store_true')
1020+
parser.add_argument('--verify',
1021+
help='Enforce signature verification for the firmware in the tarball',
1022+
action='store_true')
9421023
group2 = parser.add_mutually_exclusive_group()
9431024
group2.add_argument('-n', '--no-backup',
9441025
help='Do not generate a firmware backup',
@@ -954,25 +1035,28 @@ def main(argv):
9541035

9551036
try:
9561037
args = parser.parse_args()
957-
if args.force and (args.no_backup or args.backup_dir):
958-
parser.error("argument -f/--force: not allowed with -n/--no-backup, -d/--backup-dir")
1038+
if args.force and (args.no_backup or args.backup_dir or args.verify):
1039+
parser.error("argument -f/--force: not allowed with -n/--no-backup, -d/--backup-dir, or --verify")
1040+
# Ensure firmware is specified for any update or verify operation
1041+
if not args.rollback and not args.firmware:
1042+
print("No firmware or action specified. A firmware tarball is required for update or verification.", file=sys.stderr)
1043+
return ErrorCode.INVALID_ARG.value
1044+
9591045
except IOError as e:
9601046
print(e.strerror, file=sys.stderr)
9611047
return ErrorCode.INVALID_ARG.value
9621048

963-
if not args.rollback and not args.firmware:
964-
print("No firmware specified")
965-
return ErrorCode.INVALID_ARG.value
966-
9671049
interactor = UserInterface(args.quiet);
9681050

9691051
try:
970-
if args.rollback:
1052+
if args.rollback or args.force:
9711053
tarball = None
972-
elif not args.force:
1054+
else:
1055+
# For regular update or update with verification
9731056
tarball = FirmwareTarball(args.firmware, interactor,
974-
args.preserve_list)
975-
# FirmwareTarball to check firmware
1057+
args.preserve_list, args.verify)
1058+
1059+
# FirmwareTarball to check firmware compatibility
9761060
if not tarball.check_firmware():
9771061
print("OS image version must be newer than the minimal version, "
9781062
"no firmware image could be updated on this device!")
@@ -1004,7 +1088,8 @@ def main(argv):
10041088
args.backup_dir,
10051089
interactor,
10061090
args.rollback,
1007-
args.reset
1091+
args.reset,
1092+
args.verify
10081093
)
10091094

10101095
# FirmwareUpdate to rollback
@@ -1027,10 +1112,13 @@ def main(argv):
10271112
updater.update()
10281113
break
10291114
except UpgradeError as e:
1030-
if index > 2:
1115+
if (index > 2 or
1116+
e.code == ErrorCode.MISSING_SIGNATURE.value or
1117+
e.code == ErrorCode.BAD_SIGNATURE.value or
1118+
e.code == ErrorCode.MISSING_PUBLIC_KEY.value):
10311119
raise UpgradeError(e.err, e.code)
10321120
index += 1
1033-
print("{}, try again!".format(e.err))
1121+
print("{}, trying again!".format(e.err))
10341122
except UpgradeError as e:
10351123
print(e.err)
10361124
input_reminder = '''
@@ -1056,7 +1144,6 @@ Hit the Enter Key to Exit:
10561144
return ErrorCode.SUCCESS.value
10571145
os.system('reboot')
10581146

1059-
10601147
if __name__ == '__main__':
10611148
CODE = main(sys.argv)
1062-
sys.exit(CODE)
1149+
sys.exit(CODE)

meta-example/recipes-app/iot2050-firmware-update/iot2050-firmware-update_1.0.1.bb renamed to meta-example/recipes-app/iot2050-firmware-update/iot2050-firmware-update_1.1.0.bb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
# COPYING.MIT file in the top-level directory.
1010
#
1111

12-
PR = "2"
12+
PR = "1"
1313

1414
DESCRIPTION = "OSPI Firmware Update Scripts"
1515
MAINTAINER = "chao.zeng@siemens.com"
1616

1717
SRC_URI = " \
1818
file://update.conf.json.tmpl \
19-
file://iot2050-firmware-update.tmpl"
19+
file://iot2050-firmware-update.tmpl \
20+
file://custMpk.crt"
2021

2122
OS_VERSION_KEY ?= "BUILD_ID"
2223
MIN_OS_VERSION ?= "V01.01.01"
@@ -28,14 +29,19 @@ DPKG_ARCH = "any"
2829

2930
inherit dpkg-raw
3031

31-
DEBIAN_DEPENDS = "python3-progress, python3-packaging, u-boot-tools"
32+
DEBIAN_DEPENDS = "python3-cryptography, python3-progress, python3-packaging, u-boot-tools"
33+
DEBIAN_BUILD_DEPENDS = "openssl"
3234

3335
do_install() {
3436
install -v -d ${D}/usr/sbin/
3537
install -v -m 755 ${WORKDIR}/iot2050-firmware-update ${D}/usr/sbin/
3638

3739
install -v -d ${D}/usr/share/iot2050/fwu
3840
install -v -m 644 ${WORKDIR}/update.conf.json ${D}/usr/share/iot2050/fwu/
41+
42+
openssl x509 -in ${WORKDIR}/custMpk.crt -pubkey -noout > ${WORKDIR}/public.key
43+
install -v -d ${D}/usr/share/iot2050/fwu
44+
install -v -m 644 ${WORKDIR}/public.key ${D}/usr/share/iot2050/fwu/
3945
}
4046

4147
do_deploy_deb:append() {

0 commit comments

Comments
 (0)