diff --git a/demo/demo_director.py b/demo/demo_director.py index 4a45c91..f00423a 100644 --- a/demo/demo_director.py +++ b/demo/demo_director.py @@ -590,7 +590,8 @@ def undo_sign_with_compromised_keys_attack(vin=None): -def add_target_to_director(target_fname, filepath_in_repo, vin, ecu_serial): +def add_target_to_director(target_fname, filepath_in_repo, vin, ecu_serial, + hardware_id = None, release_counter = None ): """ For use in attacks and more specific demonstration. @@ -615,6 +616,16 @@ def add_target_to_director(target_fname, filepath_in_repo, vin, ecu_serial): The ECU to assign this target to in the targets metadata. Complies with uptane.formats.ECU_SERIAL_SCHEMA + hardware_id + A uniques idetifier for ECU classifying the ECU based on the hardware + type. This may be used to prevent installation of images with is not + supported by the hardware type. + + release_counter + An index to track the version number of the firmware of the ECU. + This is used to prevent the roll-back attack by a compromised + director. + """ uptane.formats.VIN_SCHEMA.check_match(vin) uptane.formats.ECU_SERIAL_SCHEMA.check_match(ecu_serial) @@ -640,7 +651,7 @@ def add_target_to_director(target_fname, filepath_in_repo, vin, ecu_serial): # This calls the appropriate vehicle repository. director_service_instance.add_target_for_ecu( - vin, ecu_serial, destination_filepath) + vin, ecu_serial, destination_filepath, hardware_id, release_counter) @@ -1191,7 +1202,8 @@ def clear_vehicle_targets(vin): -def add_target_and_write_to_live(filename, file_content, vin, ecu_serial): +def add_target_and_write_to_live(filename, file_content, vin, ecu_serial, + hardware_id = None, release_counter = None): """ High-level version of add_target_to_director() that creates 'filename' and writes the changes to the live directory repository. @@ -1207,7 +1219,7 @@ def add_target_and_write_to_live(filename, file_content, vin, ecu_serial): # The path that will identify the file in the repository. filepath_in_repo = filename - add_target_to_director(filename, filepath_in_repo, vin, ecu_serial) + add_target_to_director(filename, filepath_in_repo, vin, ecu_serial, hardware_id, release_counter) write_to_live(vin_to_update=vin) diff --git a/demo/demo_image_repo.py b/demo/demo_image_repo.py index 53c3195..7156572 100644 --- a/demo/demo_image_repo.py +++ b/demo/demo_image_repo.py @@ -131,13 +131,13 @@ def clean_slate(use_new_keys=False): # Add some starting image files, primarily for use with the web frontend. - add_target_to_imagerepo('demo/images/INFO1.0.txt', 'INFO1.0.txt') - add_target_to_imagerepo('demo/images/TCU1.0.txt', 'TCU1.0.txt') - add_target_to_imagerepo('demo/images/TCU1.1.txt', 'TCU1.1.txt') - add_target_to_imagerepo('demo/images/TCU1.2.txt', 'TCU1.2.txt') - add_target_to_imagerepo('demo/images/BCU1.0.txt', 'BCU1.0.txt') - add_target_to_imagerepo('demo/images/BCU1.1.txt', 'BCU1.1.txt') - add_target_to_imagerepo('demo/images/BCU1.2.txt', 'BCU1.2.txt') + add_target_to_imagerepo('demo/images/INFO1.0.txt', 'INFO1.0.txt', 'TYPE1', '0') + add_target_to_imagerepo('demo/images/TCU1.0.txt', 'TCU1.0.txt', 'TYPE2', '0') + add_target_to_imagerepo('demo/images/TCU1.1.txt', 'TCU1.1.txt', 'TYPE2', '1') + add_target_to_imagerepo('demo/images/TCU1.2.txt', 'TCU1.2.txt', 'TYPE2', '2') + add_target_to_imagerepo('demo/images/BCU1.0.txt', 'BCU1.0.txt', 'TYPE3', '0') + add_target_to_imagerepo('demo/images/BCU1.1.txt', 'BCU1.1.txt', 'TYPE3', '1') + add_target_to_imagerepo('demo/images/BCU1.2.txt', 'BCU1.2.txt', 'TYPE3', '2') print(LOG_PREFIX + 'Signing and hosting initial repository metadata') @@ -172,7 +172,8 @@ def write_to_live(): -def add_target_to_imagerepo(target_fname, filepath_in_repo): +def add_target_to_imagerepo(target_fname, filepath_in_repo, + hardware_id = None, release_counter = None): """ For use in attacks and more specific demonstration. @@ -191,6 +192,16 @@ def add_target_to_imagerepo(target_fname, filepath_in_repo): This is the name that will identify the file in the repository, and the filepath it will have relative to the root of the repository's targets directory. + + hardware_id + A uniques idetifier for ECU classifying the ECU based on the hardware + type. This may be used to prevent installation of images with is not + supported by the hardware type. + + release_counter + An index to track the version number of the firmware of the ECU. + This is used to prevent the roll-back attack by a compromised + director. """ global repo @@ -203,7 +214,12 @@ def add_target_to_imagerepo(target_fname, filepath_in_repo): shutil.copy(target_fname, destination_filepath) - repo.targets.add_target(destination_filepath) + custom = { + 'hardware_id': hardware_id, + 'release_counter': release_counter + } + + repo.targets.add_target(destination_filepath, custom = custom) diff --git a/demo/demo_primary.py b/demo/demo_primary.py index 7293856..9c69e9c 100644 --- a/demo/demo_primary.py +++ b/demo/demo_primary.py @@ -63,6 +63,8 @@ #_client_directory_name = 'temp_primary' # name for this Primary's directory _vin = 'democar' _ecu_serial = 'INFOdemocar' +_hardware_id = 'TYEP1' +_release_counter = 0 # firmware_filename = 'infotainment_firmware.txt' @@ -80,16 +82,22 @@ def clean_slate( use_new_keys=False, # client_directory_name=None, vin=_vin, - ecu_serial=_ecu_serial): + ecu_serial=_ecu_serial, + hardware_id = _hardware_id, + release_counter = _release_counter): """ """ global primary_ecu global CLIENT_DIRECTORY global _vin global _ecu_serial + global _hardware_id + global _release_counter global listener_thread _vin = vin _ecu_serial = ecu_serial + _hardware_id = hardware_id + _release_counter = release_counter # if client_directory_name is not None: # CLIENT_DIRECTORY = client_directory_name @@ -138,6 +146,8 @@ def clean_slate( director_repo_name=demo.DIRECTOR_REPO_NAME, vin=_vin, ecu_serial=_ecu_serial, + hardware_id = _hardware_id, + release_counter = _release_counter, primary_key=ecu_key, time=clock, timeserver_public_key=key_timeserver_pub) @@ -307,7 +317,20 @@ def update_cycle(): else: raise - # All targets have now been downloaded. + except uptane.ImageRollBack: + print_banner(BANNER_DEFENDED, color=WHITE + DARK_BLUE_BG, + text='The Director has instructed us to download an image' + ' that has a bad release counter and does not match with ' + ' other repositories. This image has' + ' been rejected.', sound=TADA) + except uptane.HardwareIDMismatch: + print_banner(BANNER_DEFENDED, color=WHITE + DARK_BLUE_BG, + text='The Director has instructed us to download an image' + ' that is not meant for the stated ECU. HardwareIDdoes not' + ' match with other repositorie. This image has' + ' been rejected.', sound=TADA) + + # All targets have now been downloaded. # Generate and submit vehicle manifest. diff --git a/demo/demo_secondary.py b/demo/demo_secondary.py index e3f50b2..55f58c0 100644 --- a/demo/demo_secondary.py +++ b/demo/demo_secondary.py @@ -61,6 +61,8 @@ CLIENT_DIRECTORY = None _vin = 'democar' _ecu_serial = 'TCUdemocar' +_hardware_id = 'TYPE2' +_release_counter = 0 _primary_host = demo.PRIMARY_SERVER_HOST _primary_port = demo.PRIMARY_SERVER_DEFAULT_PORT firmware_filename = 'secondary_firmware.txt' @@ -79,13 +81,17 @@ def clean_slate( vin=_vin, ecu_serial=_ecu_serial, primary_host=None, - primary_port=None): + primary_port=None, + hardware_id = _hardware_id, + release_counter = _release_counter): """ """ global secondary_ecu global _vin global _ecu_serial + global _hardware_id + global _release_counter global _primary_host global _primary_port global nonce @@ -94,6 +100,8 @@ def clean_slate( _vin = vin _ecu_serial = ecu_serial + _hardware_id = hardware_id + _release_counter = release_counter if primary_host is not None: _primary_host = primary_host @@ -154,7 +162,9 @@ def clean_slate( ecu_key=ecu_key, time=clock, firmware_fileinfo=factory_firmware_fileinfo, - timeserver_public_key=key_timeserver_pub) + timeserver_public_key=key_timeserver_pub, + hardware_id = _hardware_id, + release_counter = _release_counter) @@ -330,7 +340,16 @@ def update_cycle(): # Now tell the Secondary reference implementation code where the archive file # is and let it expand and validate the metadata. - secondary_ecu.process_metadata(archive_fname) + try: + secondary_ecu.process_metadata(archive_fname) + except uptane.ImageRollBackAttempt: + print_banner(BANNER_DEFENDED, color=WHITE+DARK_BLUE_BG, + text='The director has instructed to download an image' + 'that has a lower release conunter as that of the' + 'already installed firmware. The image has been rejected.') + generate_signed_ecu_manifest() + submit_ecu_manifest_to_primary() + return # As part of the process_metadata call, the secondary will have saved @@ -514,6 +533,10 @@ def update_cycle(): print(open(os.path.join(CLIENT_DIRECTORY, image_fname)).read()) print('---------------------------------------------------------') + # Now hence the image is installed succesfully, update the release counter + secondary_ecu.update_release_counter( + expected_target_info['fileinfo']['custom']['release_counter'] + ) # Submit info on what is currently installed back to the Primary. generate_signed_ecu_manifest() diff --git a/tests/test_primary.py b/tests/test_primary.py index f7087a9..1281cec 100644 --- a/tests/test_primary.py +++ b/tests/test_primary.py @@ -65,6 +65,8 @@ NONCE = 5 VIN = 'democar' PRIMARY_ECU_SERIAL = '00000' +PRIMARY_HARDWARE_IDENTIFIER = '' +PRIMARY_RELEASE_COUNTER = 0 @@ -176,6 +178,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=5, # INVALID ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id = PRIMARY_HARDWARE_IDENTIFIER, + release_counter = PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, @@ -188,6 +192,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=500, # INVALID + hardware_id=PRIMARY_HARDWARE_IDENTIFIER, + release_counter=PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, @@ -200,6 +206,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id=PRIMARY_HARDWARE_IDENTIFIER, + release_counter=PRIMARY_RELEASE_COUNTER, primary_key={''}, # INVALID time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub, @@ -212,6 +220,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id=PRIMARY_HARDWARE_IDENTIFIER, + release_counter=PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time='invalid because this is not a time', # INVALID timeserver_public_key=TestPrimary.key_timeserver_pub, @@ -224,6 +234,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id = PRIMARY_HARDWARE_IDENTIFIER, + release_counter = PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.initial_time, # INVALID my_secondaries=[]) @@ -235,6 +247,8 @@ def test_01_init(self): director_repo_name=5, #INVALID vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id = PRIMARY_HARDWARE_IDENTIFIER, + release_counter = PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key = TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -246,6 +260,8 @@ def test_01_init(self): director_repo_name= "invalid", #INVALID vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id=PRIMARY_HARDWARE_IDENTIFIER, + release_counter=PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key = TestPrimary.key_timeserver_pub, my_secondaries=[]) @@ -261,6 +277,8 @@ def test_01_init(self): director_repo_name=demo.DIRECTOR_REPO_NAME, vin=VIN, ecu_serial=PRIMARY_ECU_SERIAL, + hardware_id=PRIMARY_HARDWARE_IDENTIFIER, + release_counter=PRIMARY_RELEASE_COUNTER, primary_key=TestPrimary.ecu_key, time=TestPrimary.initial_time, timeserver_public_key=TestPrimary.key_timeserver_pub) @@ -272,6 +290,8 @@ def test_01_init(self): self.assertEqual([], TestPrimary.instance.nonces_sent) self.assertEqual(VIN, TestPrimary.instance.vin) self.assertEqual(PRIMARY_ECU_SERIAL, TestPrimary.instance.ecu_serial) + self.assertEqual(PRIMARY_HARDWARE_IDENTIFIER, TestPrimary.instance.hardware_id) + self.assertEqual(PRIMARY_RELEASE_COUNTER, TestPrimary.instance.release_counter) self.assertEqual(TestPrimary.ecu_key, TestPrimary.instance.primary_key) self.assertEqual(dict(), TestPrimary.instance.ecu_manifests) self.assertEqual( diff --git a/tests/test_secondary.py b/tests/test_secondary.py index cdd92bd..374d3b6 100644 --- a/tests/test_secondary.py +++ b/tests/test_secondary.py @@ -378,6 +378,8 @@ def test_01_init(self): ecu_serial=ecu_serial, ecu_key=TestSecondary.secondary_ecu_key, time=TestSecondary.initial_time, + hardware_id= 'TYPE2', + release_counter = 0, timeserver_public_key=TestSecondary.key_timeserver_pub, firmware_fileinfo=factory_firmware_fileinfo, director_public_key=None, diff --git a/uptane/__init__.py b/uptane/__init__.py index 176e4bf..239b792 100644 --- a/uptane/__init__.py +++ b/uptane/__init__.py @@ -75,6 +75,18 @@ class FailedToEncodeASN1DER(Error): """ pass +class HardwareIDMismatch(Error): + """ + Recieved an instruction from director install an image that dosen't match the + hardware type of this ECU. + """ + pass + +class ImageRollBackAttempt(Error): + """ + Recieved an instruction to install an image with the release counter value + lower than that of the image currently insatlled on the ECU. + """ # Logging configuration diff --git a/uptane/clients/primary.py b/uptane/clients/primary.py index f26c271..88225f2 100644 --- a/uptane/clients/primary.py +++ b/uptane/clients/primary.py @@ -227,6 +227,8 @@ def __init__( director_repo_name, # e.g. 'director'; value must appear in pinning file vin, # 'vin11111' ecu_serial, # 'ecu00000' + hardware_id, + release_counter, primary_key, time, timeserver_public_key, @@ -283,6 +285,8 @@ def __init__( self.vin = vin self.ecu_serial = ecu_serial + self.hardware_id = hardware_id + self.release_counter = release_counter self.full_client_dir = full_client_dir # TODO: Consider removing time from [time] here and starting with an empty # list, or setting time to 0 to start by default. @@ -475,6 +479,35 @@ def get_validated_target_info(self, target_filepath): 'the wrong repository specified as the Director repository in the ' 'initialization of this primary object?') + directed_targets = validated_target_info[self.director_repo_name] + + temp_release_counter = None + temp_hardware_id = None + + # Check if the hardware ID and release_counter specified in metadata + # of all the repositories correspond to each other. + + for repo_name in validated_target_info.keys(): + repo_targets = validated_target_info[repo_name] + current_hardware_id = repo_targets['fileinfo']['custom']['hardware_id'] + current_release_counter = repo_targets['fileinfo']['custom']['hardware_id'] + + if temp_release_counter is None: + temp_release_counter = current_release_counter + elif temp_release_counter != current_release_counter: + raise uptane.ImageRollBackAttempt('Bad value for the field \ + hardware_ID in the that did not correspond the value in \ + the other repos. Value did not match between the director \ + and the other repos. The value of director target is {}'.format( + repr(directed_targets))) + + if temp_hardware_id is None: + temp_hardware_id = current_hardware_id + elif temp_hardware_id != current_hardware_id: + raise uptane.HardwareIDMismatch('Bad value for the field \ + hardware_ID. It did not match to the value in \ + other repo') + # Defensive coding: this should already have been checked. tuf.formats.TARGETFILE_SCHEMA.check_match( validated_target_info[self.director_repo_name]) @@ -572,6 +605,46 @@ def primary_update_cycle(self): 'File: ' + repr(target_filepath), sound=TADA) time.sleep(3) + except uptane.HardwareIDMismatch: + log.warning(RED + 'Director has instructed us to download a target (' + + target_filepath + ') that is not validated by the combination of ' + 'Image + Director Repositories. That update IS BEING SKIPPED. It ' + 'the hardware IDs do not match between image and director ' + 'repositories. Try again, but if this happens often, you may be ' + 'connecting to an untrustworthy Director, or there may be an ' + 'untrustworthy Image Repository, or the Director and Image ' + 'Repository may be out of sync.' + ENDCOLORS) + + # If running the demo, display splash banner indicating the rejection. + # This clause should be pulled out of the reference implementation when + # possible. + if uptane.DEMO_MODE: # pragma: no cover + print_banner(BANNER_DEFENDED, color=WHITE + DARK_BLUE_BG, + text='The Director has instructed us to download a file that ' + 'does not exactly match the Image Repository metadata. ' + 'File: ' + repr(target_filepath), sound=TADA) + time.sleep(3) + + except uptane.ImageRollBackAttempt: + log.warning(RED + 'Dorector has instructed us to download a target (' + + target_filepath + ') that is not validated by the combination of ' + 'Image + Director Repositories. That update IS BEING SKIPPED. It ' + 'the release counters do not match between image and director ' + 'repositories. Try again, but if this happens often, you may be ' + 'connecting to an untrustworthy Director, or there may be an ' + 'untrustworthy Image Repository, or the Director and Image ' + 'Repository may be out of sync.' + ENDCOLORS) + + # If running the demo, display splash banner indicating the rejection. + # This clause should be pulled out of the reference implementation when + # possible. + if uptane.DEMO_MODE: # pragma: no cover + print_banner(BANNER_DEFENDED, color=WHITE + DARK_BLUE_BG, + text='The Director has instructed us to download a file that ' + 'does not exactly match the Image Repository metadata. ' + 'File: ' + repr(target_filepath), sound=TADA) + time.sleep(3) + # # Grab a filepath from each of the dicts of target file infos. (Each dict # # corresponds to one file, and the filepaths in all the infos in that dict diff --git a/uptane/clients/secondary.py b/uptane/clients/secondary.py index 88ea8fe..9ccaac6 100644 --- a/uptane/clients/secondary.py +++ b/uptane/clients/secondary.py @@ -177,6 +177,8 @@ def __init__( ecu_key, time, timeserver_public_key, + hardware_id, + release_counter, firmware_fileinfo=None, director_public_key=None, partial_verifying=False): @@ -244,6 +246,8 @@ def __init__( self.ecu_key = ecu_key self.vin = vin self.ecu_serial = ecu_serial + self.hardware_id = hardware_id + self.release_counter = release_counter self.full_client_dir = full_client_dir self.director_proxy = None self.timeserver_public_key = timeserver_public_key @@ -550,8 +554,25 @@ def fully_validate_metadata(self): # Ignore target info not marked as being for this ECU. if 'custom' not in target['fileinfo'] or \ 'ecu_serial' not in target['fileinfo']['custom'] or \ + 'hardware_id' not in target['fileinfo']['custom'] or \ + 'release_counter' not in target['fileinfo']['custom']or \ self.ecu_serial != target['fileinfo']['custom']['ecu_serial']: continue + elif self.hardware_id != \ + target['fileinfo']['custom']['hardware_id']: + + raise uptane.HardwareIDMismatch('Recieved a instruction' + 'by the director install a target for which the' + 'hardware id does not match with that of the ECU. ' + 'Disregarding the target') + + elif self.release_counter > \ + int(target['fileinfo']['custom']['release_counter']): + + raise uptane.ImageRollBackAttempt('Recieved a instruction from the' + 'director to install a image for which the release counter is' + 'less than that of the firmware currently installed on the ECU.' + 'Disregardng the target') # Fully validate the target info for our target(s). try: @@ -619,6 +640,17 @@ def process_metadata(self, metadata_archive_fname): + def update_release_counter(self, new_release_counter): + """ + To update the current release counter of the ECU with + the new release counter of the firmware + """ + + self.release_counter = new_release_counter + + + + def _expand_metadata_archive(self, metadata_archive_fname): """ Given the filename of an archive of metadata files validated and zipped by diff --git a/uptane/services/director.py b/uptane/services/director.py index df9a259..bc3ba5b 100644 --- a/uptane/services/director.py +++ b/uptane/services/director.py @@ -526,7 +526,8 @@ def create_director_repo_for_vehicle(self, vin): - def add_target_for_ecu(self, vin, ecu_serial, target_filepath): + def add_target_for_ecu(self, vin, ecu_serial, target_filepath, + hardware_id = None, release_counter = None): """ Add a target to the repository for a vehicle, marked as being for a specific ECU. @@ -549,6 +550,10 @@ def add_target_for_ecu(self, vin, ecu_serial, target_filepath): # elif ecu_serial not in inventory.ecu_public_keys: # raise uptane.UnknownECU('The ECU Serial provided, ' + repr(ecu_serial) + # ' is not that of an ECU known to this Director.') - + custom = { + 'ecu_serial' : ecu_serial, + 'hardware_id' : hardware_id, + 'release_counter' : release_counter + } self.vehicle_repositories[vin].targets.add_target( - target_filepath, custom={'ecu_serial': ecu_serial}) + target_filepath, custom=custom)