@@ -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
2324Example of update.conf.json:
2425{
@@ -123,6 +124,11 @@ from progress.bar import Bar
123124from threading import Thread , Event
124125from 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+
126132class 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
139147class 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
788861class 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-
10601147if __name__ == '__main__' :
10611148 CODE = main (sys .argv )
1062- sys .exit (CODE )
1149+ sys .exit (CODE )
0 commit comments